使用多线程的好处:
和进程相比——
1、它是一种非常"节俭"的多任务操作方式。我们知道,在Linux系统下,启动一个新的进程必须分配给它独立的地址空间,建立众多的数据表来维护它的代码段、堆栈段和数据段,这是一种"昂贵"的多任务工作方式。而运行于一个进程中的多个线程,它们彼此之间使用相同的地址空间,共享大部分数据,启动一个线程所花费的空间远远小于启动一个进程所花费的空间,而且,线程间彼此切换所需的时间也远远小于进程间切换所需要的时间。据统计,总的说来,一个进程的开销大约是一个线程开销的30倍左右,当然,在具体的系统上,这个数据可能会有较大的区别。
2、线程间方便的通信机制。对不同进程来说,它们具有独立的数据空间,要进行数据的传递只能通过通信的方式进行,这种方式不仅费时,而且很不方便。线程则不然,由于同一进程下的线程之间共享数据空间,所以一个线程的数据可以直接为其它线程所用,这不仅快捷,而且方便。(当然,数据的共享也带来其他一些问题,有的变量不能同时被两个线程所修改,有的子程序中声明为static的数据更有可能给多线程程序带来灾难性的打击,这些正是编写多线程程序时最需要注意的地方。)
自个具有——
1、提高应用程序响应。这对图形界面的程序尤其有意义,当一个操作耗时很长时,整个系统都会等待这个操作,此时程序不会响应键盘、鼠标、菜单的操作,而使用多线程技术,将耗时长的操作(time consuming)置于一个新的线程,可以避免这种尴尬的情况。
2、使多CPU系统更加有效。操作系统会保证当线程数不大于CPU数目时,不同的线程运行于不同的CPU上。3、改善程序结构。一个既长又复杂的进程可以考虑分为多个线程,成为几个独立或半独立的运行部分,这样的程序会利于理解和修改。
~~~~在实际的编程中到底应该使用CreateThread还是_beginthreadex?~~~~
1 //最简单的创建多线程实例 2 #include3 #include 4 //子线程函数 5 DWORD WINAPI ThreadFun(LPVOID pM) //"WINAPI"为调用约定,代表函数参数从右到左入栈;"LPVOID"是没有类型的指针 6 { 7 printf("子线程的线程ID号为:%d\n子线程输出Hello World\n", GetCurrentThreadId()); 8 return 0; 9 } 10 //主函数,所谓主函数其实就是主线程执行的函数。 11 int main() 12 { 13 printf(" 最简单的创建多线程实例\n"); 14 printf(" -- by MoreWindows( http://blog.csdn.net/MoreWindows ) --\n\n"); 15 16 HANDLE handle = CreateThread(NULL, 0, ThreadFun, NULL, 0, NULL); 17 WaitForSingleObject(handle, INFINITE); 18 return 0; 19 }
以上代码中出现的函数:
1、CreateThread
函数功能:创建线程
函数原型:
HANDLE WINAPI CreateThread(
LPSECURITY_ATTRIBUTES lpThreadAttributes, //线程内核对象的安全属性,一般传入NULL表示使用默认设置
SIZE_T dwStackSize, //线程栈空间大小。传入0表示使用默认大小(1MB)
LPTHREAD_START_ROUTINE lpStartAddress, //新线程所执行的线程函数地址,多个线程可以使用同一个函数地址
LPVOID lpParameter, //传给线程函数的参数
DWORD dwCreationFlags, //指定额外的标志来控制线程的创建,为0表示线程创建之后立即就可以进行调度,如果为CREATE_SUSPENDED则表示线程创建后暂停运行,这 样它就无法调度,直到调用ResumeThread()
LPDWORD lpThreadId //返回线程的ID号,传入NULL表示不需要返回该线程ID号
);
函数返回值:成功返回新线程的句柄,失败返回NULL
2、WaitForSingleObject
函数功能:等待函数——使线程进入等待状态,直到指定的内核对象被触发。
函数原形:
DWORD WINAPI WaitForSingleObject(
HANDLE hHandle, //要等待的内核对象
DWORD dwMilliseconds //最长等待的时间,以毫秒为单位,如传入5000就表示5秒,传入0就立即返回,传入INFINITE表示无限等待
);
ps:因为线程的句柄在线程运行时是未触发的,线程结束运行,句柄处于触发状态。所以可以用WaitForSingleObject()来等待一个线程结束运行
函数返回值:在指定的时间内对象被触发,函数返回WAIT_OBJECT_0;超过最长等待时间对象仍未被触发返回WAIT_TIMEOUT;传入参数有错误将返回WAIT_FAILED
CreateThread()函数是Windows提供的API接口,在C/C++语言另有一个创建线程的函数_beginthreadex(),在很多书上(包括《Windows核心编程》)提到过尽量使用_beginthreadex()来代替使用CreateThread(),下面讨论WHY!
首先要从标准C运行库与多线程的矛盾说起,标准C运行库在1970年被实现了,由于当时没任何一个操作系统提供对多线程的支持。因此编写标准C运行库的程序员根本没考虑多线程程序使用标准C运行库的情况。比如标准C运行库的全局变量errno。很多运行库中的函数在出错时会将错误代号赋值给这个全局变量,这样可以方便调试。但如果有这样的一个代码片段:
1 if (system("notepad.exe readme.txt") == -1) 2 { 3 switch(errno) 4 { 5 ...//错误处理代码 6 } 7 }
假设某个线程A在执行上面的代码,该线程在调用system()之后且尚未调用switch()语句时另外一个线程B启动了,这个线程B也调用了标准C运行库的函数,不幸的是这个函数执行出错了并将错误代号写入全局变量errno中。这样线程A一旦开始执行switch()语句时,它将访问一个被B线程改动了的errno。这种情况必须要加以避免!因为不单单是这一个变量会出问题,其它像strerror()、strtok()、tmpnam()、gmtime()、asctime()等函数也会遇到这种由多个线程访问修改导致的数据覆盖问题。
为了解决这个问题,Windows操作系统提供了这样的一种解决方案——每个线程都将拥有自己专用的一块内存区域来供标准C运行库中所有有需要的函数使用。而且这块内存区域的创建就是由C/C++运行库函数_beginthreadex()来负责的。
下面是_beginthreadex()源码:
2 _MCRTIMP uintptr_t __cdecl _beginthreadex( 3 void *security, 4 unsigned stacksize, 5 unsigned (__CLR_OR_STD_CALL * initialcode) (void *), 6 void * argument, 7 unsigned createflag, 8 unsigned *thrdaddr 9 ) 10 { 11 _ptiddata ptd; //pointer to per-thread data ,"_ptiddata"是个结构体指针 12 uintptr_t thdl; //thread handle 线程句柄 , "uintptr_t"是一个可以持有一个指针值的整型变量13 unsigned long err = 0L; //Return from GetLastError() 14 unsigned dummyid; //dummy returned thread ID 线程ID号 15 16 // validation section 检查initialcode是否为NULL 17 _VALIDATE_RETURN(initialcode != NULL, EINVAL, 0); 18 19 //Initialize FlsGetValue function pointer 20 __set_flsgetvalue(); 21 22 //Allocate and initialize a per-thread data structure for the to-be-created thread. 23 //相当于new一个_tiddata结构,并赋给_ptiddata指针。 24 if ( (ptd = (_ptiddata)_calloc_crt(1, sizeof(struct _tiddata))) == NULL ) 25 goto error_return; 26 27 // Initialize the per-thread data 28 //初始化线程的_tiddata块即CRT数据区域,"CRT"为标准C运行库 29 _initptd(ptd, _getptd()->ptlocinfo); 30 31 //设置_tiddata结构中的其它数据,这样这块_tiddata块就与线程联系在一起了。 32 ptd->_initaddr = (void *) initialcode; //线程函数地址 33 ptd->_initarg = argument; //传入的线程参数 34 ptd->_thandle = (uintptr_t)(-1); 35 36 #if defined (_M_CEE) || defined (MRTDLL) 37 if(!_getdomain(&(ptd->__initDomain))) //函数_getdomain()主要功能是初始化COM环境38 { 39 goto error_return; 40 } 41 #endif // defined (_M_CEE) || defined (MRTDLL) 42 43 // Make sure non-NULL thrdaddr is passed to CreateThread 44 if ( thrdaddr == NULL )//判断是否需要返回线程ID号 45 thrdaddr = &dummyid; 46 47 // Create the new thread using the parameters supplied by the caller. 48 //_beginthreadex()最终还是会调用CreateThread()来向系统申请创建线程 49 if ( (thdl = (uintptr_t)CreateThread( 50 (LPSECURITY_ATTRIBUTES)security, 51 stacksize, 52 _threadstartex, 53 (LPVOID)ptd, 54 createflag, 55 (LPDWORD)thrdaddr)) 56 == (uintptr_t)0 ) 57 { 58 err = GetLastError(); 59 goto error_return; 60 } 61 62 //Good return 63 return(thdl); //线程创建成功,返回新线程的句柄. 64 65 //Error return 66 error_return: 67 //Either ptd is NULL, or it points to the no-longer-necessary block 68 //calloc-ed for the _tiddata struct which should now be freed up. 69 //回收由_calloc_crt()申请的_tiddata块 70 _free_crt(ptd); 71 // Map the error, if necessary. 72 // Note: this routine returns 0 for failure, just like the Win32 73 // API CreateThread, but _beginthread() returns -1 for failure. 74 //校正错误代号(可以调用GetLastError()得到错误代号) 75 if ( err != 0L ) 76 _dosmaperr(err); 77 return( (uintptr_t)0 ); //返回值为NULL的效句柄 78 }
由上面的源代码可知,_beginthreadex()函数在创建新线程时会分配并初始化一个_tiddata块。这个_tiddata块自然是用来存放一些需要线程独享的数据。事实上新线程运行时会首先将_tiddata块与自己进一步关联起来,然后新线程调用标准C运行库函数如strtok()时就会先取得_tiddata块的地址再将需要保护的数据存入_tiddata块中。这样每个线程就只会访问和修改自己的数据而不会去篡改其它线程的数据了。因此,如果在代码中有使用标准C运行库中的函数时,尽量使用_beginthreadex()来代替CreateThread()。
接下来,类似于上面的程序用CreateThread()创建输出“Hello World”的子线程,下面使用_beginthreadex()来创建多个子线程:
1 //创建多子个线程实例 2 #include3 #include 4 #include 5 //子线程函数 6 unsigned int __stdcall ThreadFun(PVOID pM) 7 { 8 printf("线程ID号为%4d的子线程说:Hello World\n", GetCurrentThreadId()); 9 return 0; 10 } 11 //主函数,所谓主函数其实就是主线程执行的函数。 12 int main() 13 { 14 printf(" 创建多个子线程实例 \n"); 15 printf(" -- by MoreWindows( http://blog.csdn.net/MoreWindows ) --\n\n"); 16 17 const int THREAD_NUM = 5; 18 HANDLE handle[THREAD_NUM]; 19 for (int i = 0; i < THREAD_NUM; i++) 20 handle[i] = (HANDLE)_beginthreadex(NULL, 0, ThreadFun, NULL, 0, NULL); 21 WaitForMultipleObjects(THREAD_NUM, handle, TRUE, INFINITE); 22 return 0; 23 }
能不能来一个线程报数功能,即第一个子线程输出1,第二个子线程输出2,第三个子线程输出3,……。要实现这个功能似乎非常简单——每个子线程对一个全局变量进行递增并输出就可以了。代码如下:
1 //子线程报数 2 #include3 #include 4 #include 5 int g_nCount; 6 //子线程函数 7 unsigned int __stdcall ThreadFun(PVOID pM) 8 { 9 g_nCount++; 10 printf("线程ID号为%4d的子线程报数%d\n", GetCurrentThreadId(), g_nCount); 11 return 0; 12 } 13 //主函数,所谓主函数其实就是主线程执行的函数。 14 int main() 15 { 16 printf(" 子线程报数 \n"); 17 printf(" -- by MoreWindows( http://blog.csdn.net/MoreWindows ) --\n\n"); 18 19 const int THREAD_NUM = 10; 20 HANDLE handle[THREAD_NUM]; 21 22 g_nCount = 0; 23 for (int i = 0; i < THREAD_NUM; i++) 24 handle[i] = (HANDLE)_beginthreadex(NULL, 0, ThreadFun, NULL, 0, NULL); 25 WaitForMultipleObjects(THREAD_NUM, handle, TRUE, INFINITE); 26 return 0; 27 }
显示结果从1数到10,看起来好象没有问题。
答案是不对的,虽然这种做法在逻辑上是正确的,但在多线程环境下这样做是会产生严重的问题。