C++ 之 原子操作 Interlocked系列函数多线程(四)

首先在此感谢 MoreWindows 秒杀多线程面试题系列让我成长和学习,同时也借鉴了很多优秀观点和示例!

 

在此再一次真心的感谢!也不得不感谢CSDN!

 首先也使用一个比较直观的示例来进入我们今天的主题

 

#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系列函数)。

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的参数含义和临界区的联系)!

 

syw_selfimpr新浪微博地址: http://weibo.com/u/2945271402

 

 如果觉得本文对您有帮助,请点击‘顶’支持一下,您的支持是我写作最大的动力,谢谢。

 

注:以上在VS2010 下运行编译!

 

 

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值