多线程笔记--先了解工具
前言:
之所以要学习一下多线程,最主要的目的是要学习多线程间的同步互斥等控制,而不是学习多线程本身,或者怎么用程序编写多线程。最重要的是利用程序控制多线程,防止在多线程环境中发生死锁的现象发生,这才是这个系列的笔记的中心,其它的都是了解的东西。人的精力和时间真的是有限的,至少我是这样的。所以抓住主线的东西,那些细枝末节在用到的时候,看一下就可以了,根本不用记住,了解它,知道他是怎么回事,是个什么东西就好了。在开始学习多线程之前,先了解一下他的一些语言的工具,然后在利用多线程学习线程之间的同步和互斥关系。
首先用C\C++语言对多线程做一个入门的了解,深入的分析CreateThread与_beginthreadx的本质区别。
首先看下面的代码,先写出来一个多线程的程序来:
#include <stdio.h> #include <windows.h> //子线程的函数 //DWORD是unsigned long //WINAPI是_stdcall表明函数参数的入栈的顺序:表明是函数参数列表的入栈顺序是从左向右还是从右向左 //LPVOID是void* DWORD WINAPI ThreadFun(LPVOID PM) { printf("子线程的ID号为:%d\n", GetCurrentThreadId()); printf("子线程输出Hello World!\n"); return 0; } int main() { //HANDLE是void*,void*可以接受任何指针类型的数值对它进行赋值行为 HANDLE handle = CreateThread(NULL, 0, ThreadFun, NULL, 0, NULL); WaitForSingleObject(handle, INFINITE); return 0; }
运行的结果:
下面说一下用到的两个函数:
(1)CreateThread函数
HANDLE WINAPI CreateThread( LPSECURITY_ATTRIBUTES lpThreadAttributes, SIZE_T dwStackSize, LPTHREAD_START_ROUTINE lpStartAddress, LPVOID lpParameter, DWORD dwCreationFlags, LPDWORD lpThreadId ); //函数说明:创建一个新的线程 //第一个参数表示线程内核对象的安全属性,一般传入NULL表示使用默认。它是一个指向SECURITY_ATTRIBUTES型态结构的指针。在Windows 98中忽略该参数。在Windows NT中,它被设为NULL //第二个参数表示新线程的初始堆栈大小,默认值为0(1MB)。在任何情况下,Windows根据需要动态延长堆栈的大小 //第三个参数表示新线程所执行的线程函数的地址,多个线程可以使用同一个函数地址,函数名称没有限制,但是必须以下列形式声明:DWORD WINAPI ThreadProc (PVOID pParam); //第四个参数表示传递给线程函数ThreadProc的参数。这样主线程和从属线程就可以共享数据。 //第五个参数指定额外的标志来控制线程的创建,为0表示创建线程之后立即就可以进行调度,如果为CREATE_SUSPENDED则表示线程创建后暂停运行,这样就无法调度,直到调用ResumeThread() //第六个参数是指向DWORD的一个指针,将获得线程的ID号,传入NULL表示不需要返回线程ID号。 //函数返回值:创建成功返回线程的句柄,失败返回NULL
注意:线程互斥的提前说明,临界区要在线程执行前初始化,因为线程一但被建立即开始运行(除非手工挂起),在线程建立后在初始化临界区可能出现问题
如:孙鑫MFC中的现象
(2)WaitForSingleObject
//WaitForMultipleObjects函数说明 等待函数可使线程自愿进入等待状态,直到一个特定的内核对象变为已通知状态为止。这些等待函数中最常用的是WaitForSingleObject: DWORD WaitForSingleObject(HANDLE hObject, DWORD dwMilliseconds); 当线程调用该函数时,第一个参数hObject标识一个能够支持被通知/未通知的内核对象。 第二个参数dwMilliseconds.允许该线程指明,为了等待该对象变为已通知状态,它将等待多长时间。 调用下面这个函数将告诉系统,调用函数准备等待到hProcess句柄标识的进程终止运行为止: WaitForSingleObject(hProcess, INFINITE); 第二个参数告诉系统,调用线程愿意永远等待下去(无限时间量),直到该进程终止运行。 通常情况下, INFINITE是作为第二个参数传递给WaitForSingleObject的,不过也可以传递任何一个值(以毫秒计算)。 顺便说一下, INFINITE已经定义为0xFFFFFFFF(或-1)。 当然,传递INFINITE有些危险。如果对象永远不变为已通知状态,那么调用线程永远不会被唤醒,它将永远处于死锁状态, 不过,它不会浪费宝贵的CPU时间。 下面是如何用一个超时值而不是INFINITE来调用WaitForSingleObject的例子: DWORD dw = WaitForSingleObject(hProcess, 5000); switch(dw) { case WAIT_OBJECT_0: // The process terminated. break; case WAIT_TIMEOUT: // The process did not terminate within 5000 milliseconds. break; case WAIT_FAILED: // Bad call to function (invalid handle?) break; } 上面这个代码告诉系统,在特定的进程终止运行之前,或者在5000ms时间结束之前,调用线程不应该变为可调度状态。 因此,如果进程终止运行,那么这个函数调用将在不到5000ms的时间内返回,如果进程尚未终止运行,那么它在大约5000ms时间内返回。 注意,不能为dwMilliseconds传递0。如果传递了0,WaitForSingleObject函数将总是立即返回。 WaitForSingleObject的返回值能够指明调用线程为什么再次变为可调度状态。 如果线程等待的对象变为已通知状态,那么返回值是WAIT_OBJECT_0。 如果设置的超时已经到期,则返回值是WAIT_TIMEOUT。 如果将一个错误的值(如一个无效句柄)传递给WaitForSingleObject,那么返回值将是WAIT_FAILED(若要了解详细信息,可调用GetLastError)。 下面这个函数WaitForMultipleObjects与WaitForSingleObject函数很相似,区别在于它允许调用线程同时查看若干个内核对象的已通知状态: DWORD WaitForMultipleObjects(DWORD dwCount, CONST HANDLE* phObjects, BOOL fWaitAll, DWORD dwMilliseconds); dwCount参数用于指明想要让函数查看的内核对象的数量。这个值必须在1与MAXIMUM_WAIT_OBJECTS(在Windows头文件中定义为64)之间。 phObjects参数是指向内核对象句柄的数组的指针。 可以以两种不同的方式来使用WaitForMultipleObjects函数。 一种方式是让线程进入等待状态,直到指定内核对象中的任何一个变为已通知状态。 另一种方式是让线程进入等待状态,直到所有指定的内核对象都变为已通知状态。 fWaitAll参数告诉该函数,你想要让它使用何种方式。如果为该参数传递TRUE,那么在所有对象变为已通知状态之前,该函数将不允许调用线程运行。 dwMilliseconds参数的作用与它在WaitForSingleObject中的作用完全相同。如果在等待的时候规定的时间到了,那么该函数无论如何都会返回。 同样,通常为该参数传递INFINITE,但是在编写代码时应该小心,以避免出现死锁情况。 WaitForMultipleObjects函数的返回值告诉调用线程,为什么它会被重新调度。可能的返回值是WAIT_FAILED和WAIT_TIMEOUT,这两个值的作用是很清楚的。 如果fWaitAll参数传递TRUE,同时所有对象均变为已通知状态,那么返回值是WAIT_OBJECT_0。 如果为fWaitAll传递FALSE,那么一旦任何一个对象变为已通知状态,该函数便返回。 在这种情况下,你可能想要知道哪个对象变为已通知状态。返回值是WAIT_OBJECT_0 与(WAIT_OBJECT_0 + dwCount-1)之间的一个值。 换句话说,如果返回值不是WAIT_TIMEOUT,也不是WAIT_FAILED,那么应该从返回值中减去WAIT_OBJECT_0。 产生的数字是作为第二个参数传递给WaitForMultipleObjects的句柄数组中的索引。该索引说明哪个对象变为已通知状态。
在这里为什么要加上这个WaitForSingleObject函数呢?
因为对于WIN32控制台程序,如果main线程在一个时间片内便已经运行完成,那么所有的资源都会被系统释放,子线程便不能得到时间片运行。所以要保证main线程的运行时间足够的长,保证子线程能够获得时间片运行。
CreateThread()函数是Windows提供的API接口,在C/C++语言另有一个创建线程的函数_beginthreadex(),在很多书上(包括《Windows核心编程》)提到过尽量使用_beginthreadex()来代替使用CreateThread(),这是为什么了?下面就来探索与发现它们的区别吧。
多线程开篇:
首先要从标准C运行库与多线程的矛盾说起,标准C运行库在1970年被实现了,由于当时没任何一个操作系统提供对多线程的支持。因此编写标准C运行库的程序员根本没考虑多线程程序使用标准C运行库的情况。比如标准C运行库的全局变量errno。很多运行库中的函数在出错时会将错误代号赋值给这个全局变量,这样可以方便调试。但如果有这样的一个代码片段:
if (system("notepad.exe readme.txt") == -1) { switch(errno) { ...//错误处理代码 } }
假设某个线程A在执行上面的代码,该线程在调用system()之后且尚未调用switch()语句时另外一个线程B启动了,这个线程B也调用了标准C运行库的函数,不幸的是这个函数执行出错了并将错误代号写入全局变量errno中。这样线程A一旦开始执行switch()语句时,它将访问一个被B线程改动了的errno。这种情况必须要加以避免!因为不单单是这一个变量会出问题,其它像strerror()、strtok()、tmpnam()、gmtime()、asctime()等函数也会遇到这种由多个线程访问修改导致的数据覆盖问题。
为了解决这个问题,Windows操作系统提供了这样的一种解决方案——每个线程都将拥有自己专用的一块内存区域来供标准C运行库中所有有需要的函数使用。而且这块内存区域的创建就是由C/C++运行库函数_beginthreadex()来负责的。下面列出_beginthreadex()函数的源代码(我在这份代码中增加了一些注释)以便读者更好的理解_beginthreadex()函数与CreateThread()函数的区别
_MCRTIMP uintptr_t __cdecl _beginthreadex( void *security, unsigned stacksize, unsigned (__CLR_OR_STD_CALL * initialcode) (void *), void * argument, unsigned createflag, unsigned *thrdaddr ) { _ptiddata ptd; //pointer to per-thread data 见注1 uintptr_t thdl; //thread handle 线程句柄 unsigned long err = 0L; //Return from GetLastError() unsigned dummyid; //dummy returned thread ID 线程ID号 // validation section 检查initialcode是否为NULL _VALIDATE_RETURN(initialcode != NULL, EINVAL, 0); //Initialize FlsGetValue function pointer __set_flsgetvalue(); //Allocate and initialize a per-thread data structure for the to-be-created thread. //相当于new一个_tiddata结构,并赋给_ptiddata指针。 if ( (ptd = (_ptiddata)_calloc_crt(1, sizeof(struct _tiddata))) == NULL ) goto error_return; // Initialize the per-thread data //初始化线程的_tiddata块即CRT数据区域 见注2 _initptd(ptd, _getptd()->ptlocinfo); //设置_tiddata结构中的其它数据,这样这块_tiddata块就与线程联系在一起了。 ptd->_initaddr = (void *) initialcode; //线程函数地址 ptd->_initarg = argument; //传入的线程参数 ptd->_thandle = (uintptr_t)(-1); #if defined (_M_CEE) || defined (MRTDLL) if(!_getdomain(&(ptd->__initDomain))) //见注3 { goto error_return; } #endif // defined (_M_CEE) || defined (MRTDLL) // Make sure non-NULL thrdaddr is passed to CreateThread if ( thrdaddr == NULL )//判断是否需要返回线程ID号 thrdaddr = &dummyid; // Create the new thread using the parameters supplied by the caller. //_beginthreadex()最终还是会调用CreateThread()来向系统申请创建线程 if ( (thdl = (uintptr_t)CreateThread( (LPSECURITY_ATTRIBUTES)security, stacksize, _threadstartex, (LPVOID)ptd, createflag, (LPDWORD)thrdaddr)) == (uintptr_t)0 ) { err = GetLastError(); goto error_return; } //Good return return(thdl); //线程创建成功,返回新线程的句柄. //Error return error_return: //Either ptd is NULL, or it points to the no-longer-necessary block //calloc-ed for the _tiddata struct which should now be freed up. //回收由_calloc_crt()申请的_tiddata块 _free_crt(ptd); // Map the error, if necessary. // Note: this routine returns 0 for failure, just like the Win32 // API CreateThread, but _beginthread() returns -1 for failure. //校正错误代号(可以调用GetLastError()得到错误代号) if ( err != 0L ) _dosmaperr(err); return( (uintptr_t)0 ); //返回值为NULL的效句柄 }
讲解下部分代码:
注1._ptiddataptd;中的_ptiddata是个结构体指针。在mtdll.h文件被定义:
typedefstruct_tiddata * _ptiddata
微软对它的注释为Structure for each thread's data。这是一个非常大的结构体,有很多成员。本文由于篇幅所限就不列出来了。
注2._initptd(ptd, _getptd()->ptlocinfo);微软对这一句代码中的getptd()的说明为:
/* return address of per-thread CRT data */
_ptiddata __cdecl_getptd(void);
对_initptd()说明如下:
/* initialize a per-thread CRT data block */
void__cdecl_initptd(_Inout_ _ptiddata _Ptd,_In_opt_ pthreadlocinfo _Locale);
注释中的CRT (C Runtime Library)即标准C运行库。
注3.if(!_getdomain(&(ptd->__initDomain)))中的_getdomain()函数代码可以在thread.c文件中找到,其主要功能是初始化COM环境。
由上面的源代码可知,_beginthreadex()函数在创建新线程时会分配并初始化一个_tiddata块。这个_tiddata块自然是用来存放一些需要线程独享的数据。事实上新线程运行时会首先将_tiddata块与自己进一步关联起来。然后新线程调用标准C运行库函数如strtok()时就会先取得_tiddata块的地址再将需要保护的数据存入_tiddata块中。这样每个线程就只会访问和修改自己的数据而不会去篡改其它线程的数据了。因此,如果在代码中有使用标准C运行库中的函数时,尽量使用_beginthreadex()来代替CreateThread()。相信阅读到这里时,你会对这句简短的话有个非常深刻的印象,如果有面试官问起,你也可以流畅准确的回答了。
接下来利用函数_beginthreadex来创建新的线程,这个函数的头文件是<process.h>:
#include <stdio.h> #include <windows.h> #include <process.h> //子线程函数 unsigned int _stdcall ThreadFun(PVOID PM) { printf("线程ID号为%4d的子线程说: Hello World\n", GetCurrentThreadId()); return 0; } //主函数,所谓主函数其实就是主线程执行的函数 int main() { const int THREAD_NUM = 5; HANDLE handle[THREAD_NUM]; for(int i = 0; i < THREAD_NUM; i++) { handle[i] = (HANDLE)_beginthreadex(NULL, 0, ThreadFun, NULL, 0, NULL); } WaitForMultipleObjects(THREAD_NUM, handle, TRUE, INFINITE); return 0; }
执行结果:
WaitForMultipleObjects这个函数解释:
DWORD WaitForMultipleObjects( DWORD nCount, // number of handles in the handle array CONST HANDLE *lpHandles, // pointer to the object-handle array BOOL fWaitAll, // wait flag DWORD dwMilliseconds // time-out interval in milliseconds ); 其中参数 nCount 句柄的数量最大值为MAXIMUM_WAIT_OBJECTS(64) HANDLE 句柄数组的指针。 HANDLE 类型可以为(Event,Mutex,Process,Thread,Semaphore )数组 BOOL bWaitAll 等待的类型,如果为TRUE 则等待所有信号量有效在往下执行,FALSE 当有其中一个信号量有效时就向下执行 DWORD dwMilliseconds 超时时间 超时后向执行。 如果为WSA_INFINITE 永不超时。如果没有信号量就会在这死等。
图中每个子线程说的都是同一句话,不太好看。能不能来一个线程报数功能,即第一个子线程输出1,第二个子线程输出2,第三个子线程输出3,……。要实现这个功能似乎非常简单——每个子线程对一个全局变量进行递增并输出就可以了。
//子线程报数 #include <stdio.h> #include <process.h> #include <windows.h> int g_nCount; //子线程函数 unsigned int __stdcall ThreadFun(PVOID pM) { g_nCount++; printf("线程ID号为%4d的子线程报数%d\n", GetCurrentThreadId(), g_nCount); return 0; } //主函数,所谓主函数其实就是主线程执行的函数。 int main() {const int THREAD_NUM = 10; HANDLE handle[THREAD_NUM]; g_nCount = 0; for (int i = 0; i < THREAD_NUM; i++) handle[i] = (HANDLE)_beginthreadex(NULL, 0, ThreadFun, NULL, 0, NULL); WaitForMultipleObjects(THREAD_NUM, handle, TRUE, INFINITE); return 0; }
这种报数的方式在逻辑上似乎是合理的,但是在多线程环境下这会产生严重的问题。
原文地址:http://blog.csdn.net/morewindows/article/details/7421759