1.为什么线程同步
比如多个线程同时访问一个全局变量,如果是读取则没问题,如果某个线程改变此变量的值,与此同时其他线程读取该变量值时,不能保证读取到的数据是不是经过那个写线程修改过的。
为了确保获取到的数据是经过线程修改过的,就必须在线程写入变量时,禁止其它线程对此变量做任何访问,直到数据写入完成之后再接触对其它线程的访问限制,这就是线程同步。
2.线程同步与互斥
线程互斥:若干线程使用同一共享资源时,同一时刻只允许一个线程访问,别的需要使用该资源的线程必须等待(阻塞),直到占用资源者释放该资源。
线程同步:线程之间具有制约关系,一个线程的执行依赖于另一个线程的消息,当它没有得到另一个线程的消息时应等待,直到消息到达时才被唤醒。
互斥是一种特殊的同步,互斥和同步分别·对应着线程间通信发生的两种情况
互斥对应:当有多个线程访问共享资源而又不使资源破坏时
同步对应:当一个线程需要将某个任务已完成的情况通知另外一个或多个线程时
.3.四种同步:
临界区: 通过对多线程的串行化来访问公共资源的一段代码,速度快,适合控制数据访问。
互斥量: 为协调共同对一个共享资源的单独访问而设计的。
信号量: 为控制一个具有有限数量用户资源而设计。
事 件: 用来通知线程有一些事件已发生,从而启动后继任务的开始。
(1)临界区(Critical section)
每个线程或进程访问临界资源的那段代码称为临界区,所谓临界即一次只允许一个进程或线程访问的共享资源,属于临界区硬件的有打印机,磁带机,软件有消息队列,数组,变量,缓冲区等。
每次只允许一个线程访问临界区,其它线程则挂起。使用临界区时,一般不允许其运行时间过长,只要运行在临界区的线程还没有离开,其他所有进入此临界区的线程都会被挂起而进入等待状态,并在一定程度上影响程序的运行性能。因此使用临界区域的第一个忠告就是不要长时间锁住一份资源。
临界区包含两个操作原语
EnterCriticalSection() // 进入临界区
LeaveCriticalSection() // 离开临界区
EnterCriticalSection()语句执行后代码将进入临界区以后无论发生什么,必须确保与之匹配的 LeaveCriticalSection()都能够被执行到。否则临界区保护的共享资源将永远不会被释放。虽然临界区同步速度很快,但却只能用来同步本进程内的线程,而不可用来同步多个进程中的线程。
(2)互斥量(mutex)
互斥量类似与临界区,只有拥有互斥对象的线程才具有访问资源的权限,由于互斥对象只有一个,则决定了任何情况下共享资源都不会被多个线程所访问。当前占有资源的线程处理完应将互斥对象交出,以便其它线程获得后得以访问资源。
互斥量比临界区复杂,因为互斥量不仅可以可以在同一进程中实现多线程同步,也可在不同进程的不同线程之间实现资源的安全共享。
互斥量包含的几个操作原语
CreateMutex() // 创建一个互斥量
OpenMutex() // 打开一个互斥量
ReleaseMutex() // 释放互斥量
WaitForMultipleObjects() // 等待互斥量对象
(3)信号量(semaphore)
有时被称为信号灯,信号量与前面不同,它允许多个线程同时访问共享资源,同时又会限制同一时刻访问共享资源的最大线程数目。同操作系统中的pv操作
P操作 申请资源:
(1) S减1;
(2) 若S减1后仍大于等于零,则进程继续执行;
(3) 若S减1后小于零,则该进程被阻塞后进入与该信号相对应的队列中,然后转入进程调度。
V操作 释放资源:
(1) S加1;
(2) 若相加结果大于零,则进程继续执行;
(3) 若相加结果小于等于零,则从该信号的等待队列中唤醒一个等待进程,然后再返回原进程继续执行或转入进程调度。
信号量包含的几个操作原语:
CreateSemaphore() // 创建一个信号量
OpenSemaphore() // 打开一个信号量
ReleaseSemaphore() // 释放信号量
WaitForSingleObject() // 等待信号量
二元信号量类似与互斥量,都是同一共享资源只允许一个线程访问。其根本区别在于互斥量用于线程的同步,互斥量用户线程的互斥。互斥量的加锁与解锁必须由同一线程分别对应使用,而信号量可以由一个线程释放,另一个线程得到。
(4)事件(Event)
事件Event实际上是一个内核对象,所谓内核即操作系统进行管理的一块内存,对应用程序是不可见的。内核对象即是在操作系统中进行资源分配和管理的数据结构。内核对象不属于哪个进程,而是属于操作系统的,因此利用事件同步可以是跨进程与跨线程的。(内核对象理解: https://blog.csdn.net/qq_20828983/article/details/61921765)
事件对象是通过通知操作的方式来实现线程同步的,如下为一个利用事件来进行线程同步的例子,看了便一目了然。
https://blog.csdn.net/x_shuck/article/details/52277031
事件有两种状态,激发(有信号状态)和未激发状态(无信号状态)。事件有两种类型:手动重置事件和自动重置事件。手动重置事件设置为激活状态,会唤醒所有等待的线程,而且一直保持为激活状态,直到程序重新把它设置为未激活状态。而自动重置事件设置为激活状态时,会唤醒一个等待中的线程,然后自动恢复未未激活状态,因此用自动重置事件同步两个线程比较理想。
事件可以实现不同进程中的线程同步操作,并且可以方便的实现多个线程的优先比较等待操作,例如写多个WaitForSingleObject来代替WaitForMultipleObjects从而使编程更加灵活。
事件包含的几个操作原语:
CreateEvent() // 创建一个事件
OpenEvent() // 打开一个事件
SetEvent() // 激活事件
WaitForSingleObject() // 等待一个事件
WaitForMultipleObjects() // 等待多个事件
WaitForMultipleObjects 函数原型:
WaitForMultipleObjects(
IN DWORD nCount, // 等待句柄数
IN CONST HANDLE *lpHandles, // 指向句柄数组
IN BOOL bWaitAll, // 是否完全等待标志
IN DWORD dwMilliseconds // 等待时间
)
参数nCount指定了要等待的内核对象的数目,存放这些内核对象的数组由lpHandles来指向。fWaitAll对指定的这nCount个内核对象的两种等待方式进行了指定,为TRUE时当所有对象都被通知时函数才会返回,为FALSE则只要其中任何一个得到通知就可以返回。 dwMilliseconds在这里的作用与在WaitForSingleObject()中的作用是完全一致的。如果等待超时,函数将返回 WAIT_TIMEOUT。
总结
1.互斥量和临界区类似,但是临界区只能用来同步同一进程下的线程,而互斥量可以跨进程实现多线程同步,因此互斥量占用的资源更多。如果只是需要同一进程下的多线程同步,用临界区更快且又节省资源。
2.互斥量,信号量,事件都可以跨进程来进行数据同步操作,对于进程和线程来讲,如果进程和线程在运行状态则为无信号状态,在退出后则为有信号状态,所以可以使用WaitForSingleObject来等待进程和线程退出。
3.互斥量没法允许多个线程对同一资源进行写操作,而信号量和事件可以。
reference
https://blog.csdn.net/everyfriday_shujk/article/details/79755741
https://www.cnblogs.com/aademeng/articles/6904277.html
https://blog.csdn.net/s_lisheng/article/details/74278765