WINDOWS核心编程——线程同步

来源:互联网 发布:51牛股数据分析大师 编辑:程序博客网 时间:2024/06/06 14:20

线程同步主要涉及两个问题:1.不同的线程使用同一个资源但要保证资源完整有效。2.不同的线程之间的互相协作完成一个工作。所有的线程同步无外乎就是高效的(不浪费CPU)的实现这两个目的。

同样我们需要了解在多线程环境下为什么会出现这两个问题和这两个问题如何表现:

1.刨除软件设计本身的原因,在多线程环境下CPU的高速缓存行在多线程环境下引入了之前未考虑到的复杂性,对于在不同CPU中执行的线程使用同一资源(变量x),对于被读入高速缓存的变量x,若CPU1中执行的线程thread1将x的值修改为1,同样在CPU2中执行的线程thread2也把x读入高速缓存并将x的值修改为2,再在合适的时候将x写回到内存中,后一个写回的总是会覆盖掉前一个写回的值这是就会发生错误。为了解决这个问题CPU让高速缓存中的变量被修改时其他使用这个变量的高速缓存失效重新去内存中取也会让CPU的性能损失。

2.有些编译器在生成指令时使用寄存器保存变量,若在thread1中变量x被载入寄存器中,所有在thread2中对x的更改便不会被反应到thread1的执行过程中,这时候就出现了数据不一致的问题。

3.不同线程使用同一资源,由于windows是抢占式系统,在多线程环境中总免不了会(thread1)被中断CPU在执行其他线程(thread2)之后回来继续执行。若两个线程使用了同一个资源(变量 bool x),在thread1中断时thread1中的x为true线程进入分支语句s1,中断后CPU资源被分配给了thread2,并在thread2中变量x被设置为false。则thread1中断回来之后若在s1中默认x为true而执行的指令就发生错误。

4.若是thread1需要thread2的某个执行结果来作为thread1继续执行下去的条件。则要么不使用多线程在thread1中直接调用thread2的执行函数,thread2若有阻塞(线程挂起)或者多核CPU只能使用单核,造成CPU资源的浪费。或者在thread1中循环等待直到thread2执行完成,循环时不能做其他工作也会造成CPU的浪费。

为了解决因为多线程设计而引入的这些复杂性,我们可以采用如下方法:

1.对于高速缓存的问题,我们需要组织好对象的内存布局还要考虑目标机器的缓存行大小,通常情况下不需要考虑的这么细致,只要知道有这么一回事就好了。

2.volatile限定符来解决编译器直接使用寄存器来保存变量带来的问题,这个限定符会让编译器在生成代码时对变量的取值和复制都去操作内存。

3.使用系统提供的InterLocked系列函数来进行原子操作,保证不会存在覆盖的问题。适用于快速的对变量做修改,这里应当使用了自旋锁的技术只有让CPU一直循环直到变量可以被使用,自旋锁使用的是轮询的方式CPU做着没有意义的while循环等待资源可用,对于预计只需要等到极短的时间(至少小于一个时间片)的资源才会用这样的方式,否则应当将线程挂起,等到资源可用时才由系统将线程设置为可调度的,这样才不会将CPU浪费在无意义的循环等待中。

4.对于一个变量的修改可以使用InterLocked系列函数来保证操作为原子操作,而要将多个语句设为原子操作可以使用关键区CriticalSection。使用关键区需要先一下几个函数:

InitializeCriticalSection //初始化关键区,几乎不会失败,若失败则抛出异常一般不考虑失败的情况EnterCriticalSection //进入关键区,若失败则线程挂起等待,原子保护其他线程不能够执行下边的指令TryEnterCriticalSection //尝试进入关键区,若失败则返回false,线程不挂起可以继续执行其他操作,若成功相当于EnterCriticalSectionLeaveCriticalSection //离开关键区,被关键区保护的指令计数减去1,减到0之后可以被其他线程执行DeleteCriticalSection //释放关键区资源
对于关键区有几点需要说明的是,entry时会有自旋锁等待一小段时间(通常认为关键区中执行的代码也是很快的),线程获得访问权限之后会设置关键区的属性表示只能被当前线程所访问。对于同一个线程是可以反复进入同一个关键区,同样必须调用相同次数的leave函数退出关键区,若不调用leave函数则会在一段时间之后发生异常。

5.Slim读/写锁:关键区只允许一个线程执行关键区包含的代码,是为了这段指令对资源的操作保持原子性。而在许多应用场景中只需要对修改资源的线程保持独占访问,而对读取操作无需确保对资源的独占。故而资源处于独占状态时不可读取不可写入,若处于共享状态时可以读取不能写入。

Void InitializeSRWLock(PSRWLOCK SRWLock);   //初始化Void AcquireSRWLockExclusive(PSRWLOCK SRWLock);  //申请独占访问Void ReleaseSRWLockExclusive(PSRWLOCK SRWLock);  //释放独占访问Void AcquireSRWLockShared(PSRWLOCK SRWLock);  //申请共享访问Void ReleaseSRWLockShared(PSRWLOCK SRWLock);  //释放共享访问
6.条件变量:对于之前的都是明确的使用锁(关键区或者读写锁)对资源进行保护,若需要动态的决定是否需要对资源进行保护则可以使用条件变量和锁配合来实现。并且windows提供了下列函数用于实现这个目的:

Bool SleepConditionVariableCS //条件变量与关键区配合,当条件变量被激发则得到锁,否则阻塞Bool SleepConditionVariableSRW //条件变量与读写锁配合,当条件变量被激发则得到锁,否则阻塞Void WakeConditonVariable //激发条件变量,唤醒一个线程去执行Void WakeAllConditionVariable //激发条件变量,连续唤醒(锁被释放时)所有线程
以上所讨论的都在用户态中实现,故而是无法跨进程进行线程同步的。若要跨进程同步需要使用内核对象,内核对象一般都包含有状态两种触发(signaled)和未触发(unsigned)通过windows设定的规则这两种状态之间的转化可以作为判断内核对象状态的依据。故而内核状态特别适合两个不同线程协作时一个线程等待另外一个线程的工作结果来进行计算的场景(问题2)。为实现协作windows提供等待函数WaitForSingleObject(等待单一内核对象触发)和WaitForMultipleObjects(等待多个对象,其中一个触发或者全部触发根据参数决定)判断内核对象状态若内核对象未触发则将当前线程挂起,直到等待的对象被触发(或超时)才将线程设为等待。等待函数作用于不同的内核对象会产生不同的后果,若对于进程,线程等不可重置的对象,对象状态不变;若事件等可重置的对象会将对象重置为未触发。

7.进程,线程等触发后不可重置的内核对象适用于thread1必须等待thread2执行结束后再继续执行的场景。

8.事件对象,事件对象分为手动重置对象和自动重置对象,自动重置事件只允许一个正在等待该事件的线程会变成可调度状态;手动重置事件触发后正在等待该事件的所有线程都将变成可调度状态。常使用如下函数:

CreateEvent //创建对象SetEvent    //触发对象ResetEvent  //重置对象
9.虽然用到的不多(还不如在窗口中的settimer)但还是要提一下的计时器内核对象,可以设定一个起始时间和间隔时间,反复触发的对象。可以启动一个线程一直等待,也可以设定一个执行函数触发时执行。相关函数如下:

CreateWaitableTimer //创建计时器SetWaitableTimer    //设置时间和间隔CancelWaitableTimer //取消计时器
10.信号量,我们可以通过一个内核对象来管理一组竞争资源,若还有可用资源则对象被触发,若没有可用资源则对象不被触发,具体规则如下:

1:如果当前资源计数大于0,那么信号量处于触发状态(说明有资源可被使用,所有等待线程被调度)。
2:如果当前资源计数等于0,那么信号量处于未触发状态(没有可用资源,所有线程等待)。
3:当前资源计数不会小于0.
4:当前可用资源计数不会大于最大资源计数。

与一般内核对象一样使用创建,打开,释放这几种操作:

CreateSomaphoreOpenSemaphoreReleaseSemaphore
11.互斥量,内核对象版本的关键区。规则如下:


同样也有创建,打开,释放这几个API:

CreateMutexOpenMutexReleaseMutex
互斥量与其他内核对象不同的地方在于线程所有权的概念(也是用这个来实现关键区一样多次重入的功能)。获得一个互斥量之后会设置他的所有者的线程ID,表示这个线程独占,再次重入会对比线程ID若相同则计数加1,若不同则阻塞。同样也需要调用相同次数的release来时期计数减1,减到0之后把线程ID也设置为0触发对象。而线程所有权的存在使得互斥量可以在线程结束的时候被清理掉从而被触发,完美的解决了遗弃问题。

12.其他同步函数

1.异步设备I/O,当异步IO操作完成系统会将对象变成触发状态,线程就可以继续执行。2.WaitForInputIdle函数将自己挂起直到应用程序创建好窗口之后第一次进入idle状态时被触发。3.MsgWaitForMultipleObjects(Ex)函数处理消息的线程等待消息。4.WaitForDebugEvent函数调试器中适用,用于等待调试信号。5.SignalObjectAndWait函数触发一个内核对象并等待另一个内核对象。触发的内核对象只能为以下几种:互斥量、信号量或事件。等待的内核对象可以是内核对象的任意一种。
系统还提供了等待链遍历API来检测死锁,这个有调试器用其实就够了,不用了解太多。

在多线程中遇到同步问题可以参考下表:








阅读全文
0 0