概述
线程同步可以采用多种方式。可以在用户方式下实现,也可以在内核方式下实现。前者的优势在于速度快,因为不用在用户方式和内核方式之间切换,但只能用于同一个进程内的线程之间的同步;后者是使用内核对象的方式,速度虽慢,但可以用于不同进程之间的线程同步。而且后者相对前者方法丰富许多,功能也强大许多。
用户方式下的线程同步
互锁函数组
下列函数可以以原子的方式进行操作(即或者全做,或者全不做,而且做得过程中不会被打断):
InterlockedExchangeAdd:原子方式增加一个变量,可以在参数中提供负值来实现原子减法操作。
InterlockedExchange,InterlockedExchangePointer:实现原子赋值操作,而且返回原始数值。
InterlockedCompareExchange,InterlockedCompareExchangePointer:原子方式比较赋值,即如果目标变量和被比较值相等时,目标变量才会被赋值为原始值。
所有的互锁函数都是跟写变量相关的,没有读变量的互锁函数。因为读变量不会产生同步问题。引申出来,也没有比较两个变量是否相等的互锁函数,因为只是读变量,而不会写变量。所以下文的循环锁中的相等判断就不会产生同步问题。
循环锁
循环锁是利用互锁函数族中的InterlockedExchange来实现的一种线程同步方式。参考下面的代码:
BOOL g_bResourceInUse = FALSE;
void func()
{
//wait to access the resource
while (InterlockedExchange(&g_bResourceInUse, TRUE) == TRUE)
{
Sleep(0);
}
//access the resource
…
//no longer need to access the resource
InterlockedExchange(&g_bResourceInUse, FALSE);
}
说明:
Sleep(0)是告诉系统该线程将释放剩余的时间片,并迫使系统调度另外一个线程。主要是因为下面:
while (InterlockedExchange(&g_bResourceInUse, TRUE) == TRUE)
{
Sleep(0);
}
开始g_bResourceInUse(简称布尔量)为FALSE,如果两个线程都运行这段代码,第一个会比较布尔量和TRUE,因为布尔量是FALSE,所以布尔量被赋值为FALSE,并且返回TRUE,进入关键区;另一个线程则总是返回TRUE,所以循环直到第一个线程退出关键区重新把布尔量赋为FALSE为止。
如果等待时间很短,这种方式是相当快的。比下面的关键代码段都要快,因为发生冲突时关键代码段的等待过程还是通过内核对象来实现的。
关键代码段
Critical sections(关键代码段)是一小块用来处理一份被共享资源的代码,该段代码必须独占的对某些共享资源的访问权。这可以让多行代码以原子方式执行。实施的方式是在程序中加入“进入”或“离开”critical section的操作。如果一个线程进入了critical section,另外一个线程绝对不能进入该critical section。
为实现这种功能MS提供了五个函数:
InitializeCriticalSection:创建critical section,其实是CRITICAL_SECTION类型的变量。它不是内核对象,所以不是返回句柄。它存在于进程的内存空间中。关于使用critical section的线程以及使用计数都保存在该结构中。
DeleteCriticalSection:用完critical section后清除CRITICAL_SECTION结构。
EnterCriticalSection:在进入critical section前必须调用该函数。这样可以保证之后的代码在同一时间内只有一个线程可以进入。它会查看CRITICAL_SECTION结构,从而保证这一点。具体方法是:
1. 如果没有别的线程进入critical section,则进入。并设置critical section为自己线程所访问。
2. 如果线程自己正在访问该critical section,则只是将计数加1,然后进入。
3. 如果别的线程访问critical section,则等待。系统会在别的线程释放资源后更新CRITICAL_SECTION,从而使该唤醒该线程。可以看出,这种系统更改结构然后唤醒线程的方法必然需要内核对象的配合,而且等待意味着该线程必须从用户方式转到内核方式。
LeaveCriticalSection:查看结构中的成员变量。该函数每次计数都要递减1,指明调用线程多少次被赋予对共享资源的访问权。如果计数大于0,则该函数不做其它操作,只是返回;如果等于0,说明该线程释放了资源,所以该函数查看调用EnterCriticalSection中是否有别的线程在等待。如果至少有一个在等待,则更新成员变量,唤醒等待线程。没有等待线程则更新成员变量说明没有线程使用该资源。
一个EnterCriticalSection必须和一个LeaveCriticalSection配合。即对于同一个CRITICAL SECTION可以多次调用Enter,但必须调用同样次数的Leave。Enter只可能使其它请求使用该critical section的线程阻塞,如果本身正在使用该关键区,则只是让计数加1,不会自己阻塞自己。
TryEnterCriticalSection用来判断线程是否能够进入critical section,它马上返回结果。
关键代码段和循环锁的配合
关键代码段在发生线程等待时会转入内核状态,因为状态转换是非常费时的,所以这对于可能迅速唤醒的线程而言比较费时;而循环锁则总是处于可调度状态,对于可能需要很长时间才能获得资源而使用的线程而言则会被多次唤醒而检查到资源仍不可用,如果能将这类线程置为等待状态而不是可调度状态,会更加高效。
所以说,对于马上可以获取资源的线程,使用循环锁是比较高效的;对于长时间之后才能获得资源的线程,使用关键代码段是比较好的。如果自己实现这种策略,可以先调用一定次数的循环锁,如果仍然不能获取资源,则转为使用关键代码段。
不过微软本身在关键代码段函数族中实现了对二者的结合。如果要将循环锁用于关键代码段,可以使用下面函数:
InitializeCriticalSectionAdnSpinCount
它和InitializeCriticalSection类似,但多了一个DWORD类型的参数,用来设置线程等待(需要进入内核状态)之前想要循环锁循环迭代的次数。遗憾的是,该值只有对于多CPU才是有效的,因为MS实现的循环锁不同于自己前面的例子,而是没有调用sleep。这样对于单CPU而言循环锁执行过程中另一个线程是无法释放已经拥有的资源的。所以如果要求高效只能自己在代码中对二者进行结合。
使用内核对象进行线程同步
内核对象
每个内核对象是内核分配的一个内存块,并且只能由内核访问。该内存块是一种数据结构,它的成员负责维护该对象的各种信息。用户程序不能直接在内存中找到这些变量并修改它们。只能通过windows提供的一些接口函数对其进行操作。
每个内核对象都对应一个句柄。进程中有一个句柄表,只是个数据结构的数组。每个结构包含一个指向内核对象的指针、一个访问掩码和一些标志。句柄其实是在该表中的索引,从1开始。但在win2000中是该内核对象的开头在表中字节偏移数。句柄是进程唯一的,而非系统唯一,同一个句柄值在不同进程中是不具有同样含义的。
内核对象通过CreateXXX创建,通过CloseHandle来关闭。
用于同步的内核对象
可以用于同步的内核对象可以处于通知状态和未通知状态。线程可以等待这些对象。如果被等待独享处于已通知状态,则线程变为可调用;如果处于未通知状态,则线程阻塞。
等待函数是WaitForSingleObject和WaitForMultipleObjects。
前者用来等待单一的内核对象。第一个参数指明了对象的句柄,第二个则是等待时间。当被等待对象为未通知状态时,线程阻塞,直到指定的时间结束;当通知时,通过该代码,执行下面的语句。返回值可能是WAIT_OBJECT_0,表示被等待对象变为通知状态;WAIT_TIMEOUT表示超时;WAIT_FAILED表示被等待对象的句柄无效等错误。
后者用来等待多个内核对象。可以设置等待所有对象都变为已通知时才唤醒线程;或者其中任何一个变为已通知就唤醒线程。返回值可能是WAIT_OBJECT_N,N是自然数。在只有一个对象变为已通知状态就返回的情况下,它表示了成为已通知状态的对象的序号。
等待成功后可能会改变对象的属性,这称为成功等待的副作用。无论对等待单个对象还是多个对象,副作用都是在等待成功时一次性执行的。
进程内核对象
当进程正在运行时,进程内核对象处于未通知状态。当进程停止运行时,就处于已通知状态。可以通过等待进程来检查进程是否仍然运行。
无成功等待的副作用。
线程内核对象
当线程正在运行时,线程内核对象处于未通知状态。当线程停止运行时,就处于已通知状态。可以通过等待线程来检查线程是否仍然运行。
无成功等待的副作用。
事件内核对象
包括人工重置的事件和自动重置的事件。
当人工重置事件得到通知时,等待该事件的所有线程成为可调度线程;它没有成功等待副作用。
当自动重置的事件得到通知时,等待该事件的线程中只有一个线程变为可调度线程。其成功等待的副作用是该对象自动重置为未通知状态。
事件内核对象通过CreateEvent创建,初始可以是通知或未通知状态。SetEvent将事件改为已通知状态,ResetEvent将事件设为未通知状态。
当一个线程执行初始化操作,然后通知另一个线程执行剩余的操作时,经常使用人工事件对象。另外如果一个写线程,多个读线程,可以让写线程完成写操作时通过人工事件通知读线程读取数据。
而自动事件对象则可以用于保护资源在同一时间只有一个线程可以访问,因为它保证只有一个线程被激活。
等待定时器内核对象
等待定时器是在某个时间或按规定的时间间隔发出自己的信号通知的内核对象。包括人工重置的定时器和自动重置的定时器。初始必须是未通知状态。
当发出人工重置的定时器信号时,等待该定时器的所有线程变为可调度;无成功等待副作用。
当发出自动重置的定时器信号时,只有一个等待线程变为可调度线程。成功等待副作用是重置对象。
通过CreatWaitableTimer创建,CancelWaitableTimer撤销一个定时器,SetWaitableTimer告诉定时器何时让其变为已通知状态。
这个对象不太常用,所以自己也没有好好看。
信号量内核对象
信号量用来对资源进行计数。它包含两个32位值,一个表示能够使用的最大资源数量,一个表示当前可用的资源数量。
信号量的使用规则如下:
1. 如果当前资源数量大于0,发出信号量信号
2. 如果当前资源数量是0,不发出信号量信号
3. 不允许当前资源数量为负值
4. 当前资源数量不能大于最大信号数量
通过CreateSemaphore创建。ReleaseSemaphore来释放资源,从而使当前资源数量增加。
当调用等待函数时,它会检查信号量的当前资源数量。如果它的值大于0,那么计数器减1,调用线程处于可调度状态。如果当前资源是0,则调用函数的线程进入等待状态。当另一个线程对信号量的当前资源通过ReleaseSemaphore进行递增时,系统会记住该等待线程,并将其变为可调度状态。
当有多个资源共访问时,经常使用信号量内核对象。
其成功等待副作用是当前资源数量减1。
互斥器内核对象
互斥器保证线程拥有对单个资源的互斥访问权。互斥对象类似于关键代码区,但它是一个内核对象。
互斥器不同于其他内核对象,它有一个“线程所有权”的概念。它如果被某个线程等待成功,就属于该线程。
互斥器的使用规则如下:
1. 如果线程ID是0(无效ID),互斥对象不被任何线程拥有,并且发出该互斥对象的通知信号。
2. 如果ID是非0数字,那么一个线程可以拥有互斥对象,并且不发出该互斥对象的通知信号。
3. 互斥器有一个递归计数器。如果线程已经拥有了互斥器,而它再次等待该互斥器,则马上成功返回;而且递归计数器加1。
通过CreateMutex创建。ReleaseMutex用来释放互斥器。如果线程拥有互斥器,则首先把递归计数器减1,如果减到0,则线程释放互斥器,或者说互斥器的所属线程为空。此后其他线程就可以等待得到该互斥器了。但是如果一个线程ReleaseMutex了一个本来不归他所有的互斥器,则不会有任何效果。
互斥器常用于保护由多个线程访问的内核块。互斥器保证了访问内存块的任何线程拥有对该内存块的独占访问。
其成功等待副作用是将所有权赋予线程,并将递归计数器加1。
互斥器和关键代码区的功能是非常相似的,只是一个是用户对象,一个是内核对象。
分享到:
相关推荐
VC下线程同步的三种方法(互斥、事件、临界区)简单的代码,让你更容易理解三种同步方式。
为了解决这个问题,Windows提供了一系列的线程同步机制,其中包括“线程锁”,也就是我们常说的互斥量(Mutex)。 线程锁,或互斥量,是一种同步对象,它允许同一时间只有一个线程访问被保护的资源。当一个线程获得...
本篇将深入探讨在Windows环境下,如何使用VC++6.0进行线程互斥(Mutex)的编程实践。 线程互斥是一种同步机制,确保同一时间只有一个线程可以访问共享资源,以避免数据竞争和不一致的状态。在Windows API中,我们...
windows下threadpool的实现 ( C++ ) 1. Task对于参数的变化参考了loki的typelist的做法,可支持0 - 9个参数的函数对象。 2. task.h使用脚本自动生成(taskGen.py) 3. scheduler.h用于解耦thread_pool和task_thread...
MFC提供了对Windows API线程功能的封装,使得开发者可以更加方便地创建和管理线程。下面我们将深入探讨MFC下线程的使用及相关知识点。 1. **线程创建**: 在MFC中,创建线程主要有两种方式:通过`CWinThread`派生...
总结来说,这份"linux下线程池源代码"是一个实用的并发编程工具,它涉及到了多线程、同步机制和线程池管理等多个核心概念。通过深入研究,开发者不仅可以掌握线程池的使用,还能深化对Linux多线程编程的理解,为实际...
在深入探讨Linux下线程池的C语言实现之前,我们首先需要理解线程池的基本概念以及它在系统设计中的重要性。线程池是一种管理线程的机制,它预先创建一组固定数量的线程,等待任务的到来,从而避免了频繁创建和销毁...
Linux下线程和锁的实验代码 代码演示了在LINUX下创建线程并互斥的访问临界资源
下面我们将深入探讨如何在VC++中创建和管理线程。 首先,你需要包含必要的头文件,如`Windows.h`,它包含了创建线程所需的API定义。然后,定义一个线程函数,这是新线程将要执行的代码。线程函数的原型通常为`DWORD...
本教程将深入探讨如何在Linux下使用线程库,特别是针对TCP并发应用的实践。 线程库的选择: 在Linux环境下,主要的线程库有以下几种: 1. **NPTL (Native POSIX Thread Library)**:这是Linux内核自带的标准线程...
本篇将详细介绍在Linux环境下,如何使用C++进行线程的创建、同步以及销毁等基本操作。 一、线程创建 在C++11及以后的版本中,C++标准库提供了一个名为`std::thread`的类来支持线程操作。创建一个新线程可以使用`std...
本文将深入探讨.NET下的多线程开发,包括线程的创建、同步与通信、线程优先级以及线程安全。 一、线程创建 在.NET中,可以使用`System.Threading.Thread`类来创建新的线程。通过实例化Thread类并传递一个委托(如:...
4. **同步机制**:如互斥量(`std::mutex`)、条件变量(`std::condition_variable`)等,用于保证线程间的正确同步,防止数据竞争。 在实际使用时,用户首先需要定义自己的任务类型,这个类型需要有一个无参构造...
这种方法确保了只有一次实例化,并且避免了不必要的同步开销。 ```cpp class Singleton { private: Singleton() {} ~Singleton() {} static std::mutex mtx; static Singleton* instance; public: static ...
singleton是最常见的设计模式,但是要设计好却是不容易,尤其是多线程的时候,需要考虑线程安全的问题.
本项目结合两者,实现了一个针对`Dubbo`服务提供者(`Provider`)下线的监控系统,并提供了邮件预警功能,确保系统能够及时发现并处理服务异常情况,减少因服务不可用导致的业务中断。 1. **Zookeeper在Dubbo中的作用...
BMDThread控件是一套相当成熟的线程控件,使用它可以让你快速的创建、管理线程。含Demo
这个“易语言恶搞QQ下线”项目,从标题和描述来看,主要是利用易语言编写的一个程序,其功能可能是发送某种指令或消息,使得目标QQ账号被迫下线,这在编程领域被称为一种恶作剧或黑客行为。 首先,我们要明确,这种...