前面博客讲了互斥量(MUTEX)和关键段(CRITICAL SECTION)的使用,想来总觉不妥,就如盲人摸象一般,窥其一脚而言象,难免以偏概全,追加一篇博客查遗补漏。

win32下的线程同步技术分为用户模式下的线程同步和用内核对象进行线程同步两大类。

用户模式下的线程同步和用内核对象进行线程同步有以下的明显差异:

1.用户模式下的线程同步不需要进入操作系统核心,直接在用户模式就可以进行操作。

2.用内核对象进行线程同步需要进入操作系统核心,用户模式切换至核心模式大约花费1000个CPU周期。

3.用户模式下的线程同步只能在同一个进程中使用。

4.用内核对象进行线程同步可以被其他进程使用,因为内核对象的名称是整个操作系统唯一且可见的。

5.用户模式下的线程同步不能指定等待超时时间。

6.用内核对象进行线程同步的获取拥有权/控制权函数可以设定等待超时时间。

7.用户模式下的线程同步对象因进程异常终止而被废弃时用户无法获取相关信息也不返回。

8.用内核对象进行线程同步的获取控制权/拥有权函数在等待的内核对象被废弃时会立马返回,并可以通过返回值和GetLastError获取返回具体原因。

用户模式下的线程同步技术列表及概述:

1.原子访问

指一个线程在访问某一个资源的同时保证没有其他线程会在同一时刻访问同一资源,比如c++;p->next = p->next->next;这样的操作对线程来说是分成好几步进行的,并不是原子操作,而我们对线程共享的全局资源进行操作很有可能因为线程切换出现不可预料的结果,原子访问就是用来保护这些操作一次性完成,不管有多少个cpu、多少个线程,如果这些操作执行那就一定一次性完成。

常用API:

InterLockedExchange;

InterLockedExchangePointer;

InterLockedExchangeAdd;

InterLockedExchangeAdd64;

InterLockedIncrement。

2.关键段

一小段代码,执行之前需要独占对一些共享资源的访问权,这种方式可以让多行代码以“原子方式”来对资源进行操控,在当前线程离开关键段之前,系统不会去调度任何想要访问统一资源的其他线程。

常用API:

InitializeCriticalSection初始化CRITICAL_SECTION结构体,结构体中保存有计数器和是否有线程正在访问,应杜绝手动更新结构体的成员变量;

EnterCriticalSection获取访问权并将计数器+1,如果线程已获取访问权则只将计数器+1,等待超时时间视注册表项而定,默认为30天;

TryEnterCriticalSection尝试获取访问权,其他线程在在访问资源则返回FALSE,否则获取访问权并返回TRUE;

LeaveCriticalSection将计数器-1并检查计数器是否等于0,计数器等于0就更新成员变量表示释放访问权,否则不做额外处理;

DeleteCriticalSection重置CRITICAL_SECTION结构中的成员变量.

3.旋转锁

关键段是win32中很常用的一个线程同步工具,非常方便但来回切换等待状态开销非常大,会浪费巨量的CPU时间,比如线程A尝试进入关键段失败就会切换至等待模式,线程必须切换到内核模式,而在多CPU的处理器上,访问资源的线程B在其他CPU运行并在A切换等待模式完成前就释放资源并离开关键段,这样就造成了巨大的CPU时间浪费。

为了提高关键段的性能,microsoft把旋转锁合并到了关键段,当EnterCriticalSection的时候,会用一把旋转锁不停循环,尝试在一段时间获取对资源的访问权,只有尝试失败时,线程才切换至内核模式并进入等待状态。关键段+旋转锁作为对关键段的补充,我们应该首先使用和入了旋转锁的关键段。

和关键段不同的API:

InitializeCriticalSectionAndSpinCount,初始化关键段同时使用旋转锁,第二个参数指定旋转锁循环的次数,这个值介于0-0x00ffffff,在单CPU的机器上函数会忽略第二个参数,因为毫无意义,关于旋转次数的设置,jeffrey大神建议保护进程堆的关键段所使用的旋转次数是4000.

4.slim读/写锁

SRWLock的目的和关键段相同,对一个资源进行保护,不让其他线程访问它,但是与关键段不同的是,SRWLock允许我们区分那些想要读取资源的值的线程(读取者线程)和想要更新资源的值的线程(写入者线程)。让所有的读取者线程在同一时间访问共享资源应该是可行的,因为并不会破坏数据。只有当写入者线程想要对资源进行更新的时候才需要进行同步。因此,我们需要写入者线程独占资源的访问权,任何其他线程,无论是读取者还是写入者线程都不允许访问资源。

常用API:

InitializeSRWLock,初始化SRWLOCK结构,SRWLOCK在WinBase.h中被定义为RTL_SRWLOCK。

AcquireSRWLockExclusive,(写入者线程)获取被保护资源的独占访问权。

ReleaseSRWLockExclusive,(写入者线程)完成资源更新后,通过ReleaseSRWLockExclusive解除对资源的独占访问权。

AcquireSRWLockShared,(读取者线程)获取被保护资源的共享访问权。

ReleaseSRWLockShared,(读取者线程)完成资源读取后,释放共享访问权。

SRWLock不必手动销毁或删除,由操作系统负责自动清理。

和关键段相比,SRWLock缺乏以下两个特性。

1.不存在TryEnter(Shared/Exclusive)SRWLock之类函数,如果锁已经被占用,那么调用AcquireSRWLock(Shared/Exclusive)会阻塞调用线程。

2.不能递归获得SRWLOCK,一个线程不能为了多次写入资源而多次锁定资源,再多次调用ReleaseSRWLock*来释放资源的锁定。

5.条件变量

条件变量经常和关键段、锁联合使用,用于线程将自己阻塞,直到满足了某个条件才唤醒自己并锁定资源进行工作。

常用API:

InitializeConditionVariable,初始化一个CONDITION_VARIABLE结构体。

SleepConditionVairableCS,三个参数分别为已初始化的CONDITION_VARIABLE、等待的关键段、等待超时时长,等待超时时长可以设置为INFINITE表示一直等下去,当指定的等待时间用完的时候条件变量仍然未被触发就返回FALSE,否则返回TRUE,返回TRUE表示自动EnterCriticalSection。

SleepConditionVariableSRW,四个参数分别为已初始化的CONDITION_VARIABLE、等待的SRW锁、等待超时时长,对SRW锁的访问方式,前三个参数和上面一个函数是基本一样的,最后一个参数写入者线程传入0,表示独占资源的访问;读取者线程应该传入CONDITION_VARIABLE_LOCKMODE_SHARED,表示希望共享对资源的访问。返回结果类似上一个函数。

WakeConditionVariable,使一个SleepConditionVariable*函数中等待同一个条件变量被处罚的线程得到锁或关键段并返回,当这个线程释放同一个锁的时候,不会唤醒其他正在等待同一个条件变量的线程。

WakeAllConditionVariable, 唤醒一个或几个在SleepConditionVariable*函数中等待这个条件变量被触发的线程对资源的访问权并返回,应用于唤醒读取者线程。

PS:条件变量的使用稍微有点绕,不同于关键段或读写锁,条件变量在处理生产者/消费者模型时经常使用到两个条件变量,一个用于生产者,一个用于消费者 ,当生产者线程访问完毕时调用WakeAllConditionVariable唤醒所有消费者线程;消费者线程取完最后一个产品时调用WakeConditionVariable唤醒一个生产者。

PS:如果线程A和线程B都要求获取资源C和D的独占权,最好在A和B的代码中保证资源获取顺序一致。

PS:不要长时间锁定资源,尽可能少的在独占资源处理代码中加入Sleep和一些需要等待对端响应返回的函数。

用内核对象进行线程同步的技术列表及概述: 

1.互斥量内核对象

互斥量内核对象用来确保一个线程对资源的独占访问,互斥量对象包含一个引用计数、线程ID以及一个递归计数。互斥量与关键段的行为完全相同,但是互斥量是内核对象,关键段是用户模式下的同步对象,互斥量将比关键段慢并且互斥量可以被不同进程中的线程访问,互斥量可以指定申请访问权等待时间。

常用API:

CreateMutex,创建一个互斥量对象,参数分别指定安全属性、创建线程是否初始拥有互斥量的访问权、互斥量的名字。

OpenMutex,打开一个已经存在的互斥量对象,通常用于Client端访问Server端的资源时,OpenMutex和获取互斥量访问权没有任何关系,简单表示打开一个互斥量内核对象而已。

WaitForSingleObject,等待一个内核对象被激发并返回,此处即等待获取互斥量的访问权。

WaitForMultipleObjects,等待多个内核对象被激发并返回,此处等待多个互斥量的访问权。

MsgWaitForMultipleObjects,等待消息到来或者多个内核对象被激发,通常用于界面进程,保证等待互斥锁的同时不会影响线程消息处理。

ReleaseMutex,将互斥量对象的递归计数减1,如果线程成功的等待了互斥量对象不止一次,那么线程必须调用ReleaseMutex相同的次数才能使对象的递归计数变为0,递归计数变为0时,函数会将线程ID设为0,互斥量就会保持在触发状态,下一个等待它的线程可以立即得到它。

CloseHandle,将互斥量对象的引用计数 减1,当引用计数为0时对象被删除。

PS:按照Jeferry大神对ReleaseMutex的解释,一个线程WaitForSingleObject等待互斥量数次,那么必须手动调用ReleaseMutex相应的次数才能释放掉资源的访问权,但测试的时候并不是这样的,同时关于ReleaseMutex的描述Jefery大神和MSDN有所出入,MSDN描述:Releases ownership of the specified mutex object.

下面是测试代码:

unsigned int _stdcall ThreadExFunc(void* p)

{

HANDLE hMutex = OpenMutex(SYNCHRONIZE , TRUE, TEXT("foo46"));

if(hMutex == NULL)

printf("Wrong Mutex");

else {

//WaitforsingleObject将等待指定的一个mutex,直至获取到拥有权 //通过互斥锁保证除非输出工作全部完成,否则其他线程无法输出。 WaitForSingleObject(hMutex, 1000);

WaitForSingleObject(hMutex, 1000);

WaitForSingleObject(hMutex, 1000);

WaitForSingleObject(hMutex, 1000);

for(int i = 0; i < 10; i++)

{

printf("%d%d%d%d%d%d%d%d%d%d\n", p, p, p, p, p, p, p, p, p,p);

Sleep(10);

}

ReleaseMutex(hMutex);

}

CloseHandle(hMutex);

return 0; }

这是线程执行代码,在win7+vs2012环境测试,所有的线程都正确打印,这里到底是大神笔误还是我没理解透彻呢,还望有人指点一下。

2.信号量内核对象

信号量内核对象用来对资源进行计数,一个信号量内核对象包括使用计数、最大资源计数、当前资源计数。最大资源计数表示信号量可以控制的最大资源数量,当前资源计数表示信号量当前可用资源的数量。

信号量的规则如下:

1.如果当前资源计数大于0,那么信号量处于触发状态。

2.如果当前资源计数等于0,那么信号量处于未触发状态。

3.系统绝对不会让当前资源计数变为负数。

4.当前资源计数绝对不会大于最大资源计数。

常用API:

CreateSemaphore,创建一个信号量内核对象。

OpenSemaphore,打开一个已经存在的信号量内核对象。

WaitForSingleObject,使用和等待其他内核对象一样,差别在于semaphore并不存在拥有权的说法,对一个semaphore可以重复Wait...以获取多份资源。

WaitForMultipleObjects, 参照WaitForSingleObject介绍。

ReleaseSemaphore,将指定的信号量当前资源计数增加N个,同时获取改变资源计数之前的可用资源计数。

CloseHandle,对每个内核对象都要做的,大家懂。

PS:想要在Semaphore创建后继续做一些初始化工作,比如分配内存等,该怎么办?CreateSemaphore可以指定当前可用的资源计数为0,这样其他的线程就无法获取访问权,等到初始化工作完成调用ReleaseSemaphore将可用资源计数加至最大。

3.事件内核对象

所有的内核对象中,事件比其他对象要简单的多,事件包含一个内核对象都有的引用计数,一个用来表示事件是自动重置事件还是手动重置事件的布尔值,以及另一个用来表示事件有没有被触发的布尔值。

事件的触发表示一个操作已经完成,有两种不同类型的事件对象:手动重置事件和自动重置事件,当一个手动重置事件被触发时,正在等待该事件的所有线程都变成可调度状态;而当一个自动重置事件被触发的时候,只有一个正在等待该事件的线程会变成可调度状态。

事件最通常的用途:让一个线程执行初始化工作,然后再触发另一个线程,让它执行其余的工作。一开始我们将事件初始化为未触发状态,然后当线程完成初始化工作的时候触发事件。此时,另一个线程一直在等待该事件,它发现该事件被触发,于是变成可调度状态,第二个线程知道第一个线程已经完成了它的工作。

常用API:

CreateEvent,创建一个事件内核对象,4个参数分别表示安全属性、手动重置事件(TRUE)或者自动重置事件(FALSE)、触发状态(TRUE表示触发,FALSE表示未触发)、 事件内核对象名称。

OpenEvent,打开一个已经存在的事件内核对象,也可以用CreateEvent指定已经存在的事件名称来打开,此外CreateEventEx可以用减少权限的方式打开事件内核对象。

WaitForSingleObject,通用的内核对象等待方法,成功等待一个自动重置事件时会自动将事件设置为未触发状态,所以自动重置事件通常不须调用ResetEvent。

WaitForMultipleObjects,通用的内核对象等待方法,同上。

SetEvent,把事件变成触发状态。

ResetEvent,把事件变成未触发状态。

PulseEvent, 触发事件并立即将其恢复至未触发状态,这个函数通常不太有用,因为无法确定其他等待事件的线程会收到触发信号。

CloseHandle,内核对象引用计数减1,减至0时摧毁该内核对象。

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

可等待的计时器是这样一种内核对象,它们会在某个指定的时间触发,或每隔一段时间触发一次,听起来很像消息队列中的SetTimer和KillTimer组合,从建立以及触发方式来看应该适用于对时间精度要求不是很高的场合。

常用API:

CreateWaitableTimer,创建一个可等待的计时器,三个参数分别表示安全属性、自动重置计时器还是手动重置计时器、计时器名称,刚创建的可等待计时器总是处于未触发状态。

OpenWaitableTimer,获取一个已经存在的可等待计时器的句柄。

SetWaitableTimer,设置计时器触发时间,最难缠的计时器函数,可以设置首次触发时间以及之后多久触发一次,也可以设置只触发一次,各位网搜一下详细用法吧。

CancelWaitableTimer,取消一个计时器,直到下一次调用SetWaitableTimer才会再次被触发,这个函数不是必须的,每次调用SetWaitableTimer都会在设置新的触发时间之前将原来的触发时间取消。

PS:手动重置计时器被触发时,正在等待该计时器的所有线程都会变成可调度状态,当自动重置计时器被触发时,只有一个正在等待该计时器的线程会变成可调度状态。

小结:终于把纲要做完了,目的只是给各位一个直观的了解,有很多地方讲的太过模糊,有兴趣的朋友可以联系我共同讨论:believing_dan@hotmail.com或者qq:382128698.时间允许的话会对每种技术做一个单章介绍。

【WIN32进阶之路】:线程同步技术纲要的更多相关文章

  1. C#线程同步技术(二) Interlocked 类

    接昨天谈及的线程同步问题,今天介绍一个比较简单的类,Interlocked.它提供了以线程安全的方式递增.递减.交换和读取值的方法. 它的特点是: 1.相对于其他线程同步技术,速度会快很多. 2.只能 ...

  2. iOS开发系列-线程同步技术

    概述 多线程的本质就是CPU轮流随机分配给每条线程时间片资源执行任务,看起来多条线程同时执行任务. 多条线程同时访问同一块资源,比如操作同一个对象.统一变量.同一个文件,就会引发数据错乱和数据安全的问 ...

  3. Delphi 线程同步技术(转)

    上次跟大家分享了线程的标准代码,其实在线程的使用中最重要的是线程的同步问题,如果你在使用线程后,发现你的界面经常被卡死,或者无法显示出来,显示混乱,你的使用的变量值老是不按预想的变化,结果往往出乎意料 ...

  4. Win32多线程编程(3) — 线程同步与通信

      一.线程间数据通信 系统从进程的地址空间中分配内存给线程栈使用.新线程与创建它的线程在相同的进程上下文中运行.因此,新线程可以访问进程内核对象的所有句柄.进程中的所有内存以及同一个进程中其他所有线 ...

  5. Linux/Unix 线程同步技术之互斥量(1)

    众所周知,互斥量(mutex)是同步线程对共享资源访问的技术,用来防止下面这种情况:线程A试图访问某个共享资源时,线程B正在对其进行修改,从而造成资源状态不一致.与之相关的一个术语临界区(critic ...

  6. C#线程同步技术(一) lock 语句

    开篇语: 上班以后,烦恼少了,至少是没有什么好烦的了,只要负责好自己的工作就可以了,因此也有更多的时间去探索自己喜欢的程序.买回来的书已经看了一半,DEMO也敲了不少,昨晚终于在这里开BLOG,记录一 ...

  7. Visual C++线程同步技术剖析:临界区,时间,信号量,互斥量

    使线程同步 在程序中使用多线程时,一般很少有多个线程能在其生命期内进行完全独立的操作.更多的情况是一些线程进行某些处理操作,而其他的线程必须对其处理结果进行了解.正常情况下对这种处理结果的了解应当在其 ...

  8. 【WIN32进阶之路】:内存映射文件

    第一章:源起  遇到一个问题,如果一个客户数据文件有2g大,客户要通过界面查询文件中的数据并用列表控件显示数据,要怎么处理这个文件才能让应用程序不会长时间无响应,客户感觉不到程序的卡顿? 第二章:解决 ...

  9. 转:C# 线程同步技术 Monitor 和Lock

    原文地址:http://www.cnblogs.com/lxblog/archive/2013/03/07/2947182.html 今天我们总结一下 C#线程同步 中的 Monitor 类 和 Lo ...

随机推荐

  1. VS下遇到未能加载文件或程序集 错误

    这个的错误原因可能是在64的系统上编译32位的应用程序,遇到这个错误,可以通过下面的手段解决! 1.关闭Visual Studio. 2. 在Visual Studio Tools子目录,以管理员身份 ...

  2. semantic versioning语义化版本号

    语义化版本号 是由github创始人 Tom Preston-Werner 发起的一个关于软件版本号的命名规范,关于这个规范详细的说明可以在 官网 查看,也可访问其 GitHub项目页面 ,官网文档: ...

  3. 8 Things Every Person Should Do Before 8 A.M.

    https://medium.com/@benjaminhardy/8-things-every-person-should-do-before-8-a-m-cc0233e15c8d 1. Get A ...

  4. eclipse+pydev (python) 配置出错

    错误: eclipse+pydev 配置出错,就是在选择python interpreter那一步: See error log for details.com.sun.org.apache.xerc ...

  5. Eclipse优化

    未特别说明,以下均处理在Window->Preferences下 General列表下 Startup and Shutdown可以去掉一些不必要的启动项 怎样才能知道哪些启动项有用呢?我现在把 ...

  6. JPA详解

    2006 年 5 月 2 日,EJB 3.0 规范最终版由 JCP(Java Community Process) 正式公布,标准号为 JSR(Java Specification Request)2 ...

  7. Android 内存管理分析(四)

    尊重原创作者,转载请注明出处: http://blog.csdn.net/gemmem/article/details/8920039 最近在网上看了不少Android内存管理方面的博文,但是文章大多 ...

  8. 利用Merge Into 更新表,集合数据到数据库中

    使用Merge INTO 将表数据更新到数据库中 创建User-Defined Table Types   创建要更新的UserDetails表 创建更新存储过程 程序调用存储过程 查看结果

  9. oracle 问题若干 提醒注意

    1.Powerdesigner 里生成sql,在oracle中运行时报错:ORA-00907: 缺失右括号 解决:这样的问题很多时候是因为用了不正确的数据类型造成的.比如写作nvarchar(n),但 ...

  10. Spring @Resource、@Autowired、@Qualifier的注解注入及区别

    spring2.5提供了基于注解(Annotation-based)的配置,我们可以通过注解的方式来完成注入依赖.在Java代码中可以使用 @Resource或者@Autowired注解方式来经行注入 ...