0%

线程之间的数据同步问题(1)

简介

我们为了提高程序运行效率,大多会采用多线程工作处理,最简单的例子就是 界面线程+工作线程 的模式。
不同的线程处理不同的功能,所以随之而来的就有多个线程访问同一段数据时的竞争问题。

数据传输方式

微软自身有一套消息处理机制,在一些逻辑简单的场合,我们可以直接使用这一机制,通过自定义消息来
发送数据,一般使用 PostThreadMessage 来向某个线程发送消息。

1
2
3
4
5
6
BOOL WINAPI PostThreadMessage(
_In_ DWORD idThread,
_In_ UINT Msg,
_In_ WPARAM wParam,
_In_ LPARAM lParam
);

除此之外更多的是使用 全局变量,来存储多线程之间共有的数据。

线程同步方式

多线程同时访问同一个全局变量时,假设这个全局变量是个结构体,当一个线程正在写数据时,另一个线程
正好也开始写数据,就会造成数据混乱。线程的同步就是让这种同时操作,限制为顺序操作,来保证每次的
操作都是完整的。

一般常用的同步方法,有 Event(事件) Semaphore(信号量) Mutex(互斥体) CriticalSection(临界区)
Interlock(原子操作) 等。其中 事件 信号量 互斥体 含有 信号态非信号态 两种状态。我们使用等待函数
WaitForSingleObjectWaitForMultipleObjects 来等待相关对象由 非信号态 切换到 信号态

1
2
3
4
DWORD WINAPI WaitForSingleObject(
_In_ HANDLE hHandle,
_In_ DWORD dwMilliseconds
);
1
2
3
4
5
6
DWORD WINAPI WaitForMultipleObjects(
_In_ DWORD nCount,
_In_ const HANDLE *lpHandles,
_In_ BOOL bWaitAll,
_In_ DWORD dwMilliseconds
);

在Wait时,我们可以指定等待的时间,单位是毫秒,INFINITE 表示无限等待。如果是因为 超时 等待结束,
会返回一个 WAIT_TIMEOUT 返回值,正常的因为 信号态 等待结束,返回值为 WAIT_OBJECT_0

事件/信号量/互斥体

当Wait的 事件 处于 非信号态 时,线程进入休眠状态,当 事件 变为 信号态 时,线程结束休眠继续执行。

1
2
3
4
5
6
HANDLE WINAPI CreateEvent(
_In_opt_ LPSECURITY_ATTRIBUTES lpEventAttributes,
_In_ BOOL bManualReset, // 自动 or 手动
_In_ BOOL bInitialState,
_In_opt_ LPCTSTR lpName
); // 创建事件
1
2
3
BOOL WINAPI SetEvent(
_In_ HANDLE hEvent
); // 设置为信号态
1
2
3
BOOL WINAPI ResetEvent(
_In_ HANDLE hEvent
); // 设置为非信号态

在创建 事件 时,还可以指定 自动恢复到非信号态 还是 手动恢复到非信号态。如果指定为自动,在Wait结束后,
会自动把当前的 信号态 设置为 非信号态,而手动就是需要我们自己来改变状态。

信号量的原理与事件相同,只不过信号量只有自动模式,并且可以指定 Wait次数 后才会切换到 非信号态

1
2
3
4
5
6
HANDLE WINAPI CreateSemaphore(
_In_opt_ LPSECURITY_ATTRIBUTES lpSemaphoreAttributes,
_In_ LONG lInitialCount,
_In_ LONG lMaximumCount,
_In_opt_ LPCTSTR lpName
); // 创建信号量
1
2
3
4
5
BOOL WINAPI ReleaseSemaphore(
_In_ HANDLE hSemaphore,
_In_ LONG lReleaseCount,
_Out_opt_ LPLONG lpPreviousCount
); // 增加Wait次数

如果我们同步操作的代码逻辑特别简短,事件信号量 频繁的 休眠线程唤醒线程 会浪费大量的时间,
这个时候我们可以使用 互斥体 来进行同步。互斥体 在Wait时不会休眠线程,而是会不停的轮询当前状态。
互斥体 只有手动模式,在Wait过后需要主动释放。

1
2
3
4
5
HANDLE WINAPI CreateMutex(
_In_opt_ LPSECURITY_ATTRIBUTES lpMutexAttributes,
_In_ BOOL bInitialOwner,
_In_opt_ LPCTSTR lpName
); // 创建互斥体
1
2
3
BOOL WINAPI ReleaseMutex(
_In_ HANDLE hMutex
); // 释放互斥体

在创建 事件 信号量 互斥体 时,有一个参数能够指定对象名称。如果是在进程内部使用,可以不指定名称,
也就是 匿名对象 ,我们直接使用句柄进行操作。如果指定名称创建 命名对象 ,就可以在不同的进程间使用,
在不同进程中打开或创建的同名事件,都是同一个事件。

事件示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int _tmain(int argc, _TCHAR* argv[])
{
// 创建手动控制的事件
HANDLE hEvent = CreateEvent(NULL, FALSE, FALSE, "Global\\TestEvent");
if (hEvent == NULL) return 0;
// 设置为信号态
SetEvent(hEvent);
// 等待事件为信号态
WaitForSingleObject(hEvent, INFINITE);
// 设置为非信号态
ResetEvent(hEvent);
// 关闭事件句柄
CloseHandle(hEvent);
return 0;
}

临界区/原子操作

临界区原子操作 的原理,跟 互斥体 类似,只不过不需要使用Wait函数。

1
2
3
void WINAPI InitializeCriticalSection(
_Out_ LPCRITICAL_SECTION lpCriticalSection
); // 初始化临界区
1
2
3
void WINAPI DeleteCriticalSection(
_Inout_ LPCRITICAL_SECTION lpCriticalSection
); // 删除临界区
1
2
3
void WINAPI EnterCriticalSection(
_Inout_ LPCRITICAL_SECTION lpCriticalSection
); // 进入临界区
1
2
3
void WINAPI LeaveCriticalSection(
_Inout_ LPCRITICAL_SECTION lpCriticalSection
); // 离开临界区

临界区 的使用示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 全局变量
CRITICAL_SECTION CriticalSection = { 0 };

int _tmain(int argc, _TCHAR* argv[])
{
// 初始化临界区,只需要初始化一次
InitializeCriticalSection(&CriticalSection);
// …… 其他代码 ……
// 程序退出时,删除临界区
DeleteCriticalSection(&CriticalSection);
return 0;
}

DWORD WINAPI ThreadProc(LPVOID lpParameter)
{
// …… 其他代码 ……
// 进入临界区
EnterCriticalSection(&CriticalSection);
// …… 需要同步操作的代码 ……
// 离开临界区
LeaveCriticalSection(&CriticalSection);
// …… 其他代码 ……
return 0;
}

原子操作 是操作系统提供的,只针对基本类型数据进行操作,比如针对LONG型数据执行算术运算

1
2
3
4
LONG __cdecl InterlockedAdd(
_Inout_ LONG volatile *Addend,
_In_ LONG Value
); // 原子操作加法
1
2
3
LONG __cdecl InterlockedIncrement(
_Inout_ LONG volatile *Addend
); // 原子操作加1
1
2
3
LONG __cdecl InterlockedDecrement(
_Inout_ LONG volatile *Addend
); // 原子操作减1
1
2
3
4
LONG __cdecl InterlockedExchange(
_Inout_ LONG volatile *Target,
_In_ LONG Value
); // 原子操作数据交换
1
2
3
4
PVOID __cdecl InterlockedExchangePointer(
_Inout_ PVOID volatile *Target,
_In_ PVOID Value
); // 原子操作地址交换

还有大量的其他原子操作函数,这里只列出了常见的几种,其他请自行查阅微软的 MSDN 资料