多线程同步概述
线程之间通信的两个基本问题是互斥和同步
- 线程同步是指线程之间所具有的一种制约关系 , 一个线程的执行依赖另一个线程的消息 , 当它没有得到另一个线程的消息时应等待 , 直到消息到达时才被唤醒 ;
- 线程互斥是指对于共享的操作系统资源 , 在各线程访问时的排它性 ; 当有若干个线程都要使用某一共享资源时 , 任何时刻最多只允许一个线程去使用 , 其它要使用该资源的线程必须等待 , 直到占用资源者释放该资源 ;
线程互斥是一种特殊的线程同步
实际上 , 互斥和同步对应着线程间通信发生的两种情况 :
- 当有多个线程访问共享资源而不使资源被破坏时 ;
- 当一个线程需要将某个任务已经完成的情况通知另外一个或多个线程时 ;
从大的方面讲 , 线程的同步可分用户模式的线程同步和内核对象的线程同步两大类 ;
用户模式中线程的同步方法主要有原子访问和临界区等方法 ; 其特点是同步速度特别快 , 适合于对线程运行速度有严格要求的场合 ;
内核对象的线程同步则主要由事件 , 等待定时器 , 信号量以及信号灯等内核对象构成 ; 由于这种同步机制使用了内核对象 , 使用时必须将线程从用户模式切换到内核模式 , 而这种转换一般要耗费近千个CPU周期 , 因此同步速度较慢 , 但在适用性上却要远优于用户模式的线程同步方式 ;
在 Win32 中 , 同步机制主要有以下几种 :
- 事件 (Event) ;
- 信号量 (Semaphore) ;
- 互斥量 (Mutex) ;
- 临界区 (Critical Section) ;
具体实现
1. 事件
事件 (Event) 是 Win32 提供的最灵活的线程间同步方式 , 事件可以处于激发状态 (signaled or true) 或未激发状态 (unsignal or false) ; 根据状态变迁方式的不同 , 事件可分为两类 :
- 手动设置 : 这种对象只可能用程序手动设置 , 在需要该事件或者事件发生时 , 采用 SetEvent 及 ResetEvent 来进行设置 ;
- 自动恢复 : 一旦事件发生并被处理后 , 自动恢复到没有事件状态 , 不需要再次设置 ;
使用事件 (Event)机制应注意以下事项 :
- 如果跨进程访问事件 , 必须对事件命名 , 在对事件命名的时候 , 要注意不要与系统命名空间中的其它全局命名对象冲突 ;
- 事件是否要自动恢复 ;
- 事件的初始状态设置 ;
由于 Event 对象属于内核对象 , 故进程 B 可以调用 OpenEvent 函数通过对象的名字获得进程 A 中 Event 对象的句柄 , 然后将这个句柄用于 ResetEvent , SetEvent 和 WaitForMultipleObjects 等函数中 ;
相关的函数信息如下 :
创建 Event
HANDLE CreateEvent(
LPSECURITY_ATTRIBUTES lpEventAttributes, // SECURITY_ATTRIBUTES结构指针,可为 NULL
BOOL bManualReset, // 手动/自动 , TRUE : 在 WaitForSingleObject 后必须手动调用 ResetEvent 清除信号
FALSE:在 WaitForSingleObject 后 , 系统自动清除事件信号
BOOL bInitialState, // 初始状态
LPCTSTR lpName // 事件的名称
);
将 Event 置为发信号
BOOL SetEvent(HANDLE hEvent);
将 Event 置为不发信号
BOOL ResetEvent(HANDLE hEvent);
实现代码如下 :
#include "stdafx.h"
#include <Windows.h>
#include <iostream>
using namespace std;
DWORD WINAPI ThreadFunc_1(LPVOID lpParmeter);
DWORD WINAPI ThreadFunc_2(LPVOID lpParmeter);
HANDLE g_hEvent = INVALID_HANDLE_VALUE;
int g_nCount = 100;
int main()
{
HANDLE hThreads[2];
hThreads[0] = INVALID_HANDLE_VALUE;
hThreads[1] = INVALID_HANDLE_VALUE;
g_hEvent = CreateEvent(NULL, false, false, "Event");
if (!g_hEvent)
{
cout<<"Failed To Create Event"<<endl;
return 0;
}
// 创建两个子线程
hThreads[0] = CreateThread(NULL, 0, ThreadFunc_1, NULL, 0, NULL);
hThreads[1] = CreateThread(NULL, 0, ThreadFunc_2, NULL, 0, NULL);
// 设置全局 Event 为有信号状态
SetEvent(g_hEvent);
// 阻塞等待两个线程结束
WaitForMultipleObjects(2, hThreads, TRUE, INFINITE);
// 关闭句柄
CloseHandle(hThreads[0]);
CloseHandle(hThreads[1]);
return 0;
}
DWORD WINAPI ThreadFunc_1(LPVOID lpParmeter)
{
while (true)
{
// 请求事件对象
WaitForSingleObject(g_hEvent, INFINITE); // INFINITE : 无限等待
if (g_nCount > 0)
{
Sleep(20);
cout<<"ThreadFunc_1 : "<<g_nCount--<<endl;
SetEvent(g_hEvent);
}
else
{
SetEvent(g_hEvent);
break;
}
}
return 0;
}
DWORD WINAPI ThreadFunc_2(LPVOID lpParameter)
{
while (true)
{
WaitForSingleObject(g_hEvent,INFINITE);
if (g_nCount > 0)
{
Sleep(20);
cout << "ThreadFunc_2 : " << g_nCount-- << endl;
SetEvent(g_hEvent);
}
else
{
SetEvent(g_hEvent);
break;
}
}
return 0;
}
2. 信号量
信号量是维护 0 到指定最大值之间的同步对象 ; 信号量状态在其计数大于 0 时是有信号的 , 而其计数是 0 时是无信号的 ; 信号量对象在控制上可以支持有限数量共享资源的访问 , 是一个 生产者-消费者模型
;
信号量的特点和用途可用下列几句话定义:
- 如果当前资源的数量大于 0 , 则信号量有效 ;
- 如果当前资源数量是 0 , 则信号量无效 ;
- 系统决不允许当前资源的数量为负值 ;
- 当前资源数量决不能大于最大资源数量 ;
相关的函数信息如下 :
创建信号量
HANDLE CreateSemaphore(
LPSECURITY_ATTRIBUTESlpSemaphoreAttributes, // 指定一个SECURITY_ATTRIBUTES结构,或传递零值
LONGlInitialCount, // 设置信号量的初始计数 , 可设置零到 lMaximumCount 之间的一个值
LONGlMaximumCount, // 设置信号量的最大计数
LPCTSTRlpName // 指定信号量对象的名称
);
释放信号量 , 通过调用ReleaseSemaphore函数 , 线程就能够对信标的当前资源数量进行递增 , 该函数原型为 :
BOOL WINAPI ReleaseSemaphore(
HANDLE hSemaphore, // 所要操作的信号量对象的句柄
LONG lReleaseCount, // 这个信号量对象在当前基础上所要增加的值 , 这个值必须大于0
LPLONG lpPreviousCount // 指向返回信号量上次值的变量的指针 , 如果不需要可以设置为NULL
);
打开信号量 , 和其他核心对象一样 , 信号量也可以通过名字跨进程访问 , 打开信号量的 API 为 :
HANDLE OpenSemaphore (
DWORD fdwAccess, // 表示访问权限 , 一般传入 `SEMAPHORE_ALL_ACCESS` 来完全访问
BOOL bInherithandle, // 如果允许子进程继承句柄,则设为TRUE
PCTSTR pszName // 指定要打开的对象的名字
);
实现代码如下 :
#include "stdafx.h"
#include <Windows.h>
#include <iostream>
#include <vector>
using namespace std;
DWORD WINAPI ThreadFunc_1(LPVOID lpParmeter);
DWORD WINAPI ThreadFunc_2(LPVOID lpParmeter);
HANDLE g_hSemaphore = INVALID_HANDLE_VALUE;
int g_nCount = 100;
int main()
{
HANDLE hThreads[2];
hThreads[0] = INVALID_HANDLE_VALUE;
hThreads[1] = INVALID_HANDLE_VALUE;
g_hSemaphore = CreateSemaphore(NULL, 1, 1, "Semaphore");
if (g_hSemaphore == INVALID_HANDLE_VALUE)
{
cout<<"Failed To Create Semaphore"<<endl;
return 0;
}
hThreads[0] = CreateThread(NULL, 0, ThreadFunc_1, NULL, 0, NULL);
hThreads[1] = CreateThread(NULL, 0, ThreadFunc_2, NULL, 0, NULL);
WaitForMultipleObjects(2, hThreads, TRUE, INFINITE);
CloseHandle(hThreads[0]);
CloseHandle(hThreads[1]);
return 0;
}
DWORD WINAPI ThreadFunc_1(LPVOID lpParmeter)
{
while (true)
{
// 阻塞等待 , 并消耗一个信号量
WaitForSingleObject(g_hSemaphore, INFINITE);
if (g_nCount > 0)
{
Sleep(20);
cout <<"ThreadFunc_1 : "<< g_nCount--<<endl;
// 释放一个信号量
ReleaseSemaphore(g_hSemaphore, 1, NULL);
}
else
{
ReleaseSemaphore(g_hSemaphore, 1, NULL);
break;
}
}
return 0;
}
DWORD WINAPI ThreadFunc_2(LPVOID lpParameter)
{
while (true)
{
WaitForSingleObject(g_hSemaphore, INFINITE);
if (g_nCount > 0)
{
Sleep(20);
cout<<"ThreadFunc_2 : "<<g_nCount--<<endl;
ReleaseSemaphore(g_hSemaphore, 1, NULL);
}
else
{
ReleaseSemaphore(g_hSemaphore, 1, NULL);
break;
}
}
return 0;
}
3. 互斥量
当两个或更多线程需要同时访问一个共享资源时 , 系统需要使用同步机制来确保一次只有一个线程使用该资源 ; Mutex 是同步基元 , 它只向一个线程授予对共享资源的独占访问权 , 如果一个线程获取了互斥体 , 则要获取该互斥体的第二个线程将被挂起 , 直到第一个线程释放该互斥体 ;
相关的函数信息如下 :
创建互斥量
HANDLE CreateMutex(
LPSECURITY_ATTRIBUTESlpMutexAttributes, // 指定一个 SECURITY_ATTRIBUTES 结构 , 或传递零值
BOOLbInitialOwner, // 初始化互斥对象的所有者 , 如创建进程希望立即拥有互斥体则设为 TRUE , 该互斥体同时只能由一个线程拥有
LPCTSTRlpName // 指定互斥体对象的名字
);
释放互斥对象的控制权
BOOL WINAPI ReleaseMutex(HANDLE hMutex);
实现代码如下 :
#include "stdafx.h"
#include <windows.h>
#include <iostream>
using namespace std;
DWORD WINAPI ThreadFunc_1(LPVOID lpParmeter);
DWORD WINAPI ThreadFunc_2(LPVOID lpParmeter);
HANDLE g_hMutex = INVALID_HANDLE_VALUE;
int g_nCount = 100;
int main()
{
HANDLE hThreads[2];
hThreads[0] = INVALID_HANDLE_VALUE;
hThreads[1] = INVALID_HANDLE_VALUE;
g_hMutex = CreateMutex(NULL, FALSE, "Mutex"); // 第二个参数 FALSE 表示当前创建进程没有所有权,
if (!g_hMutex)
{
cout<<"Failed to Create Mutex"<<endl;
return 0;
}
hThreads[0] = CreateThread(NULL, 0, ThreadFunc_1, NULL, 0, NULL);
hThreads[1] = CreateThread(NULL, 0, ThreadFunc_2, NULL, 0, NULL);
WaitForMultipleObjects(2, hThreads, TRUE, INFINITE);
CloseHandle(hThreads[0]);
CloseHandle(hThreads[1]);
return 0;
}
DWORD WINAPI ThreadFunc_1(LPVOID lpParmeter)
{
while (true)
{
WaitForSingleObject(g_hMutex, INFINITE);
if (g_nCount > 0)
{
Sleep(20);
cout<<"ThreadFunc_1 : "<<g_nCount--<<endl;
// 释放互斥量
ReleaseMutex(g_hMutex);
}
else
{
ReleaseMutex(g_hMutex);
break;
}
}
return 0;
}
DWORD WINAPI ThreadFunc_2(LPVOID lpParameter)
{
while (true)
{
WaitForSingleObject(g_hMutex,INFINITE);
if (g_nCount > 0)
{
Sleep(20);
cout<<"ThreadFunc_2 : "<<g_nCount--<<endl;
ReleaseMutex(g_hMutex);
}
else
{
ReleaseMutex(g_hMutex);
break;
}
}
return 0;
}
4. 临界区
临界区 (Critical Section) 是一段独占对某些共享资源访问的代码 , 在任意时刻只允许一个线程对共享资源进行访问 ; 如果有多个线程试图同时访问临界区 , 那么在有一个线程进入后其他所有试图访问此临界区的线程将被挂起 , 并一直持续到进入临界区的线程离开 ; 临界区在被释放后 , 其他线程可以继续抢占 , 并以此达到用原子方式操作共享资源的目的 ;
临界区在使用时以 CRITICAL_SECTION
结构对象保护共享资源 , 并分别用 EnterCriticalSection()
和 LeaveCriticalSection()
函数去标识和释放一个临界区 ; 所用到的 CRITICAL_SECTION
结构对象必须经过 InitializeCriticalSection()
的初始化后才能使用 , 而且必须确保所有线程中的任何试图访问此共享资源的代码都处在此临界区的保护之下 ; 否则临界区将不会起到应有的作用 , 共享资源依然有被破坏的可能 ;
相关的函数信息如下 :
初始化临界区
VOID InitializeCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
进入临界区
VOID EnterCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
离开临界区
VOID LeaveCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
释放临界区资源
VOID DeleteCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
实现代码如下 :
#include "stdafx.h"
#include <windows.h>
#include <iostream>
using namespace std;
DWORD WINAPI ThreadFunc_1(LPVOID lpParmeter);
DWORD WINAPI ThreadFunc_2(LPVOID lpParmeter);
CRITICAL_SECTION g_csCriticalSection;
static int g_nCount = 100;
int main()
{
HANDLE hThreads[2];
hThreads[0] = INVALID_HANDLE_VALUE;
hThreads[1] = INVALID_HANDLE_VALUE;
// 初始化临界区对象
InitializeCriticalSection(&g_csCriticalSection);
hThreads[0] = CreateThread(NULL, 0, ThreadFunc_1, NULL, 0, NULL);
hThreads[1] = CreateThread(NULL, 0, ThreadFunc_2, NULL, 0, NULL);
WaitForMultipleObjects(2, hThreads, TRUE, INFINITE);
CloseHandle(hThreads[0]);
CloseHandle(hThreads[1]);
// 释放临界区资源
DeleteCriticalSection(&g_csCriticalSection);
return 0;
}
DWORD WINAPI ThreadFunc_1(LPVOID lpParmeter)
{
while (true)
{
// 进入临界区 , 获得所有权 , 其他线程就等待
EnterCriticalSection(&g_csCriticalSection);
if (g_nCount > 0)
{
Sleep(20);
cout<<"ThreadFunc_1 : "<<g_nCount--<<endl;
// 离开临界区 , 释放所有权
LeaveCriticalSection(&g_csCriticalSection);
}
else
{
LeaveCriticalSection(&g_csCriticalSection);
break;
}
}
return 0;
}
DWORD WINAPI ThreadFunc_2(LPVOID lpParameter)
{
while (true)
{
EnterCriticalSection(&g_csCriticalSection);
if (g_nCount > 0)
{
Sleep(20);
cout<<"ThreadFunc_2 : "<<g_nCount--<<endl;
LeaveCriticalSection(&g_csCriticalSection);
}
else
{
LeaveCriticalSection(&g_csCriticalSection);
break;
}
}
return 0;
}
总结
- 临界区 : 通过对多线程的串行化来访问公共资源或一段代码 , 速度快 , 适合控制数据访问 ;
- 互斥量 : 为协调共同对一个共享资源的单独访问而设计的 ;
- 信号量 : 为控制一个具有有限数量用户资源而设计 ;
- 事件 : 用来通知线程有一些事件已发生 , 从而启动后继任务的开始 ;
临界区 Critical Section (同一个进程内 , 实现互斥)
保证在某一时刻只有一个线程能访问数据的简便办法 ; 在任意时刻只允许一个线程对共享资源进行访问 ; 如果有多个线程试图同时访问临界区 , 那么在有一个线程进入后其他所有试图访问此临界区的线程将被挂起 , 并一直持续到进入临界区的线程离开 ; 临界区在被释放后 , 其他线程可以继续抢占 , 并以此达到用原子方式操作共享资源的目的 ;
互斥量 Mutex (可以跨进程 , 实现互斥)
互斥量跟临界区很相似 , 只有拥有互斥对象的线程才具有访问资源的权限 , 由于互斥对象只有一个 , 因此就决定了任何情况下此共享资源都不会同时被多个线程所访问 ; 当前占据资源的线程在任务处理完后应将拥有的互斥对象交出 , 以便其他线程在获得后得以访问资源 ; 互斥量比临界区复杂 ; 因为使用互斥不仅仅能够在同一应用程序不同线程中实现资源的安全共享 , 而且可以在不同应用程序的线程之间实现对资源的安全共享 ;
互斥量与临界区的作用非常相似 , 但互斥量是可以命名的 , 也就是说它可以跨越进程使用 ; 所以创建互斥量需要的资源更多 , 所以如果只为了在进程内部是用的话使用临界区会带来速度上的优势并能够减少资源占用量 ;
信号量 Semaphores (主要是实现同步 , 可以跨进程)
信号量对象对线程的同步方式与前面几种方法不同 , 信号允许多个线程同时使用共享资源 , 这与操作系统中的 PV 操作相同 ; 它指出了同时访问共享资源的线程最大数目 ; 它允许多个线程在同一时刻访问同一资源 , 但是需要限制在同一时刻访问此资源的最大线程数目 ; 一般是将当前可用资源计数设置为最大资源计数 , 每增加一个线程对共享资源的访问 , 当前可用资源计数就会减 1 , 只要当前可用资源计数是大于 0 的 , 就可以发出信号量信号 ; 但是当前可用计数减小到 0 时则说明当前占用资源的线程数已经达到了所允许的最大数目 , 不能在允许其他线程的进入 , 此时的信号量信号将无法发出 ;
事件 Event (实现同步 , 可以跨进程)
事件对象也可以通过通知操作的方式来保持线程的同步 ; 并且可以实现不同进程中的线程同步操作 ;
作者 Github : tojohnonly , 博客 : EnskDeCode