在达内Windows/Win32编程专栏中,我们已经介绍过线程同步与线程互斥技术,包括了原子锁,互斥体,事件和信号量。但是与海哥讲的线程同步与线程互斥技术不太一样,这篇文章来带领大家学习线程同步与线程互斥技术,包含了【Windows线程开发】Windows线程同步技术文章中的技术和海哥讲的技术,来系统了解一下线程同步与线程互斥技术。
本篇文章包含了线程同步技术(事件,信号量)和线程互斥技术(原子锁,临界区,互斥体)。之前写过相关文章,但是在最近逆向的时候发现还是不熟悉,而且之前的文章中讲到的技术与海哥讲的技术不是很贴合,今天写一篇文章来系统学习一下线程同步与线程互斥技术。
线程互斥
一. 原子锁
原子锁我们在这里介绍到了,但是严格来说,它不属于线程互斥对象。
我们先来看看存在线程安全问题的一个控制台程序:
#include <iostream> #include <windows.h> 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; }
我们来看看运行效果:
二. 临界区
临界区也被称作关键节,关键节对象提供与互斥对象提供的同步类似,但关键节只能由单个进程的线程使用。 关键节对象不能跨进程共享。
在使用临界区的时候,我们需要一下几个步骤:
- 创建全局临界区对象
- 初始化临界区对象
- 之后,我们在使用临界区的时候,可以使用函数进入临界区,离开临界区。
1. 创建关键节对象
这里给出官方文档地址:Critical Section 对象
CRITICAL_SECTION cs;
2.初始化关键节对象
这里给出官方文档地址:initializeCriticalSection 函数 (synchapi.h)
语法:
void InitializeCriticalSection( [out] LPCRITICAL_SECTION lpCriticalSection );
参数说明:
LPCRITICAL_SECTION:指向关键节对象的指针。
3. 进入关键节
这里给出官方文档地址:enterCriticalSection 函数 (synchapi.h)
函数功能:等待指定关键部分对象的所有权。 此函数将在授予调用线程所有权时返回。
语法:
void EnterCriticalSection( [in, out] LPCRITICAL_SECTION lpCriticalSection );
参数说明:
LPCRITICAL_SECTION指向关键节对象的指针。
4. 离开关键节
这里给出官方文档地址:LeaveCriticalSection 函数 (synchapi.h)
函数功能:释放指定关键节对象的所有权。
语法:
void LeaveCriticalSection( [in, out] LPCRITICAL_SECTION lpCriticalSection );
参数说明:
LPCRITICAL_SECTION:指向关键节对象的指针
5. 释放关键节资源
这里给出官方文档地址:deleteCriticalSection 函数 (synchapi.h)
函数功能:释放未拥有的关键节对象使用的所有资源。
语法:
void DeleteCriticalSection( [in, out] LPCRITICAL_SECTION lpCriticalSection );
参数说明:
LPCRITICAL_SECTION:指向关键节对象的指针。 该对象以前必须使用 InitializeCriticalSection 函数进行初始化。
我们来使用临界区解决最开始提出的线程安全问题:
#include <iostream> #include <windows.h> 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; }
我们来看看运行效果:
可以看到我们成功使用临界区实现了线程互斥。
三.互斥体
大家可以到官方文档中查看互斥对象:互斥体对象
互斥对象状态设置为当任何线程不拥有时发出信号,一次只能有一个线程可以拥有互斥体对象。与临界区不同的是,互斥体可以跨进程使用。
互斥体的使用相对来说较为简单,我们只需要创建互斥体对象即可使用。
1. 创建互斥体
使用CreateMutex()
函数,这里给出官方文档地址:createMutexA 函数 (synchapi.h)
语法:
HANDLE CreateMutexA( [in, optional] LPSECURITY_ATTRIBUTES lpMutexAttributes, [in] BOOL bInitialOwner, [in, optional] LPCSTR lpName );
参数说明:
LPSECURITY_ATTRIBUTES:安全属性,我们一般不关注。
bInitialOwner:如果此值设置为TRUE,并且调用方创建了互斥体,则调用线程获取互斥体对象的使用权。也就是说,我们在某个线程中创建了互斥体,并且此字段设置为TRUE,那么这个线程立即拥有该互斥体。
lpName:为互斥体对象命名。
返回值:互斥体句柄。
2. 多线程使用互斥体
临界区对象创建并且初始化之后,可以使用进入临界区或者离开临界区的方式来实现线程互斥,那么我们如何使用互斥体实现线程间的互斥呢?
- 我们可以在创建互斥体的时候,将bInitialOwner字段设置为FALSE,然后在使用互斥体的时候,采用等候消息的方式来获取互斥体使用权:
这里给出官方文档地址:WaitForSingleObject 函数 (synchapi.h)
使用WaitForSingleObject
函数。
语法:
DWORD WaitForSingleObjectEx( [in] HANDLE hHandle, [in] DWORD dwMilliseconds, );
参数说明:
hHandle:要等待的对象的句柄。
dwMilliseconds:超时间隔(以毫秒为单位)。
- 在一个线程使用互斥体结束后,想要释放互斥体让其他线程使用,我们可以使用
ReleaseMutex()
函数来释放已获得的互斥体。
这里给出官方文档地址:releaseMutex 函数 (synchapi.h)
函数功能:释放指定互斥对象的所有权。
语法:
BOOL ReleaseMutex( [in] HANDLE hMutex );
参数说明:hMutex:要释放的互斥体对象句柄。
- 在互斥体使用结束后,我们可以使用
CloseHandle
函数来关闭互斥体。
这里给出官方文档地址:closeHandle 函数 (handleapi.h)
函数功能:关闭打开的对象句柄。