InterlockedIncrement 用处 和用法,对于只执行一次的类

转自:InterlockedIncrement和InterlockedDecrement的妙用

一般来说,在多用户线程环境中,我们使用临界区、事件对象甚至互斥量来进行同步,尤其是临界区,可以很方便地对某些变量甚至代码块进行锁定执行,防止多线程之间资源恶性抢夺。

既然如此,为啥微软又单独提供了专用于一个数值锁定计算的API函数InterlockedIncrement和InterlockedDecrement呢?他们又有什么特殊作用呢?

恰好近段时间写了一个这方面的应用,帮我加深了对这类API函数的理解。
首先描述一下需求,在应用中,有这样一个类,它可能只被实例化一次,也可能会被实例化多次,但不管被实例化了几次,它必须在构造函数里执行一项初始化计算,

假设初始化计算的函数为WSAStartup,同时还需要在析构函数里执行一下注销计算,假设注销计算的函数为WSACleanup,现在有一个要求,就是它们的初始化和注销计算只能被执行一次,

就如同在一个项目中,只能运行一次WSAStartup和WSACleanup一样。当然,大家可能会想到直接在工程的开始和结尾处实现这样的功能,但是,如果把这个类的文件包括在其它测试工程里进行测试,

同时不改变其它工程的代码,又该如何实现呢?
其实,我们可以充分利用InterlockedIncrement和InterlockedDecrement,就如同COM的CoInitialize()和CoUninitialize()一样,描述代码如下:

复制代码
class A {
protected:
static long m_nRef;
public:
//类A的构造函数 A() {
if(1 == InterlockedIncrement(&m_nRef)) {
//以下代码只执行一次 WSADATA wsaData;
WSAStartup(MAKEWORD(2,2), &wsaData); }
};
//类A的虚析构函数 virtual ~A() {
if(0 == InterlockedDecrement(&m_nRef)) {
//以下代码只执行一次 WSACleanup(); }
}
};
long A::m_nRef = 0;
复制代码
这样,无论我们创建了类A的多少个实例,在类的构造函数和析构函数里,WSAStartup和WSACleanup均只被执行一次,有效地保证了单元代码的封装性。

InterLockedIncrement 和 InterLockedDecrement处理线程访问

当我们在处理多线程或者模拟一个类似COM的引用计数时,我们为了防止多个线程同时访问一个变量,导致不可预料的结果时。我们便可以方便的使用InterLockedIncrement 和 InterLockedDecrement 来对这个变量进行锁定访问:

long volatile lReference;

InterlockedIncrement( &lReference ); // 对这个引用计数进行锁定并ADD 1

InterlockedDecrement( &lReference ); // 对这个引用计数进行锁定并SUB 1

这样就可以方便的操作这个变量,而不需要自己处理同步。当然还有其他方法。。这里不暂时一一说明。

秒杀多线程第三篇 原子操作 Interlocked系列函数

上一篇《多线程第一次亲密接触 CreateThread与_beginthreadex本质区别》中讲到一个多线程报数功能。为了描述方便和代码简洁起见,我们可以只输出最后的报数结果来观察程序是否运行出错。这也非常类似于统计一个网站每天有多少用户登录,每个用户登录用一个线程模拟,线程运行时会将一个表示计数的变量递增。程序在最后输出计数的值表示有今天多少个用户登录,如果这个值不等于我们启动的线程个数,那显然说明这个程序是有问题的。整个程序代码如下:

复制代码
#include <stdio.h>
#include <process.h>
#include <windows.h>
volatile long g_nLoginCount; //登录次数
unsigned int __stdcall Fun(void *pPM); //线程函数
const int THREAD_NUM = 10; //启动线程数
unsigned int __stdcall ThreadFun(void *pPM)
{
Sleep(100); //some work should to do
g_nLoginCount++;
Sleep(50);
return 0;
}
int main()
{
g_nLoginCount = 0;

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); 
printf("有%d个用户登录后记录结果是%d\n", THREAD_NUM, g_nLoginCount);
return 0;

}
复制代码

程序中模拟的是10个用户登录,程序将输出结果:

和上一篇的线程报数程序一样,程序输出的结果好象并没什么问题。下面我们增加点用户来试试,现在模拟50个用户登录,为了便于观察结果,在程序中将50个用户登录过程重复20次,代码如下:

复制代码
#include <stdio.h>
#include <windows.h>
volatile long g_nLoginCount; //登录次数
unsigned int __stdcall Fun(void *pPM); //线程函数
const DWORD THREAD_NUM = 50;//启动线程数
DWORD WINAPI ThreadFun(void *pPM)
{
Sleep(100); //some work should to do
g_nLoginCount++;
Sleep(50);
return 0;
}
int main()
{
printf(" 原子操作 Interlocked系列函数的使用\n");
printf(" – by MoreWindows( http://blog.csdn.net/MoreWindows ) --\n\n");

//重复20次以便观察多线程访问同一资源时导致的冲突
int num= 20;
while (num--)
{    
    g_nLoginCount = 0;
    int i;
    HANDLE  handle[THREAD_NUM];
    for (i = 0; i < THREAD_NUM; i++)
        handle[i] = CreateThread(NULL, 0, ThreadFun, NULL, 0, NULL);
    WaitForMultipleObjects(THREAD_NUM, handle, TRUE, INFINITE);
    printf("有%d个用户登录后记录结果是%d\n", THREAD_NUM, g_nLoginCount);
}
return 0;

}
复制代码

运行结果如下图:

现在结果水落石出,明明有50个线程执行了g_nLoginCount++;操作,但结果输出是不确定的,有可能为50,但也有可能小于50。

   要解决这个问题,我们就分析下g_nLoginCount++;操作。在VC6.0编译器对g_nLoginCount++;这一语句打个断点,再按F5进入调试状态,然后按下Debug工具栏的Disassembly按钮,这样就出现了汇编代码窗口。可以发现在C/C++语言中一条简单的自增语句其实是由三条汇编代码组成的,如下图所示。

讲解下这三条汇编意思:

第一条汇编将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__cdec InterlockedExchangeAdd(LONG volatile* Addend, LONGValue);

返回运算后的值,注意!加个负数就是减。

2.赋值操作

LONG__cdeclInterlockedExchange(LONG volatile* Target, LONGValue);

Value就是新值,函数会返回原先的值。

在本例中只要使用InterlockedIncrement()函数就可以了。将线程函数代码改成:

复制代码
DWORD WINAPI ThreadFun(void *pPM)
{
Sleep(100);//some work should to do
//g_nLoginCount++;
InterlockedIncrement((LPLONG)&g_nLoginCount);
Sleep(50);
return 0;
}
复制代码

再次运行,可以发现结果会是唯一的。

   因此,在多线程环境下,我们对变量的自增自减这些简单的语句也要慎重思考,防止多个线程导致的数据访问出错。更多介绍,请访问MSDN上Synchronization Functions这一章节,地址为 http://msdn.microsoft.com/zh-cn/library/aa909196.aspx

看到这里,相信本系列首篇《秒杀多线程第一篇 多线程笔试面试题汇总》中选择题第一题(百度笔试题)应该可以秒杀掉了吧(知其然也知其所以然),正确答案是D。另外给个附加问题,程序中是用50个线程模拟用户登录,有兴趣的同学可以试下用100个线程来模拟一下(上机试试绝对会有意外发现_)。

下一篇《秒杀多线程第四篇 一个经典多线程同步问题》将提出一个稍为复杂点但却非常经典的多线程同步互斥问题,这个问题会采用不同的方法来解答,从而让你充分熟练多线程同步互斥的“招式”。更多精彩,欢迎继续参阅。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Interlocked是一组Windows API,提供了一些原子操作,可以保证在执行时不会被其他线程中断,从而确保了线程安全。它通常用于保护共享资源,防止并发访问时出现竞争条件。 以下是Interlocked的一些常用函数: 1. InterlockedIncrement:原子地将指定变量的值加一。 ```C++ LONG InterlockedIncrement(LONG volatile *lpAddend); ``` 2. InterlockedDecrement:原子地将指定变量的值减一。 ```C++ LONG InterlockedDecrement(LONG volatile *lpAddend); ``` 3. InterlockedExchange:原子地将指定变量的值设置为一个新值,并返回旧值。 ```C++ LONG InterlockedExchange(LONG volatile *Target, LONG Value); ``` 4. InterlockedCompareExchange:原子地比较指定变量的值和期望值,并在它们相等时将变量的值设置为一个新值,返回旧值。 ```C++ LONG InterlockedCompareExchange(LONG volatile *Destination, LONG Exchange, LONG Comparand); ``` 使用Interlocked的一般步骤如下: 1. 定义一个变量,用于存储共享资源的值。 ```C++ LONG g_sharedVariable = 0; ``` 2. 在访问共享资源的地方使用Interlocked函数。 ```C++ InterlockedIncrement(&g_sharedVariable); ``` 以上代码实现了对共享变量g_sharedVariable的原子增量操作。 需要注意的是,Interlocked函数使用时需要将变量的指针传入函数中,而且变量必须是volatile型的,以确保编译器不会对变量进行优化,从而造成不必要的错误。 另外,Interlocked函数的执行是原子性的,不会被其他线程中断,因此可以保证线程安全。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值