#include <iostream> using namespace std; unsigned Counter; volatile long g_nLoginCount = 0; const int THREAD_NUM = 10;//线程启动数 unsigned __stdcall SecondThreadFunc( PVOID pArguments ) { Sleep((500)); g_nLoginCount++; Sleep(50); _endthreadex( 0 ); return 0; } int main() { unsigned threadID; HANDLE hThread[THREAD_NUM]; int num =10; while(num--) { g_nLoginCount = 0; for (int i=0; i < THREAD_NUM ; i++) { // Create the 10 thread. hThread[i]= (HANDLE)_beginthreadex(NULL, 0, &SecondThreadFunc, NULL, 0, NULL ); } //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; } 上面这个代码告诉系统,在特定的进程终止运行之前,或者在5 0 0 0 m s时间结束之前,调用线程不应该变为可调度状态。因此,如果进程终止运行,那么这个 函数调用将在不到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的句柄数组中的索引。该索引说明哪个对象变为已通知状态。 */ WaitForMultipleObjects(THREAD_NUM, hThread,true, INFINITE ); //cout<<"线程"<<g_nLoginCount<<"开始启动..."<<endl; cout<<"线程总数为:"<<THREAD_NUM<<"启动线程结果是"<<g_nLoginCount<<endl; } system("pause"); return 0; }
运行结果如下:
看上面的运行效果您一定认为和你正常对吧!那么我们继续增加THREAD_NUM线程启动数量看看效果又是怎么样呢?
代码仅仅修改了THREAD_NUM值!
#include <windows.h> #include <process.h> #include <iostream> using namespace std; unsigned Counter; volatile long g_nLoginCount = 0; const int THREAD_NUM = 50;//线程启动数 unsigned __stdcall SecondThreadFunc( PVOID pArguments ) { Sleep((500)); g_nLoginCount++; Sleep(50); _endthreadex( 0 ); return 0; } int main() { unsigned threadID; HANDLE hThread[THREAD_NUM]; int num =50; while(num--) { g_nLoginCount = 0; for (int i=0; i < THREAD_NUM ; i++) { // Create the 10 thread. hThread[i]= (HANDLE)_beginthreadex(NULL, 0, &SecondThreadFunc, NULL, 0, NULL ); } //WaitForMultipleObjects函数说明 WaitForMultipleObjects(THREAD_NUM, hThread,true, INFINITE ); //cout<<"线程"<<g_nLoginCount<<"开始启动..."<<endl; cout<<"线程总数为:"<<THREAD_NUM<<"启动线程结果是"<<g_nLoginCount<<endl; } system("pause"); return 0; }
运行新效果如下:
也不要惊讶!只是发现出问题了吧!那么是什么原因造成的呢?我准备在g_nLoginCount++;这来打个断点说明一下;
断点效果:
讲解下这三条汇编意思:(引用MoreWindows )
第一条汇编将g_nLoginCount的值从内存中读取到寄存器eax中。
第二条汇编将寄存器eax中的值与1相加,计算结果仍存入寄存器eax中。
第三条汇编将寄存器eax中的值写回内存中。
这样由于线程执行的并发性,很可能线程A执行到第二句时,线程B开始执行,线程B将原来的值又写入寄存器eax中,这样线程A所主要计算的值就被线程B修改了。这样执行下来,结果是不可预知的——可能会出现50,可能小于50。
因此在多线程环境中对一个变量进行读写时,我们需要有一种方法能够保证对一个值的递增操作是原子操作——即不可打断性,一个线程在执行原子操作时,其它线程必须等待它完成之后才能开始执行该原子操作。这种涉及到硬件的操作会不会很复杂了,幸运的是,Windows系统为我们提供了一些以Interlocked开头的函数来完成这一任务(下文将这些函数称为Interlocked系列函数)。
1.增减操作
LONG__cdeclInterlockedIncrement(LONG volatile* Addend);
LONG__cdeclInterlockedDecrement(LONG volatile* Addend);
返回变量执行增减操作之后的值。
LONG__cdecInterlockedExchangeAdd(LONG volatile*Addend,LONGValue);
返回运算后的值,注意!加个负数就是减。
2.赋值操作
LONG__cdeclInterlockedExchange(LONG volatile* Target,LONGValue);
Value就是新值,函数会返回原先的值。
那么在使用该函数以后情况呢?
修改如下:
unsigned __stdcall SecondThreadFunc( PVOID pArguments ) { Sleep((500)); /*g_nLoginCount++;*/ InterlockedIncrement((LPLONG)&g_nLoginCount); Sleep(50); _endthreadex( 0 ); return 0; }
运行效果:
ok一切正常!
在大多数计算机上,增加变量操作不是一个原子操作,需要执行下列步骤:
1. 将实例变量中的值加载到寄存器中。
2. 增加或减少该值。
3. 在实例变量中存储该值。
在多线程环境下,线程会在执行完前两个步骤后被抢先。然后由另一个线程执行所有三个步骤,当第一个线程重新开始执行时,它覆盖实例变量中的值,造成第二个线程执行增减操作的结果丢失。
期待将持续更新(将说明CRITICAL_SECTION的参数含义和临界区的联系)!
如果觉得本文对您有帮助,请点击‘顶’支持一下,您的支持是我写作最大的动力,谢谢。
注:以上在VS2010 下运行编译!