首页 > 代码库 > Windows多线程编程及常见问题

Windows多线程编程及常见问题

 提要:

  • Windows 多线程Helloworld
  • 以Windows代码为例,分析多线程编程中易出现的问题

Windows多线程的Helloworld:

  笔者写过Java多线程的程序(实现Runnable接口,利用Thread类执行),也写过Linux多线程程序(利用pthread)。最近由于另有需要使用Windows多线程,由于Windows API历来难用,特此记录,以作备忘。

  Helloworld源代码如下:

 1 #include <stdio.h>
 2 #include <windows.h>
 3 
 4 #define THREAD_SUM 10
 5 int tmp;
 6 int sum;
 7 
 8 DWORD WINAPI ThreadProc(LPVOID para){
 9     int i;
10     for(i= 0;i<10000;i++)
11         //InterlockedExchangeAdd(&tmp,1);
12         tmp++;
13     printf("%d ",(int)para);
14     sum+=(int)para;
15     return 0;
16 }
17 
18 int main(){
19     HANDLE hThread[THREAD_SUM];
20     DWORD id;
21     int i ;
22     for(i= 0;i<THREAD_SUM;i++){
23         hThread[i] = CreateThread(NULL,0,ThreadProc, i,0,&id);
24     //    printf("%d ", id);
25     }
26     for(i=0;i<THREAD_SUM;i++){
27         WaitForSingleObject(hThread[i],INFINITE);
28     }    
29     printf("\n%d\n%d", tmp, sum);
30     return 0;
31 }

 

  重点API CreateThread 函数原型如下:

HANDLE CreateThread( //返回线程的句柄
 LPSECURITY_ATTRIBUTES lpThreadAttributes, //线程参数,常置为NULL
 SIZE_T dwStackSize , //初始线程栈大小,可置为0,系统将自行调整
 LPTHREAD_START_ROUTINE lpStartAddress, //线程函数
 LPVOID lpParameter, //传递给线程函数的参数
 DWORD dwCreationFlags,//创建线程标志,可置为NULL
 LPDWORD lpThreadId //线程号
);

  (顺便说下,Windows API 使用匈牙利命名法,即变量名=属性+类型+对象描述,如H代表handle,L代表long,P代表Pointer)

  说明:

  1)其中前两个参数常置为0。

  2)第四个参数为传递给线程的参数,通常为结构体指针,以传递更多的参数。但注意指针指向的内存通常指向的数据不应为局部变量(栈空间变化导致数据异常)。

   此参数也可做为DWORD(双字)型数据传递32位整数数据(本文代码用来传递一个int型数据)

  3)线程号为返回参数,也可置为NULL。

  4)第三个参数即为线程函数,原型如下: 

 DWORD WINAPI ThreadProc(LPVOID para)

  常见问题分析:

  1、如何等待线程。主线程通常需要在各个线程运行结束之后进行一些汇总工作。可以使用WaitForSingleObject(hThread[i],INFINITE)函数等待单个线程结束(类似Linux多线程中的pthread_join)。第一个参数为线程句柄,第二个参数为等待时间(单位毫秒,INFINITE表示一直等待,直到线程结束)。可将代码中WaitForSingleObject这一段注释掉运行,可以发现打印行数减少(甚至可能不输出),且每次数量不同。原理在于主线程结束后进程结束,各个未执行完的线程直接结束。

  2、全局变量共享。代码中各个线程共享全局变量tmp和sum。由于线程执行过程中可能在任意位置中断,因此在线程中更改全局变量的值必须考虑互斥问题。直接进行变量值更新,即如代码中所示。会出现数据异常。本段代码表现为tmp值小于10000*10。对于sum由于更新次数少,线程数也少,数据出现问题几率较小。解决这一问题,可以使用信号量,或者进行加锁等,这里调用InterlockedExchangeAdd,也可以满足要求。该函数可以实现对指定地址的互斥操作。将此段代码注释取消,tmp++代码注释掉,可以发出结果正常。注意此处使用volatile关键字修饰tmp不能解决问题,volatile本身只保证每次使用数据时将从内存读入,忽略寄存器优化,这里仍可存在内在到寄存器存取指令间的中断,从而影响结果。但volatile可用于如下情况:线程中存在循环while(flag),其他线程修改flag标志,使循环结束。

  3、代码运行中还可以发现一个问题,即各个线程printf("%d ",(int)para)结果异常,正常情况下应该输出结果为0~9的乱序,其中互不重复,但实际上,输出结果可能如下:0 0 1 3 3 4 3 4 6 7 2 5 5 8 9(随意截取一次结果)。输出中存在重复项。起初分析以为传递参数时受编译器优化导致传递参数出现问题。但分析汇编可以排除这种可能。细心观察可以发现输出结果数也大于10,可以想到问题出现在printf上。于是忽然想到printf的不可重入。所谓函数不可重入,通常指函数执行过程中不可以发生中断而执行其他的相同函数。这种情况通常由于函数存在全局或者静态变量引用,printf中即存在对stdout的引用。重入导致输出混乱。这里可以观察到sum结果为仍为45(这个结果也可能因为共享全局变量问题发生错误,但本例中更新次数较少,这种情况发生几率很低,也可以用InterlockedExchangeAdd彻底杜绝这种情况的发生),说明参数传递是正确的。