当我们进行多线程进行编程的时候呢,确实可以让我们的程序变得更加高效,但是,在面临多线程时,我们必须要做好线程之间的同步。比如说,有一个线程在读内存,另一个线程在写入内存,这将发生不可描述的事件,这就好比一个人在另一个人读书的时候修改书中的文字一样。因此,线程同步就变得相当重要了。好在呢,windows提供了很多的方式可以让线程同步变得容易。下面来介绍下在应用层下的几种线程同步方式。
第一种是自旋锁,Interlocked系列的原子访问操作,什么是原子操作呢?就是不可再分的,不能打断的。也就是说在同一时刻只能有一个在访问同一资源,不能同时进行访问。下面来看一个叫做InterlockedExchange这个API,它是用来将两个数进行交换的一个原子访问操作,其声明如下:
LONG InterlockedExchange(
LPLONG Target, // 要交换的变量值
LONG Value // 交换后的新值
);
该函数会返回原来的值,因此,在实现自旋锁的时候呢,该函数就变得非常有用,以下是第一种方式测试代码:
#include <windows.h>
#include <list>
//全局变量,关键数据
std::list<int> test = { 1, 2, 3, 4, 5 };
unsigned int locker = FALSE;
DWORD WINAPI Thread1(LPVOID lpParameter)
{
while (1)
{
//将locker的值与TRUE进行交换,返回原来的值
while (InterlockedExchange(&locker, TRUE) == TRUE) // #1
Sleep(0);
if (test.size())
{
printf("%d\n", lpParameter);
test.pop_front(); // #3
}
//再一次进行交换
InterlockedExchange(&locker, FALSE);// #2
}
return 0;
}
int _tmain(int argc, _TCHAR* argv[])
{
for (int i = 0; i < 2; i++)
{
CreateThread(0, 0, Thread1, (LPVOID)(i + 1), 0, 0);
}
system("pause");
return 0;
}
当我们将#1和#2注释掉时会发现,程序会崩溃,这是由于当进行#3代码(还没运行完)时,刚好到了另一个线程运行,另一个线程也执行到该代码且运行完,然后CPU时间片到了,原来的线程继续运行,但是该值已经被删除了,所以程序出错了。因此,当我们使用自旋锁时,就能保证该删除操作不会被打断,可以很好的进行线程同步了。
接下来,介绍第二种线程同步的方式----关键段(临界区)。
关键段与自旋锁很像,但还是有些不同,自旋锁它是一直自己把CPU的时间片握在手里(即使没有工作要做),而关键段则是将CPU的时间片贡献出去,让其他的线程进行运行,比如说,你有1分钟上厕所的时间,但是去卫生间的时候发现里边有人,这时你就只能在这里等,但是自旋锁呢,则是直接自己占用卫生间,虽然不能干什么事,直到时间到了,其他人才能继续使用。因此,自旋锁对自身程序来说,是比较有利的,但对操作系统来讲会稍微降低一点性能,而关键段则于此相反。接下来看下几个API:
要使用关键段呢,首先得声明一个CRITICAL_SECTION这个结构体,它就相当于上述例子中的卫生间。我们并不需要该结构体的成员,这里就不再赘述。
当我们声明好CRITICAL_SECTION之后,我们需要给它进行一下初始化,调用InitializeCriticalSection,其声明如下:
VOID InitializeCriticalSection(
LPCRITICAL_SECTION lpCriticalSection // CRITICAL_SECTION地址
);
初始化完之后,我们就可以将关键数据放在EnterCriticalSection和LeaveCriticalSection之间,将关键数据进行保护起来,它们的声明如下:
VOID EnterCriticalSection(
LPCRITICAL_SECTION lpCriticalSection // <span style="font-family: Arial, Helvetica, sans-serif;">CRITICAL_SECTION地址</span>
);
VOID LeaveCriticalSection(
LPCRITICAL_SECTION lpCriticalSection // <span style="font-family: Arial, Helvetica, sans-serif;">CRITICAL_SECTION地址</span>
);
当我们使用完关键段后,可以把它释放掉,DeleteCriticalSection用于释放关键段,其声明如下:
VOID DeleteCriticalSection(
LPCRITICAL_SECTION lpCriticalSection // CRITICAL_SECTION地址
);
以下是关键段的测试代码:
#include <windows.h>
#include <list>
//全局变量,关键数据
std::list<int> test = { 1, 2, 3, 4, 5 };
CRITICAL_SECTION cs;
DWORD WINAPI Thread1(LPVOID lpParameter)
{
while (1)
{
//进入关键段
EnterCriticalSection(&cs);
if (test.size())
{
printf("%d\n", lpParameter);
test.pop_front();
}
//离开关键段
LeaveCriticalSection(&cs);
}
return 0;
}
int _tmain(int argc, _TCHAR* argv[])
{
//初始化关键段
InitializeCriticalSection(&cs);
for (int i = 0; i < 2; i++)
{
CreateThread(0, 0, Thread1, (LPVOID)(i + 1), 0, 0);
}
//DeleteCriticalSection(&cs); //用完之后,释放关键段,这里注释掉是由于主线程将其释放了之后,在运行EnterCriticalSection会出错的
system("pause");
return 0;
}
接下来介绍第三种线程同步方式-------读写锁,
读写锁和关键段非常类似,但是与关键段不同的是,读写锁可以分为俩种状态,读和写,当多个线程对关键数据进行读取的时候是允许的,若多个线程对关键数据进行写入时是不行的。除此之外呢,读写锁在性能上是稍强关键段的,我们可以使用读写锁来替代关键段。
在使用读写锁时,应先声明一个SRWLOCK结构,然后调用InitializeSRWLock来进行初始化,声明如下:
VOID WINAPI InitializeSRWLock(
PSRWLOCK SRWLock<span style="white-space:pre"> </span>//SRWLOCK结构地址
);
之后,我们可以将关键数据放在AcquireSRWLockExclusive和ReleaseSRWLockExclusive之间,由于读写锁有两种方式,因此与之对应的就有两套API,前面介绍的是写的时候的,读取的应该将关键数据放在AcquireSRWLockShared和ReleaseSRWLockShared之间,它们的声明如下:
VOID WINAPI AcquireSRWLockExclusive(
PSRWLOCK SRWLock //SRWLOCK 结构地址
);
VOID WINAPI ReleaseSRWLockExclusive(
PSRWLOCK SRWLock //SRWLOCK 结构地址
);
VOID WINAPI AcquireSRWLockShared(
PSRWLOCK SRWLock //SRWLOCK 结构地址
);
VOID WINAPI ReleaseSRWLockShared(
PSRWLOCK SRWLock //SRWLOCK 结构地址
);
读写锁并没有释放函数,它是由操作系统进行释放的,我们不需要进行管理。一下是读写锁的一些测试代码:
#include <windows.h>
#include <list>
//全局变量,关键数据
std::list<int> test = { 1, 2, 3, 4, 5 };
SRWLOCK srw;
DWORD WINAPI Thread1(LPVOID lpParameter)
{
while (1)
{
AcquireSRWLockExclusive(&srw);
if (test.size())
{
printf("%d\n", lpParameter);
test.pop_front();
}
ReleaseSRWLockExclusive(&srw);
}
return 0;
}
int _tmain(int argc, _TCHAR* argv[])
{
//初始化读写锁
InitializeSRWLock(&srw);
for (int i = 0; i < 2; i++)
{
CreateThread(0, 0, Thread1, (LPVOID)(i + 1), 0, 0);
}
system("pause");
return 0;
}