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

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

创新互联基于分布式IDC数据中心构建的平台为众多户提供雅安电信机房 四川大带宽租用 成都机柜租用 成都服务器租用。

在单处理器上(或单核处理器上),同一个进程中的不同线程交替得到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  
  2. #include  
  3. DWORD WINAPI ThreadProc(LPVOID lpParam) 
  4.   printf("ThreadProc \r\n"); 
  5.   return 0; 
  6. int main() 
  7.   HANDLE hThread = CreateThread(NULL,0,ThreadProc,NULL,0,NULL); 
  8.   printf("main \r\n"); 
  9.   CloseHandle(hThread); 
  10.   return 0; 
  11. }

代码在主线程中打印一行“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.   int nTmp = 0; 
  4.   for ( int i = 0; i < 10; i ++ ) 
  5.   { 
  6.     nTmp = g_Num_One; 
  7.     nTmp ++; 
  8.     // Sleep(1)的作用是让出 CPU 
  9.     // 使其他线程被调度运行 
  10.     Sleep(1);
  11.     g_Num_One = nTmp; 
  12.   } 
  13.   return 0; 
  14. }

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

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

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

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

名称栏目:网络安全编程:多线程编程基础知识
网站URL:http://www.mswzjz.cn/qtweb/news35/285985.html

攀枝花网站建设、攀枝花网站运维推广公司-贝锐智能,是专注品牌与效果的网络营销公司;服务项目有等

广告

声明:本网站发布的内容(图片、视频和文字)以用户投稿、用户转载内容为主,如果涉及侵权请尽快告知,我们将会在第一时间删除。文章观点不代表本网站立场,如需处理请联系客服。电话:028-86922220;邮箱:631063699@qq.com。内容未经允许不得转载,或转载时需注明来源: 贝锐智能