技术演进中的开发沉思-8:window编程系列-内核对象线程同步(上)

前面几个章节,都在谈了windows线程开发及内核相关知识。在 Windows 系统构建的编程王国中,线程们就像穿梭于城市各个角落的快递员,各自奔波着执行任务。有的忙着搬运数据包裹,有的赶着处理用户请求。但城市的道路与资源有限,如果快递员们各自为政、横冲直撞,必然会导致交通堵塞与混乱。这时候,就需要一系列交通规则和调度员来维持秩序,而 Windows 内核对象正是承担起了这一重任,等待函数则是调度员手中那指挥若定的哨子。今天我们聊聊内核对象和线程同步。

一、用内核对象进行线程同步

1、等待函数

在编程的 “城市道路” 上,等待函数就是那一盏盏交通信号灯。当线程执行到WaitForSingleObject或WaitForMultipleObjects这样的等待函数时,就如同快递员骑着车风风火火地赶到路口,却被亮起的红灯拦住了去路,只能停下脚步,耐心等待合适的时机再重新出发。

WaitForSingleObject专注于等待单个内核对象变为有信号状态,就像快递员只关注路口某一个方向的信号灯变化。而WaitForMultipleObjects则更像是在复杂的十字路口,快递员需要同时留意好几个方向的信号灯,只有当满足特定条件(所有或部分信号灯变绿,取决于bWaitAll参数的设置)时,才能继续前行。

记得曾参与过一个监控系统的开发项目,系统需要实时采集多个传感器的数据。其中,数据处理线程就像是负责分拣包裹的仓库管理员,它必须等待数据采集线程将数据 “包裹” 送达后,才能进行下一步处理。在代码中,我们使用WaitForSingleObject来实现这一同步过程:


#include <windows.h>

#include <iostream>

// 模拟数据采集线程函数

DWORD WINAPI DataCollectThreadProc(LPVOID lpParam) {

HANDLE hEvent = *(HANDLE*)lpParam;

std::cout << "数据采集线程开始工作" << std::endl;

// 模拟采集数据过程,耗时2秒

Sleep(2000);

std::cout << "数据采集完成,触发事件" << std::endl;

SetEvent(hEvent);

return 0;

}

int main() {

// 创建一个自动重置的事件对象,初始状态为无信号

HANDLE hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);

if (hEvent == NULL) {

std::cerr << "创建事件失败" << std::endl;

return 1;

}

HANDLE hThread;

DWORD dwThreadId;

// 创建数据采集线程

hThread = CreateThread(NULL, 0, DataCollectThreadProc, &hEvent, 0, &dwThreadId);

if (hThread == NULL) {

std::cerr << "创建线程失败" << std::endl;

CloseHandle(hEvent);

return 1;

}

std::cout << "数据处理线程等待数据采集完成" << std::endl;

// 数据处理线程等待事件变为有信号状态,无限等待

WaitForSingleObject(hEvent, INFINITE);

std::cout << "开始处理采集到的数据" << std::endl;

// 关闭事件句柄和线程句柄

CloseHandle(hEvent);

CloseHandle(hThread);

std::cout << "所有线程执行完毕" << std::endl;

return 0;

}

在这个例子里,数据处理线程通过WaitForSingleObject等待事件内核对象hEvent被触发,就像仓库管理员守在门口,眼睛紧紧盯着路口信号灯,一旦变绿(事件有信号),便立刻行动起来。

2、等待成功所引起的副作用

然而,等待成功并不是故事的完美结局,它往往会引发一系列连锁反应,就如同推倒第一块多米诺骨牌,随之而来的是一连串骨牌的倾倒。当一个线程成功获取到资源并进行操作后,资源的状态会发生改变,这一变化可能会像蝴蝶效应一般,影响到其他正在等待该资源的线程。

我在开发一款多用户协作的文档编辑软件时,就深刻体会到了这种副作用的威力。多个线程同时对文档内容进行修改,当其中一个线程成功获取到文档资源的访问权并修改了内容后,如果其他线程没有及时感知到这种变化,继续按照旧的内容进行操作,就会导致文档数据混乱,出现内容丢失或错误叠加的情况。这就好比多个作家共同创作一本书,一个人修改了章节内容后,却没有通知其他人,结果后续的创作就会变得一团糟。为了避免这种情况,我们在代码中采用了加锁机制,就像给文档加上一把锁,同一时间只有一个线程能拿到钥匙进入 “编辑房间”,从而保证了数据的一致性和准确性。

3、事件内核对象

事件内核对象是众多内核对象中极为常用的一种,它就像学校里准时响起的上课铃,铃声一响,学生们便知道该回到教室开始上课了。在代码的世界里,事件内核对象也有着两种状态:有信号和无信号。当它处于有信号状态时,就如同上课铃响起,那些正在等待该事件的线程会立刻被唤醒,从暂停状态中恢复,继续执行后续的任务代码。

事件分为手动重置和自动重置两种类型。手动重置事件就像学校里的特殊铃声,需要管理员手动控制响铃和停止;而自动重置事件则如同普通的上课铃,响过一次,完成提醒任务后,便会自动恢复到初始状态。在一个在线视频播放系统的开发中,我们利用事件内核对象实现了视频缓冲与播放的同步。当视频数据缓冲到一定程度时,触发事件通知播放线程开始播放视频,而播放线程则通过WaitForSingleObject等待事件的触发,就像学生们等着上课铃响才走进教室。

4、可等待的计时器内核对象

可等待的计时器内核对象,恰似厨房中那嘀嗒作响的定时器,它能在设定的时间一到,便发出清脆的提醒声,告知线程是时候执行特定任务了。在实际的软件开发中,我们常常会用到它来实现定时任务,比如每隔一段时间自动清理系统缓存,释放内存资源;或者定时对重要数据进行备份,防止数据丢失。

创建可等待计时器的过程,就像我们在厨房设置定时器的参数。通过CreateWaitableTimer函数设定好安全属性、是否手动重置以及计时器名称等信息后,再使用SetWaitableTimer函数来设置触发时间和周期。如果需要在计时结束时自动执行一些额外操作,还可以添加 APC 调用,这就好比给定时器赋予了特殊功能,不仅能提醒,还能帮忙完成一些简单的任务。

但就像厨房定时器的时间可能会存在些许误差一样,可等待计时器在时间精度上也并非绝对精准。在开发一个对时间要求极高的实时交易系统时,我们就遇到了这个问题。为了减小时间误差带来的影响,我们采用了高精度的计时函数,并结合多次测量取平均值的方法,就像反复核对厨房定时器的时间,确保任务能在最准确的时刻执行。

5、信号量内核对象

信号量内核对象的作用,如同停车场入口的智能闸机,它精准地控制着进入停车场(获取资源)的车辆(线程)数量。我们可以预先设置好信号量的初始值和最大值,这两个数值就决定了停车场的容量上限。当有线程获取到信号量时,闸机打开,线程进入停车场,信号量的计数随之减一;当线程释放信号量时,计数加一,就像车辆离开停车场,闸机再次开放。

在开发一个多线程下载工具时,我们使用信号量来控制同时下载的文件数量,避免因线程过多占用大量系统资源,导致系统卡顿。通过合理设置信号量的数值,就像规划好停车场的车位数量,既保证了下载效率,又维持了系统的稳定运行。

6、互斥量内核对象

互斥量内核对象,就像是会议室那把独一无二的钥匙,在同一时刻,只有一个线程能够拿到这把钥匙,进入会议室(访问共享资源)进行工作。它有效地解决了多个线程同时访问共享资源时可能产生的冲突问题,确保资源在同一时间只会被一个线程使用。

但这把 “钥匙” 也存在隐患,那就是遗弃问题。就像有人拿着会议室钥匙离开公司却忘记归还,导致其他人无法进入会议室开会。在编程中,如果拥有互斥量的线程意外终止,没有及时释放互斥量,就会造成其他线程无限期等待,形成死锁。

与关键段相比,互斥量更具开放性,它可以在不同进程间传递 “钥匙”,就像公共会议室的钥匙可供多个部门使用;而关键段则更像是部门内部的小会议室,钥匙只能在同一进程的线程间传递。在一个多进程协作的大型项目中,我们使用互斥量来保护共享的数据库资源,确保数据的安全与准确访问。

2. 同步设备 I/O 与异步设备 I/O

在 Windows 编程的庞大体系里,同步设备 I/O 和异步设备 I/O 如同两种截然不同的工作模式。同步 I/O 就像传统工厂里的流水线作业,每完成一道工序,都必须等待下一道工序反馈完成信号,才能继续进行;而异步 I/O 则更像是现代灵活的项目小组,成员们各自分工,不需要时刻等待他人完成任务,而是在任务结束时通过消息通知来协同工作。

在同步 I/O 操作中,线程一旦发起操作,就会被牢牢阻塞,如同被定身咒束缚,只能专注等待操作完成,期间无法去处理其他任何任务。就像早期的邮政系统,每寄出一封信,都得眼巴巴等着邮局确认信件送达,才能接着处理下一封信件。在程序里,当我们使用同步方式读取文件时,线程会停在读取操作这一步,如同被按下暂停键,直到文件数据全部读取到内存中,才会继续执行后续代码。

而异步 I/O 则展现出完全不同的灵活性。线程发起 I/O 操作后,无需停留等待,可立即转身去执行其他任务,就像快递员把包裹交给中转站后,不用在原地傻等,而是继续去揽收其他包裹。系统会在 I/O 操作完成时,通过事件通知、完成例程等方式告知线程。比如在网络编程中,使用异步 I/O 可以在发送或接收数据的同时,处理用户界面的交互、数据的预处理等任务,极大提升了程序的响应速度和整体性能,让程序运行起来更加流畅自如。

最后小结

历经无数项目的洗礼,Windows 内核对象线程同步相关的技术知识早已融入我的编程血脉。这些看似冰冷的函数与对象,在无数个日夜的调试与优化中,渐渐有了温度,承载着我对编程的热爱与执着。希望我的这些分享,能让更多人感受到 Windows 编程世界的奇妙之处,还有工作要处理,本想一个章节写完的,就分成两个篇幅来写吧!这些技术虽然是20年前的内容,但是很多思想和方法对于技术人员都是相通的,希望对您有帮助!未完待续........

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值