文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

网络安全编程:多线程编程基础知识

2024-12-03 10:31

关注

 线程是进程中的一个执行单位(每个进程都必须有一个主线程),一个进程可以有多个线程,而一个线程只存在于一个进程中。在数据关系上,进程与线程是一对多的关系。线程不拥有系统资源,线程所使用的资源全部由进程向系统申请,线程拥有的是CPU的时间片。

在单处理器上(或单核处理器上),同一个进程中的不同线程交替得到CPU的时间片。在多处理器上(或多核处理器上),不同的线程可以同时运行在不同的CPU上,这样可以提高程序运行的效率。除此之外,在有些方面必须使用多线程。比如,如果在扫描磁盘并同时在程序界面上同步显示当前扫描的位置时,必须使用多线程。因为在程序界面上显示和磁盘的扫描工作在同一个线程中,而且界面也在不停进行重新显示,这样就会导致软件看起来像是卡死一样。在这种情况下,分为两个线程就可以解决该问题,界面的显示由主线程完成,而扫描磁盘的工作由另外一个线程完成,两个线程协同工作,这样就可以达到实时显示当前扫描状态的效果了。

首先了解一下线程的创建。线程的创建使用CreateThread()函数,该函数的原型如下: 

  1. HANDLE CreateThread(  
  2.  LPSECURITY_ATTRIBUTES lpThreadAttributes, // SD  
  3.  DWORD dwStackSize, // initial stack size  
  4.  LPTHREAD_START_ROUTINE lpStartAddress, // thread function  
  5.  LPVOID lpParameter, // thread argument  
  6.  DWORD dwCreationFlags, // creation option  
  7.  LPDWORD lpThreadId // thread identifier  
  8. ); 

参数说明如下。

lpThreadAttributes:指明创建线程的安全属性,为指向 SECURITY_ATTRIBUTES 结构的指针,该参数一般设置为 NULL。

dwStackSize:指定线程使用缺省的堆栈大小,如果为 NULL,则与进程主线程栈相同。

lpStartAddress:指定线程函数,线程即从该函数的入口处开始运行,函数返回时就意味着线程终止运行,该函数属于一个回调函数。线程函数的定义形式如下: 

  1. DWORD WINAPI ThreadProc(  
  2.  LPVOID lpParameter // thread data  
  3. ); 

线程函数的返回值为DWORD类型,线程函数只有一个参数,该参数在CreateThread()函数中给出。该函数的函数名称可以任意给定。很多时候并不能保证执行了CreateThread()函数后线程就会立即启动,线程的启动需要等待CPU的调度,CPU将时间片给该线程时,该线程才会执行,当然这个时间短到可以忽略它。

lpParameter:该参数表示传递给线程函数的一个参数,可以是指向任意数据类型的指针。这里是一个指针,可以方便的将多个参数通过结构体等一次性传到线程函数中。

dwCreationFlags:该参数指明创建线程后的线程状态,在创建线程后可以让线程立刻执行(这里的立即执行的意思是不会受人为的去让它处于等待状态),也可以让线程处于暂停状态。如果需要立刻执行,该参数设置为 0;如果要让线程处于暂停状态,那么该参数设置为 CREATE_SUSPENDED,待需要线程执行时调用ResumeThread()函数让线程的状态调整为等待运行的状态,然后由 CPU 分配时间片后去执行。

lpThreadId:该参数用于返回新创建线程的线程 ID。

如果线程创建成功,该函数返回线程的句柄,否则返回NULL。创建新线程后,该线程就开始启动执行了。但如果在dwCreationFlags中使用了CREATE_SUSPENDED参数,那么线程并不马上执行,而是先挂起,等到调用ResumeThread后才开始启动线程。线程的句柄需要通过CloseHandle()进行关闭,以便释放资源。

写一个简单的多线程的例子,代码如下: 

  1. #include <windows.h>  
  2. #include <stdio.h>  
  3. DWORD WINAPI ThreadProc(LPVOID lpParam)  
  4.  
  5.   printf("ThreadProc \r\n");  
  6.   return 0;  
  7.  
  8. int main()  
  9.  
  10.   HANDLE hThread = CreateThread(NULL,0,ThreadProc,NULL,0,NULL);  
  11.   printf("main \r\n");  
  12.   CloseHandle(hThread);  
  13.   return 0;  

代码在主线程中打印一行“main”,在创建的新线程中会打印一行“ThreadProc”。编译运行,查看其运行结果,如图1所示。

图1  多线程程序输出结果

从图1中看出,程序的输出跟预期的结果并不相同。程序的问题出在了哪里呢?每个线程都有属于自己的CPU时间片,当主线程创建新线程后,主线程的CPU时间片并未结束,它会向下继续执行。由于主线程的代码非常少,因此主线程在CPU分配的时间片中就执行完成并退出了。由于主线程的结束,意味着进程也就结束并退出了。因此,在代码中创建的线程虽然被创建了,但是根本就没有执行的机会。那么在这么短的代码中,如何保证新创建的线程在主线程结束前就能得到执行呢?或者说,主线程的运行需要等待新线程的完成才得以执行。这里需要使用WaitForSingleObject()函数,该函数的原型如下: 

  1. DWORD WaitForSingleObject(  
  2.  HANDLE hHandle, // handle to object  
  3.  DWORD dwMilliseconds // time-out interval  
  4. ); 

参数说明如下。

hHandle:该参数为要等待的对象句柄。

dwMilliseconds:该参数指定等待超时的毫秒数,如果设为 0,则立即返回,如果设为 INFINITE,则表示一直等待线程函数的返回。INFINITE 是系统定义的一个宏,其定义如下。

  1. #define INFINITE 0xFFFFFFFF 

如果该函数失败,则返回WAIT_FAILED;如果等待的对象编程激发状态,则返回WAIT_ OBJECT_0;如果等待对象变成激发状态之前,等待时间结束了,将返回WAIT_TIMEOUT。

修改上面的代码,在CreateThread()函数后面加入如下代码: 

  1. WaitForSingleObject(hThread, INFINITE); 

添加WaitForSingleObject()函数以后,主线程会等待新创建的线程结束再继续向下执行主线程后续的代码。这样在控制台上的输出如图2所示。

图2  主线程等待子线程的执行

WaitForSingleObject()只能等待一个线程,可是在程序中往往要创建多个线程来执行,那么如果需要等待若干个线程的完成状态的话,WaitForSingleObject()函数就无能为力了。不过,系统除了提供WaitForSingleObject()函数外,还提供了另外一个可以等待多个线程的完成状态的函数WaitForMultipleObjects(),该函数的定义如下: 

  1. DWORD WaitForMultipleObjects(  
  2.  DWORD nCount, // number of handles in array  
  3.  CONST HANDLE *lpHandles, // object-handle array  
  4.  BOOL fWaitAll, // wait option  
  5.  DWORD dwMilliseconds // time-out interval  
  6. ); 

该函数的参数比WaitForSingleObject()函数多2个参数,下面介绍这些参数。

nCount:该参数用于指明想要让函数等待的线程的数量。该参数的取值范围在 1 到 MAXIMUM_WAIT _OBJECTS 之间。

lpHandles:该参数是指向等待线程句柄的数组指针。

fWaitAll:该参数表示是否等待全部线程的状态完成,如果设置为 TRUE,则等待全部。

dwMilliseconds:该参数与 WaitForSingleObject()函数中的 dwMilliseconds 用法相同。

WaitForSingleObject()和WaitForMultipleObjects()两个函数除了可以等待线程外,还可以等待用于多线程同步和互斥的内核对象。

在使用多线程的时候常常需要考虑和注意的问题很多。比如多线程同时对一个共享资源进行操作,通过线程需要按照一定的顺序执行等。看一个简单的多线程例子: 

  1. int g_Num_One = 0 
  2. DWORD WINAPI ThreadProc(LPVOID lpParam)  
  3.  
  4.   int nTmp = 0 
  5.   for ( int i = 0; i < 10; i ++ )  
  6.   {  
  7.     nTmp = g_Num_One 
  8.     nTmp ++;  
  9.     // Sleep(1)的作用是让出 CPU  
  10.     // 使其他线程被调度运行  
  11.     Sleep(1); 
  12.     g_Num_One = nTmp 
  13.   }  
  14.   return 0;  

每个线程都有一个CPU时间片,当自己的时间片运行完成后,CPU会停止该线程的运行,并切换到其他线程去运行。当多线程同时操作一个共享资源时,这样的切换会带来隐形的问题。这里的代码比较短,在一个CPU时间片内肯定会完成,无法体现出因线程切换而产生的错误。为了达到能够因线程切换导致的错误,在代码中加入了Sleep(1),使得线程主动让出CPU,让CPU进行线程切换。在代码中,线程处理的共享资源是全局变量g_Num_One变量。主函数创建线程的代码如下: 

  1. int main()  
  2.  
  3.   HANDLE hThread[10] = { 0 };  
  4.   int i;  
  5.   for ( i = 0; i < 10; i ++ )  
  6.   {  
  7.     hThread[i] = CreateThread(NULL, 0, ThreadProc, NULL, 0, NULL);  
  8.   }  
  9.   WaitForMultipleObjects(10, hThread, TRUE, INFINITE);  
  10.   for ( i = 0; i < 10; i ++ )  
  11.   {  
  12.     CloseHandle(hThread[i]);  
  13.   }  
  14.   printf("g_Num_One = %d \r\n", g_Num_One);  
  15.   return 0;  

在主函数中,通过CreateThread()创建了10个线程,每个线程都让g_Num_One自增10次,每次的增量为1。那么10个线程会使得g_Num_One的结果变成100。编译运行上面的代码,查看输出结果,如图3所示。

图3  多线程操作共享资源的错误结果

这个结果和预测的结果并不相同。为什么会产生这种不同呢?这里进行一次模拟分析。为了方便分析,把线程的数量缩小为两个线程,分别是A线程和B线程。

① g_Num_One的初始值为0。

② 当A线程中执行nTmp = g_Num_One和nTmp++后(此时nTmp的值为1),因为Sleep(1)的原因发生了线程切换,此时g_Num_One的初始值仍然为0。

③ 当B线程中执行nTmp = g_Num_One和nTmp++后(此时nTmp的值也为1),因为Sleep(1)的原因又发生了线程切换。

④ A线程执行g_Num_One = nTmp,此时g_Num_One的值为1,接着执行下一次循环中的nTmp = g_Num_One和nTmp++的操作,又进行切换。

⑤ B线程执行g_Num_One = nTmp,此时g_Num_One的值为1。

到第⑤步时,不继续往下分析了,已经可以看出原因。g_Num_One的值是最后一次nTmp进行赋值后的值(线程中的局部变量属于线程内私有的,虽然是同一个线程函数,但是nTmp在每个线程中是私有的)。

解决该问题,这里使用的是临界区。临界区对象是一个CRITICAL_SECTION的数据结构,Windows操作系统使用该数据结构对关键代码进行保护,以确保多线程下的共享资源。在同一时间内,Windows只允许一个线程进入临界区。

临界区的函数有4个,分别是初始化临界区对象(InitializeCriticalSection())、进入临界区(EnterCriticalSection())、离开临界区(LeaveCriticalSection())和删除临界区对象(DeleteCriticalSection())。临界区很好的保护了共享资源,临界区在现实生活中有很多类似的例子。比如,在进行体检的时候,一个体检室内只有一个体检医生,体检医生会叫一个患者进去体检,这时其他人是不能进入的,当这个患者离开后,下一个患者才可以进入。这里体检医生就是一个共享的资源,而每个体检的患者是多个不同的线程。临界区就是以类似的方式保护了共享资源不被破坏的。下面依次来看一下这四个函数关于临界区的函数的定义,分别如下: 

  1. VOID InitializeCriticalSection(  
  2.  LPCRITICAL_SECTION lpCriticalSection // critical section  
  3. );  
  4. VOID EnterCriticalSection(  
  5.  LPCRITICAL_SECTION lpCriticalSection // critical section  
  6. );  
  7. VOID LeaveCriticalSection(  
  8.  LPCRITICAL_SECTION lpCriticalSection // critical section  
  9. );  
  10. VOID DeleteCriticalSection(  
  11.  LPCRITICAL_SECTION lpCriticalSection // critical section  
  12. ); 

这4个API函数的参数都是指向CRITICAL_SECTION结构体的指针。修改上面有问题的代码,修改后的代码如下: 

  1. #include <windows.h>  
  2. #include <stdio.h>  
  3. int g_Num_One = 0 
  4. CRITICAL_SECTION g_cs;  
  5. DWORD WINAPI ThreadProc(LPVOID lpParam)  
  6.  
  7.   int nTmp = 0 
  8.   for ( int i = 0; i < 10; i ++ )  
  9.   {  
  10.     // 进入临界区  
  11.     EnterCriticalSection(&g_cs);  
  12.     nTmp = g_Num_One 
  13.     nTmp ++;  
  14.     Sleep(1);  
  15.     g_Num_One = nTmp 
  16.     // 离开临界区  
  17.     LeaveCriticalSection(&g_cs);  
  18.   }  
  19.   return 0;  
  20.  
  21. int main()  
  22.  
  23.   InitializeCriticalSection(&g_cs);  
  24.   HANDLE hThread[10] = { 0 };  
  25.   int i;  
  26.   for ( i = 0; i < 10; i ++ )  
  27.   {  
  28.     hThread[i] = CreateThread(NULL, 0, ThreadProc, NULL, 0, NULL);  
  29.   }  
  30.   WaitForMultipleObjects(10, hThread, TRUE, INFINITE);  
  31.   printf("g_Num_One = %d \r\n", g_Num_One);  
  32.   for ( i = 0; i < 10; i ++ )  
  33.   {  
  34.     CloseHandle(hThread[i]);  
  35.   }  
  36.   DeleteCriticalSection(&g_cs);  
  37.   return 0;  

编译以上代码并运行,输出结果为想要的正确结果,即g_Num_One的值为100。除了使用临界区以外,对于线程的同步与互斥还有其他方法,这里就不一一进行介绍了。在开发多线程程序时,要注意多线程的同步与互斥问题。临界区对象只能用于多线程的互斥。 

 

来源:计算机与网络安全内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯