提要:
- 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彻底杜绝这种情况的发生),说明参数传递是正确的。