在达内Windows/Win32编程专栏中,我们已经介绍过线程同步与线程互斥技术,包括了原子锁,互斥体,事件和信号量。但是与海哥讲的线程同步与线程互斥技术不太一样,这篇文章来带领大家学习线程同步与线程互斥技术,包含了【Windows线程开发】Windows线程同步技术文章中的技术和海哥讲的技术,来系统了解一下线程同步与线程互斥技术。
本篇文章包含了线程同步技术(事件,信号量)和线程互斥技术(原子锁,临界区,互斥体)。之前写过相关文章,但是在最近逆向的时候发现还是不熟悉,而且之前的文章中讲到的技术与海哥讲的技术不是很贴合,今天写一篇文章来系统学习一下线程同步与线程互斥技术。
原子锁我们在这里介绍到了,但是严格来说,它不属于线程互斥对象。
我们先来看看存在线程安全问题的一个控制台程序:
#include
#include
DWORD WINAPI ThreadProc(LPVOID);
int g_value = 0;
int main()
{
HANDLE Thread[2] = { 0 };
Thread[0]=CreateThread(NULL, 0, ThreadProc, 0, NULL, NULL);
Thread[1]=CreateThread(NULL, 0, ThreadProc, 0, NULL, NULL);
WaitForMultipleObjects(2, Thread, TRUE, INFINITE);
printf("%d", g_value);
return 0;
}
DWORD WINAPI ThreadProc(LPVOID lpParameter) {
for (int i = 0; i < 10000000; i++) {
g_value++;
}
return 0;
}
程序运行:
我们创建了两个线程分别对全局变量g_value进行+1的操作,分别进行10000000次,但是最后出现的结果并不是20000000,具体原因看【Windows线程开发】Windows线程同步技术。
这时候就需要我们使用原子锁,对全局变量g_value进行加锁了。
我们来看看原子锁:
原子锁主要解决的问题就是:当多个线程使用同一个变量时,对变量进行“加锁”技术,防止多个线程使用同一个变量。原子锁主要解决的就是对变量进行写的操作时,方式多个线程同时对同一个变量操作。
对于每一个不同类型的变量,都有自己的“加锁”函数,对于不同的运算操作,也有不同的“加锁”函数,具体可查看文档。
这里展示对变量++操作时使用原子锁:
这里为了方便大家查看,只给出了线程处理函数:
DWORD WINAPI ThreadProc(LPVOID lpParameter) {
for (int i = 0; i < 10000000; i++) {
InterlockedIncrement(&g_value);
}
return 0;
}
我们来看看运行效果:
临界区也被称作关键节,关键节对象提供与互斥对象提供的同步类似,但关键节只能由单个进程的线程使用。 关键节对象不能跨进程共享。
在使用临界区的时候,我们需要一下几个步骤:
这里给出官方文档地址:Critical Section 对象
CRITICAL_SECTION cs;
这里给出官方文档地址:initializeCriticalSection 函数 (synchapi.h)
语法:
void InitializeCriticalSection(
[out] LPCRITICAL_SECTION lpCriticalSection
);
参数说明:
LPCRITICAL_SECTION:指向关键节对象的指针。
这里给出官方文档地址:enterCriticalSection 函数 (synchapi.h)
函数功能:等待指定关键部分对象的所有权。 此函数将在授予调用线程所有权时返回。
语法:
void EnterCriticalSection(
[in, out] LPCRITICAL_SECTION lpCriticalSection
);
参数说明:
LPCRITICAL_SECTION指向关键节对象的指针。
这里给出官方文档地址:LeaveCriticalSection 函数 (synchapi.h)
函数功能:释放指定关键节对象的所有权。
语法:
void LeaveCriticalSection(
[in, out] LPCRITICAL_SECTION lpCriticalSection
);
参数说明:
LPCRITICAL_SECTION:指向关键节对象的指针
这里给出官方文档地址:deleteCriticalSection 函数 (synchapi.h)
函数功能:释放未拥有的关键节对象使用的所有资源。
语法:
void DeleteCriticalSection(
[in, out] LPCRITICAL_SECTION lpCriticalSection
);
参数说明:
LPCRITICAL_SECTION:指向关键节对象的指针。 该对象以前必须使用 InitializeCriticalSection 函数进行初始化。
我们来使用临界区解决最开始提出的线程安全问题:
#include
#include
DWORD WINAPI ThreadProc(LPVOID);
CRITICAL_SECTION cs;
DWORD g_value = 0;
int main()
{
InitializeCriticalSection(&cs);
HANDLE Thread[2] = { 0 };
Thread[0]=CreateThread(NULL, 0, ThreadProc, 0, NULL, NULL);
Thread[1]=CreateThread(NULL, 0, ThreadProc, 0, NULL, NULL);
WaitForMultipleObjects(2, Thread, TRUE, INFINITE);
printf("%d", g_value);
DeleteCriticalSection(&cs);
return 0;
}
DWORD WINAPI ThreadProc(LPVOID lpParameter) {
for (int i = 0; i < 10000000; i++) {
EnterCriticalSection(&cs);
g_value++;
LeaveCriticalSection(&cs);
}
return 0;
}
我们来看看运行效果:
可以看到我们成功使用临界区实现了线程互斥。
大家可以到官方文档中查看互斥对象:互斥体对象
互斥对象状态设置为当任何线程不拥有时发出信号,一次只能有一个线程可以拥有互斥体对象。与临界区不同的是,互斥体可以跨进程使用。
互斥体的使用相对来说较为简单,我们只需要创建互斥体对象即可使用。
使用CreateMutex()
函数,这里给出官方文档地址:createMutexA 函数 (synchapi.h)
语法:
HANDLE CreateMutexA(
[in, optional] LPSECURITY_ATTRIBUTES lpMutexAttributes,
[in] BOOL bInitialOwner,
[in, optional] LPCSTR lpName
);
参数说明:
LPSECURITY_ATTRIBUTES:安全属性,我们一般不关注。
bInitialOwner:如果此值设置为TRUE,并且调用方创建了互斥体,则调用线程获取互斥体对象的使用权。也就是说,我们在某个线程中创建了互斥体,并且此字段设置为TRUE,那么这个线程立即拥有该互斥体。
lpName:为互斥体对象命名。
返回值:互斥体句柄。
临界区对象创建并且初始化之后,可以使用进入临界区或者离开临界区的方式来实现线程互斥,那么我们如何使用互斥体实现线程间的互斥呢?
WaitForSingleObject
函数。DWORD WaitForSingleObjectEx(
[in] HANDLE hHandle,
[in] DWORD dwMilliseconds,
);
参数说明:
hHandle:要等待的对象的句柄。
dwMilliseconds:超时间隔(以毫秒为单位)。
ReleaseMutex()
函数来释放已获得的互斥体。函数功能:释放指定互斥对象的所有权。
语法:
BOOL ReleaseMutex(
[in] HANDLE hMutex
);
参数说明:hMutex:要释放的互斥体对象句柄。
CloseHandle
函数来关闭互斥体。函数功能:关闭打开的对象句柄。
语法:
BOOL CloseHandle(
[in] HANDLE hObject
);
函数说明:
hOnject:对象的有效句柄。
返回值:如果函数成功,则返回非零值。
我们来看一个新的线程安全问题:
#include
#include
DWORD WINAPI ThreadProc1(LPVOID);
DWORD WINAPI ThreadProc2(LPVOID);
DWORD g_value = 0;
HANDLE hMutex;
int main()
{
hMutex = CreateMutex(NULL, FALSE, NULL);
HANDLE Thread[2] = { 0 };
Thread[0]=CreateThread(NULL, 0, ThreadProc1, 0, NULL, NULL);
Thread[1]=CreateThread(NULL, 0, ThreadProc2, 0, NULL, NULL);
WaitForMultipleObjects(2, Thread, TRUE, INFINITE);
printf("%d", g_value);
return 0;
}
DWORD WINAPI ThreadProc1(LPVOID lpParameter) {
while (1) {
for (int i = 0; i < 10; i++) {
std::cout << "++ ";
Sleep(100);
}
std::cout << std::endl;
}
return 0;
}
DWORD WINAPI ThreadProc2(LPVOID lpParameter) {
while (1) {
for (int i = 0; i < 10; i++) {
std::cout << "-- ";
Sleep(100);
}
std::cout << std::endl;
}
return 0;
}
我们的本意是输出十个“++”后,换行输出十个“-- ”,但是由于CPU时间片的问题,我们实际输出是这样的:
我们使用互斥体来解决:
#include
#include
DWORD WINAPI ThreadProc1(LPVOID);
DWORD WINAPI ThreadProc2(LPVOID);
DWORD g_value = 0;
HANDLE hMutex= CreateMutex(NULL, FALSE, NULL);
int main()
{
HANDLE Thread[2] = { 0 };
Thread[0] = CreateThread(NULL, 0, ThreadProc1, 0, NULL, NULL);
Thread[1] = CreateThread(NULL, 0, ThreadProc2, 0, NULL, NULL);
WaitForMultipleObjects(2, Thread, TRUE, INFINITE);
printf("%d", g_value);
return 0;
}
DWORD WINAPI ThreadProc1(LPVOID lpParameter) {
while (1) {
WaitForSingleObject(hMutex,INFINITE);
for (int i = 0; i < 10; i++) {
std::cout << "++ ";
Sleep(100);
}
std::cout << std::endl;
ReleaseMutex(hMutex);
}
return 0;
}
DWORD WINAPI ThreadProc2(LPVOID lpParameter) {
while (1) {
WaitForSingleObject(hMutex,INFINITE);
for (int i = 0; i < 10; i++) {
std::cout << "-- ";
Sleep(100);
}
std::cout << std::endl;
ReleaseMutex(hMutex);
}
return 0;
}
我们来看看运行效果:
可以发现我们使用互斥体解决了该线程安全问题。
前面介绍的都是线程互斥技术,在我们实际开发的过程中,有很多地方是有好几个线程相互依赖,这时候就需要线程同步技术了,这里首先我们来介绍事件:
这里给出官方文档地址:事件对象 (同步)
事件我个人理解为就是一个通知,当两个线程相互依赖的时候,其中一个线程完成了工作,就将事件设置为有信号(可以理解为发出了通知)另一个线程等待到事件消息后,开始工作。
事件的使用也较为简单,主要包括以下操作:
使用CreatEvent()
函数,这里给出官方文档地址:createEventA 函数 (synchapi.h)
函数功能:创建或打开命名或未命名的事件对象
语法:
HANDLE CreateEventA(
[in, optional] LPSECURITY_ATTRIBUTES lpEventAttributes,
[in] BOOL bManualReset,
[in] BOOL bInitialState,
[in, optional] LPCSTR lpName
);
参数说明:
lpEventAttributes:安全属性
bManualReset:如果设置为TRUE,则操作系统会自动重置事件,如果设置为FALSE,则需要程序员手动设置事件。
bInitialState:如果此参数为 TRUE,则会向事件对象发出初始状态信号;否则,它将不进行签名。
lpName:为事件命名
返回值:如果函数成功,则将返回事件对象句柄,如果函数失败,则返回NULL
SetEvent()
函数,这里给出官方文档地址:SetEvent 函数 (synchapi.h)。函数功能:将指定的事件对象设置为有信号状态。
语法:
BOOL SetEvent(
[in] HANDLE hEvent
);
参数说明:
hEnent:指定要设置的事件对象。
返回值:如果函数成功,则返回非零值,如果函数失败,则返回NULL。
当事件对象有信号后,在其他线程中可以打开事件对象:
使用OpenEvent()
函数,这里给出官方文档地址:OpenEventA 函数 (synchapi.h)。
函数功能:打开现有的命名事件对象
语法:
HANDLE OpenEventA(
[in] DWORD dwDesiredAccess,
[in] BOOL bInheritHandle,
[in] LPCSTR lpName
);
参数说明:
dwDesireAccess:访问事件对象。
bInheritHandle:如果此值为 TRUE,则此过程创建的进程将继承句柄。 否则,进程不会继承此句柄。
lpName:要打开的事件的名称。 名称比较区分大小写。
返回值:如果函数成功,则将返回事件对象的句柄,如果函数失败,则将返回NULL。
ResetEnent
函数,这里给出官方文档地址:ResetEvent 函数 (synchapi.h)。函数功能:将事件对象设置为非对齐状态(无消息状态)
语法:
BOOL ResetEvent(
[in] HANDLE hEvent
);
参数说明:
hEnent:要设置的事件句柄。
返回值:如果函数成功,则返回非零值,如果函数失败,则返回NULL。
CloseHandel
函数来关闭事件。我们来看看事件的使用:
这里创建了两个线程,当一个线程将全局变量g_value增加到100后,通知另一个线程工作。
#include
#include
DWORD WINAPI ThreadProc1(LPVOID);
DWORD WINAPI ThreadProc2(LPVOID);
HANDLE hEvent = 0;
DWORD g_value = 0;
int main()
{
hEvent = CreateEvent(NULL, FALSE, 0, NULL);
HANDLE Thread[2] = { 0 };
Thread[0] = CreateThread(NULL, 0, ThreadProc1, 0, NULL, NULL);
Thread[1] = CreateThread(NULL, 0, ThreadProc2, 0, NULL, NULL);
WaitForMultipleObjects(2, Thread, TRUE, INFINITE);
printf("%d", g_value);
CloseHandle(hEvent);
return 0;
}
DWORD WINAPI ThreadProc1(LPVOID lpParameter) {
for (int i = 0; i < 100; i++) {
g_value++;
}
SetEvent(hEvent);
printf("g_value已达到100,将进行输出\n");
return 0;
}
DWORD WINAPI ThreadProc2(LPVOID lpParameter) {
WaitForSingleObject(hEvent, INFINITE);
while (g_value<120) {
for (int i = 0; i < 10; i++) {
std::cout << "-- ";
Sleep(1);
}
g_value++;
std::cout << std::endl;
}
ResetEvent(hEvent);
return 0;
}
我们来看看这两个线程之间的协调工作:
我们之前的线程同步和线程互斥技术,都是创建了对应的对象后,被相关线程获取后即可使用,那么有没有一种技术,能够指定被获取多少次呢?这样我们就可以控制相关的线程执行多少次了。
答案是有的,我们称之为信号量,这里给出官方文档地址:信号灯对象
信号灯对象是一个同步对象,用于维护零和指定最大值之间的计数。 每次线程完成信号灯对象的等待时,计数都会递减,每次线程释放信号灯时递增。 当计数达到零时,不会再成功等待信号灯对象状态发出信号。 当信号量计数大于零时,会将信号量的状态设置为已发出信号;当信号量计数为零时,会将信号量的状态设置为未发出信号。
那么当我们使用信号量对象时,操作主要包括为:创建信号量,增加信号量的计数,等待信号量,离开信号量,关闭信号量。
使用CreateSemaphore
函数,这里给出官方文档地址:createSemaphoreA 函数 (winbase.h)
语法:
HANDLE CreateSemaphoreA(
[in, optional] LPSECURITY_ATTRIBUTES lpSemaphoreAttributes,
[in] LONG lInitialCount,
[in] LONG lMaximumCount,
[in, optional] LPCSTR lpName
);
参数说明:
lpSemaphoreAttributes:安全属性
lInitialCount:信号量的初始计数,此值必须小于lMaximumCount。
lMaximumCOunt:信号量的最大数量
lpName:为信号量命名
返回值:若函数成功,则返回信号量句柄,若函数失败,则返回NULL。
使用WaitForSingluObject()
函数,前文已经介绍过,这里不再赘述。
使用ReleaseSemaphore
函数,这里给出官方文档地址:ReleaseSemaphore 函数 (synchapi.h)
函数功能:按指定量增加指定信号量的计数
语法:
BOOL ReleaseSemaphore(
[in] HANDLE hSemaphore,
[in] LONG lReleaseCount,
[out, optional] LPLONG lpPreviousCount
);
参数说明:
hSemaphore:指定信号量的句柄
lReleaseCount:指定要增加的数量
lpPreviousCount:这是一个OUT类型的参数,用于接收上一个计数,也就是增加之前的信号量计数
返回值:若函数成功,则返回非零值,若函数失败,则返回NULL。
使用CloseHandle()
函数,前文已经介绍过,这里不再做赘述。
我们来看看信号量的使用:
#include
#include
DWORD WINAPI ThreadProc1(LPVOID);
DWORD WINAPI ThreadProc2(LPVOID);
HANDLE hSemaphore = 0;
DWORD g_value = 0;
int main()
{
hSemaphore = CreateSemaphore(NULL, 0, 3, NULL);
HANDLE Thread[2] = { 0 };
Thread[0] = CreateThread(NULL, 0, ThreadProc1, 0, NULL, NULL);
Thread[1] = CreateThread(NULL, 0, ThreadProc2, 0, NULL, NULL);
WaitForMultipleObjects(2, Thread, TRUE, INFINITE);
printf("%d", g_value);
CloseHandle(hSemaphore);
return 0;
}
DWORD WINAPI ThreadProc1(LPVOID lpParameter) {
for (int i = 0; i < 100; i++) {
g_value++;
}
printf("g_value已达到100,将进行输出\n");
ReleaseSemaphore(hSemaphore, 2, NULL);
return 0;
}
DWORD WINAPI ThreadProc2(LPVOID lpParameter) {
for (int k = 0; k < 3; k++) {
WaitForSingleObject(hSemaphore, INFINITE);
for (int i = 0; i < 10; i++) {
std::cout << "-- ";
Sleep(1);
}
std::cout << std::endl;
}
return 0;
}
我们来看看这段代码:我们本意是,当线程2执行的时候,输出三行,但是我们设置了信号量为2,所以只能输出两行。
我们看看执行效果:
本篇文章就分享到这里,如果大家发现其中有错误或者是个人理解不到位的地方,还请大家指出来,我会非常虚心地学习,希望我们共同进步!!!