当进行多线程编程的时候,一个随时都需要面对的问题就是对一些共享变量进行操作,例如增加/降低某个数值,更改某一个数据,对某一个数据进行各种逻辑操作,如XOR AND, OR等。在这时候,比较让人头疼的地方就是,C++的操作符不保证线程安全。例如:++X,不能保证X在自增的过程中不被打断。于是就有可能:
X+1,但是X的新值还没有被放到X中去,线程就因为时间片等问题被挂起,在这样的环境下,如果某一个线程读取了X的数值,那么该线程就得到了错误的数据。
如果某一个计算在完成之前不能被打断,那么该计算就被称为原子计算。windows提供了interlocked系列函数来保证计算的原子性,
例如InterlockedExchangeAdd函数,或者InterlockedExchange函数。下面这段代码给出了一个使用这些函数的例子。
// Interlocked Family.cpp : Defines the entry point for the console application.
//
#include "stdafx.h"
#include <Windows.h>
#include <process.h>
#include <iostream>
using namespace std;
#define THREAD_NUM MAXIMUM_WAIT_OBJECTS
LONG lCounter = 0;
LONG lCounterLocked = 0;
LONG lCounterSpinLock = FALSE;
unsigned int WINAPI Thread(LPVOID pParam)
{
while(InterlockedExchange(&lCounterSpinLock,TRUE) == TRUE) SwitchToThread(); //lock
printf_s("Thread %d gains access\n",pParam);
lCounter++;
Sleep(500);
InterlockedExchange(&lCounterSpinLock,FALSE); //unlock
::InterlockedExchangeAdd(&lCounterLocked,1);
return 0;
}
int _tmain(int argc, _TCHAR* argv[])
{
HANDLE pThreads[THREAD_NUM];
for(int i = 0; i<THREAD_NUM; i++)
pThreads[i] = (HANDLE)_beginthreadex(NULL,NULL,Thread,(LPVOID)i,NULL,NULL);
WaitForMultipleObjects(THREAD_NUM,pThreads,TRUE,INFINITE);
for(int i = 0; i<THREAD_NUM; i++)
CloseHandle(pThreads[i]);
cout<<lCounter<<ends<<lCounterLocked<<endl;
return 0;
}
在线程函数中我们可以看到,有一个非线程安全的变量lCounter在自增。那么为了保证这个变量的线程安全,我使用了
InterlockedExchange
函数来实现了一个自旋锁。线程在锁内增加变量的值,并休眠0.5秒。同时,还有另一个变量通过原子操作来自增。在执行过程中可以看到,每0.5秒会有一个线程获得lCounter的访问权,并自增该变量。
读过我之前写的那个线程优先级那点事的人也应该知道,SwitchToThread代表在获得Access之前,该线程自动放弃时间片改去调度其它线程,再次强调一遍这个函数和Sleep(0)之间的区别,Sleep(0)只会让OS调度同级或者高优先级的线程,而SwitchToThread的调度是全系统范围的。
最后,这段代码的结果显示了用自旋锁自增的变量和用Interlocked实现的变量最后的数值是一致的。
该代码另外的一些小点提示:
1.注意我使用了_beginthreadex函数,该函数在前面的博文里提到过,是CreateThread的替代品,简单的说,它为CRT的多线程安全提供了保证,这一点CreateThread没有考虑。
2.注意,我在最后关闭了所有线程的句柄,如果你使用WinVista或者Win7,你可以观察到这个进程的句柄从125降低到了61(因为线程开了64个),记得关闭句柄是一个很好的习惯。
3.你可以从任务管理器看到自旋锁是如何FUCK CPU的,CPU的使用率一直都在100%,直到这64个线程"轮奸"完成。自旋锁只适合哪种你确定你短期之内就能访问到共享资源的情况,否则请使用内核对象(如信号量,临界区)。
最后一句话是: