Windows核心编程 读书笔记(2)
进/线程同步
用户模式下的线程同步
可以理解为同一进程内的线程同步, 如果不涉及跨进程, 使用这些函数同步效率更高.
原子访问 Interlocked
对标C++的atomic, 可以实现完全替代, C++atomic在windows上运行底层用的就是这个.
关键段 critical_section
对标C++的mutex, 基本上完全替代, 除了critical_section
可以通过EnterCriticalSection
获取线程访问资源(是否有线程访问资源, 哪个线程在访问资源等).
内部逻辑都是先进行一段时间的自旋, 如果仍未解锁则切换到内核模式进入等待.
读写锁 SRWLock
对标C++的shared_mutex, 可以实现完全替代.
由于本人对于shared_mutex使用比较少, 这里复习一下使用方式 :
简单说就是读写锁在读时用shared_lock锁, 在写时用unique_lock锁.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19#include <shared_mutex>
#include <vector>
std::shared_mutex smtx;
std::vector<int> data;
// 读线程(多个线程可同时执行)
void ReadData() {
std::shared_lock<std::shared_mutex> lock(smtx); // 读锁
for (int val : data) {
std::cout << val << " ";
}
} // lock 自动释放
// 写线程(独占访问)
void WriteData(int value) {
std::unique_lock<std::shared_mutex> lock(smtx); // 写锁
data.push_back(value);
} // lock 自动释放延迟加锁(
defer_lock
):不是创建即加锁, 可以做些条件判断后再手动加.
1
2
3
4
5std::shared_lock<std::shared_mutex> lock(smtx, std::defer_lock);
if (/* 检查条件 */) {
lock.lock(); // 手动加锁
// 读操作...
}尝试加锁(
try_lock
):非阻塞尝试获取读锁,失败时立即返回.
1
2
3
4
5
6std::shared_lock<std::shared_mutex> lock(smtx, std::try_to_lock);
if (lock.owns_lock()) { // 检查是否加锁成功
// 读操作...
} else {
// 执行其他逻辑
}超时加锁(
try_lock_for
/try_lock_until
)在指定时间内尝试获取读锁.
1
2
3
4
5using namespace std::chrono_literals;
std::shared_lock<std::shared_mutex> lock(smtx, 100ms); // 等待最多 100ms
if (lock.owns_lock()) {
// 读操作...
}
还可以和条件变量结合 :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19std::shared_mutex smtx;
std::condition_variable_any cv;
bool data_ready = false;
// 读线程等待条件
void WaitForData() {
std::shared_lock<std::shared_mutex> lock(smtx);
cv.wait(lock, [] { return data_ready; }); // 自动释放锁并等待
// 数据已就绪,继续读操作...
}
// 写线程通知条件
void NotifyDataReady() {
{
std::unique_lock<std::shared_mutex> lock(smtx);
data_ready = true;
}
cv.notify_all(); // 唤醒所有等待线程
}
条件变量 SleepConditionVariable
对标C++的condition_variable, 可以完全替代.
锁使用
- 代码中任何地方都要以完全相同的顺序获取锁.
- 在锁区域中尽量避免调用高消耗函数, 假如只是希望投放到函数中的某些共享资源不要被修改, 那么可以先在锁区域中拷贝(C++有移动拷贝, 如果想直接把资源夺过来, 直接移动即可), 然后解锁放入函数执行, 如果有恢复的需要再swap回去就行了, 这会比整个函数加锁好.
用内核对象进行线程同步
这种同步使用于两种情况 :
- 跨进程同步
- 和内核对象强关联的同步
等待函数
共两种 :
WaitForSingleObject
: 等待单个内核对象.1
2
3
4DWORD WaitForSingleObject(
HANDLE hHandle, // 事件句柄
DWORD dwMilliseconds // 超时时间(INFINITE表示无限等待)
);WaitForMultipleObjects
: 等待多个内核对象.1
2
3
4
5
6DWORD WaitForMultipleObjects(
DWORD nCount, // 对象数量
const HANDLE *lpHandles, // 对象句柄数组
BOOL bWaitAll, // TRUE=等待所有,FALSE=等待任意一个
DWORD dwMilliseconds // 超时时间
);
使用方法非常简单, 传入 内核句柄 + 时间 即可, Multi版本传入的内核句柄数组, 还有一个标志位bWaitAll
, 该标志位传入false
表示有一个触发就继续, 传入true
表示所有触发才能继续.
如果仅限于进线程同步的使用, 使用方法和join差不多, 都是目标进线程都结束就触发.
触发条件
- 所有内核对象都会存在一种信号机制, 无信号表示未触发, 有信号表示触发.
- 不同的内核对象都有不同的信号触发情况, 最终目的都是为了根据对象的某些变化做出对应的反应.
下面是常见内核对象的触发条件 :
内核对象 | 常见创建函数 | 无信号状态 (等待原因) | 有信号状态 (触发条件) |
---|---|---|---|
线程 (Thread) | CreateThread |
线程正在运行 | 线程终止 |
进程 (Process) | CreateProcess |
进程正在运行 | 进程终止 |
互斥体 (Mutex) | CreateMutex |
被线程拥有 (Wait 后未Release ) |
不被任何线程拥有(已释放) |
事件 (Event) | CreateEvent |
手动重置:需要显式调用 SetEvent 自动重置:需要显式调用 SetEvent |
手动重置:SetEvent 设置,需ResetEvent 手动重置 自动重置:SetEvent 设置,系统自动重置(唤醒一个线程后即复位) |
信号量 (Semaphore) | CreateSemaphore |
计数器 == 0 | 计数器 > 0 (ReleaseSemaphore 增加计数) |
等待定时器 (Waitable Timer) | CreateWaitableTimer |
未到设定时间 | 到达设定时间(SetWaitableTimer ) |
作业对象 (Job Object) | CreateJobObject |
作业中的所有进程尚未全部终止 | 作业中的所有进程已全部终止 |
文件变更通知 | FindFirstChangeNotification |
指定目录/文件未发生变更 | 指定目录/文件发生了变更 |
事件对象
你可以认为这是一个配合等待函数实现事件触发的内核对象.
事件对象的信号有无是可以被随意设置的.
所谓”事件”, 你可以认为是在某些条件具备后可以执行的任务. 通常情况下是一个进/线程持有事件句柄调用等待函数, 在其他线程将某些条件准备好后触发事件句柄, 让该进程脱离等待.
事件对象的独特优势在于可以跨进程通知和一次性通知多个等待者.
SetEvent
:将事件句柄传入该函数, 即可立即触发该对象.
1
BOOL SetEvent(HANDLE hEvent);
CreateEvent
:1
2
3
4
5
6HANDLE CreateEvent(
LPSECURITY_ATTRIBUTES lpEventAttributes, // 安全属性,通常NULL
BOOL bManualReset, // 手动/自动重置 TRUE/FALSE
BOOL bInitialState, // 初始状态:TRUE(有信号)/FALSE(无信号)
LPCSTR lpName // <- 事件对象的名字(关键参数!)
);- bManualReset : 这里所谓的手动/自动重置(TRUE/FALSE), 其实就是在被
SetEvent
触发后是否重置为无信号. 实际我们会更关注其副现象 : 前者被SetEvent
触发会直接唤醒所有等待线程, 而后者只会唤醒一个, 类似于notice_all
和notice_one
的区别.
- bManualReset : 这里所谓的手动/自动重置(TRUE/FALSE), 其实就是在被
OpenEvent
:用于打开一个已有的事件句柄.
1
2
3
4
5HANDLE OpenEvent(
DWORD dwDesiredAccess, // 访问权限(如EVENT_ALL_ACCESS)
BOOL bInheritHandle, // 是否可被子进程继承
LPCSTR lpName // 事件名称
);关键在于传入的名字, 我们在
CreateEvent
中有设置, 只要名字相同, 就可以在不同进程拥有同一个事件句柄, 这是跨进程通信的关键. 因此如果想进行跨进程通信, 保证事件名称相同即可, 可以在规定中记录, 也可以注册到注册表中, 多个进程查表拿名字也行.ResetEvent
: 将已触发的句柄设置为未触发.
信号量
用于维护资源数量的进程间同步方式.
CreateSemaphore
/CreateSemaphoreEx
: 创建或打开一个命名/匿名信号量。OpenSemaphore
: 打开一个已存在的命名信号量。ReleaseSemaphore
: 增加信号量的计数,释放资源。WaitForSingleObject
/WaitForMultipleObjects
: 用于等待(获取)信号量。
互斥量
如果是需要跨进程通信, 可以使用该互斥量, 不然乖乖用std::mutex
就行.
- 该互斥锁具有“所有权”概念。如果一个线程持有内核互斥锁时终止,系统会将其视为已释放,并唤醒等待队列中的一个线程。
MsgWaitForMultipleObjectsEx
这个函数在WaitForMultipleObjectsEx
的基础上, 可以关心一些额外的事件(比如键盘和鼠标), 当这些事件触发后也会退出等待. 这种方式可以防止UI界面卡死.
1 |
|
1 |
|
不过现代C++也有更简单的替代方案(future + async) :
1 |
|