简介
我们为了提高程序运行效率,大多会采用多线程工作处理,最简单的例子就是 界面线程+工作线程 的模式。
不同的线程处理不同的功能,所以随之而来的就有多个线程访问同一段数据时的竞争问题。
数据传输方式
微软自身有一套消息处理机制,在一些逻辑简单的场合,我们可以直接使用这一机制,通过自定义消息来
发送数据,一般使用 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(原子操作) 等。其中 事件 信号量 互斥体 含有 信号态 与 非信号态 两种状态。我们使用等待函数
WaitForSingleObject 和 WaitForMultipleObjects 来等待相关对象由 非信号态 切换到 信号态。
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, _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过后需要主动释放。
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 2 3
| LONG __cdecl InterlockedDecrement( _Inout_ LONG volatile *Addend );
|
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 资料