- 领 域
- 汇编语言
- 中文名
- 互斥锁
-
2021-06-07 15:37:41
1.引言:
互斥锁,是一种信号量,常用来防止两个进程或线程在同一时刻访问相同的共享资源。可以保证以下三点:
原子性:把一个互斥量锁定为一个原子操作,这意味着操作系统(或pthread函数库)保证了如果一个线程锁定了一个互斥量,没有其他线程在同一时间可以成功锁定这个互斥量。
唯一性:如果一个线程锁定了一个互斥量,在它解除锁定之前,没有其他线程可以锁定这个互斥量。
非繁忙等待:如果一个线程已经锁定了一个互斥量,第二个线程又试图去锁定这个互斥量,则第二个线程将被挂起(不占用任何cpu资源),直到第一个线程解除对这个互斥量的锁定为止,第二个线程则被唤醒并继续执行,同时锁定这个互斥量。
从以上三点,我们看出可以用互斥量来保证对变量(关键的代码段)的排他性访问。
2.函数说明:
需要的头文件:pthread.h1)初始化互斥锁
函数原型:intpthread_mutex_init(pthread_mutex_t *mp, const pthread_mutexattr_t *mattr)
参数说明:mp互斥锁地址mattr属性通常默认null
初始化互斥锁之前,必须将其所在的内存清零。
如果互斥锁已初始化,则它会处于未锁定状态。互斥锁可以位于进程之间共享的内存中或者某个进程的专用内存中。
2)锁定互斥锁
函数原型:
int pthread_mutex_lock(pthread_mutex_t *mutex); #include pthread_mutex_t mutex; int ret; ret = pthread_ mutex_lock(&mp); /* acquire the mutex */
函数说明:
当pthread_mutex_lock()返回时,该互斥锁已被锁定。调用线程是该互斥锁的属主。如果该互斥锁已被另一个线程锁定和拥有,则调用线程将阻塞,直到该互斥锁变为可用为止。
如果互斥锁类型为PTHREAD_MUTEX_NORMAL,则不提供死锁检测。尝试重新锁定互斥锁会导致死锁。如果某个线程尝试解除锁定的互斥锁不是由该线程锁定或未锁定,则将产生不确定的行为。
如果互斥锁类型为PTHREAD_MUTEX_ERRORCHECK,则会提供错误检查。如果某个线程尝试重新锁定的互斥锁已经由该线程锁定,则将返回错误。如果某个线程尝试解除锁定的互斥锁不是由该线程锁定或者未锁定,则将返回错误。
如果互斥锁类型为PTHREAD_MUTEX_RECURSIVE,则该互斥锁会保留锁定计数这一概念。线程首次成功获取互斥锁时,锁定计数会设置为1。线程每重新锁定该互斥锁一次,锁定计数就增加1。线程每解除锁定该互斥锁一次,锁定计数就减小1。锁定计数达到0时,该互斥锁即可供其他线程获取。如果某个线程尝试解除锁定的互斥锁不是由该线程锁定或者未锁定,则将返回错误。
如果互斥锁类型是PTHREAD_MUTEX_DEFAULT,则尝试以递归方式锁定该互斥锁将产生不确定的行为。对于不是由调用线程锁定的互斥锁,如果尝试解除对它的锁定,则会产生不确定的行为。如果尝试解除锁定尚未锁定的互斥锁,则会产生不确定的行为。
返回值:
pthread_mutex_lock()在成功完成之后会返回零。其他任何返回值都表示出现了错误。如果出现以下任一情况,该函数将失败并返回对应的值。
EAGAIN:由于已超出了互斥锁递归锁定的最大次数,因此无法获取该互斥锁。
EDEADLK:当前线程已经拥有互斥锁。
3)解除锁定互斥锁
函数原型:
int pthread_mutex_unlock(pthread_mutex_t *mutex); #include pthread_mutex_t mutex; int ret; ret = pthread_mutex_unlock(&mutex); /* release the mutex */
函数说明:pthread_mutex_unlock()可释放mutex引用的互斥锁对象。互斥锁的释放方式取决于互斥锁的类型属性。如果调用pthread_mutex_unlock()时有多个线程被mutex对象阻塞,则互斥锁变为可用时调度策略可确定获取该互斥锁的线程。对于PTHREAD_MUTEX_RECURSIVE类型的互斥锁,当计数达到零并且调用线程不再对该互斥锁进行任何锁定时,该互斥锁将变为可用。
返回值:pthread_mutex_unlock()在成功完成之后会返回零。
其他任何返回值都表示出现了错误。如果出现以下情况,该函数将失败并返回对应的值。
EPERM :当前线程不拥有互斥锁。
4)使用非阻塞互斥锁锁定
函数原型:
int pthread_mutex_trylock(pthread_mutex_t *mutex); #include pthread_mutex_t mutex; int ret; ret = pthread_mutex_trylock(&mutex); /* try to lock the mutex */
函数说明:pthread_mutex_trylock()是pthread_mutex_lock()的非阻塞版本。如果mutex所引用的互斥对象当前被任何线程(包括当前线程)锁定,则将立即返回该调用。否则,该互斥锁将处于锁定状态,调用线程是其属主。
返回值:pthread_mutex_trylock()在成功完成之后会返回零。其他任何返回值都表示出现了错误。如果出现以下任一情况,该函数将失败并返回对应的值。
EBUSY :
由于mutex所指向的互斥锁已锁定,因此无法获取该互斥锁。
EAGAIN:描述:
由于已超出了mutex的递归锁定最大次数,因此无法获取该互斥锁。
5)销毁互斥锁
函数原型:
int pthread_mutex_destroy(pthread_mutex_t *mp); #include pthread_mutex_t mp; int ret; ret = pthread_mutex_destroy(&mp); /* mutex is destroyed */请注意,没有释放用来存储互斥锁的空间。
返回值:
pthread_mutex_destroy()在成功完成之后会返回零。其他任何返回值都表示出现了错误。如果出现以下任一情况,该函数将失败并返回对应的值。
EINVAL: mp指定的值不会引用已初始化的互斥锁对象。
3.例子:
互斥锁用来保证一段时间内只有一个线程在执行一段代码。必要性显而易见:假设各个线程向同一个文件顺序写入数据,最后得到的结果一定是灾难性的。
我们先看下面一段代码。这是一个读/写程序,它们公用一个缓冲区,并且我们假定一个缓冲区只能保存一条信息。即缓冲区只有两个状态:有信息或没有信息。
void reader_function ( void );
void writer_function ( void );
char buffer;
int buffer_has_item=0;
pthread_mutex_t mutex;
struct timespec delay;
void main ( void ){
pthread_t reader;
/*定义延迟时间*/
delay.tv_sec = 2;
delay.tv_nec = 0;
/*用默认属性初始化一个互斥锁对象*/
pthread_mutex_init (&mutex,NULL);
pthread_create(&reader, pthread_attr_default, (void *)&reader_function), NULL);
writer_function( );
}
void writer_function (void){
while(1){
/*锁定互斥锁*/
pthread_mutex_lock (&mutex);
if (buffer_has_item==0){
buffer=make_new_item( );
buffer_has_item=1;
}
/*打开互斥锁*/
pthread_mutex_unlock(&mutex);
pthread_delay_np(&delay);
}
}
void reader_function(void){
while(1){
pthread_mutex_lock(&mutex);
if(buffer_has_item==1){
consume_item(buffer);
buffer_has_item=0;
}
pthread_mutex_unlock(&mutex);
pthread_delay_np(&delay);
}
}
程序说明:
这里声明了互斥锁变量mutex,结构pthread_mutex_t为不公开的数据类型,其中包含一个系统分配的属性对象。函数pthread_mutex_init用来生成一个互斥锁。NULL参数表明使用默认属性。如果需要声明特定属性的互斥锁,须调用函数pthread_mutexattr_init。函数pthread_mutexattr_setpshared和函数pthread_mutexattr_settype用来设置互斥锁属性。前一个函数设置属性pshared,它有两个取值,PTHREAD_PROCESS_PRIVATE和PTHREAD_PROCESS_SHARED。前者用来不同进程中的线程同步,后者用于同步本进程的不同线程。
在上面的例子中,我们使用的是默认属性PTHREAD_PROCESS_ PRIVATE。后者用来设置互斥锁类型,可选的类型有PTHREAD_MUTEX_NORMAL、PTHREAD_MUTEX_ERRORCHECK、PTHREAD_MUTEX_RECURSIVE和PTHREAD _MUTEX_DEFAULT。它们分别定义了不同的上锁、解锁机制,一般情况下,选用最后一个默认属性。
pthread_mutex_lock声明开始用互斥锁上锁,此后的代码直至调用pthread_mutex_unlock为止,均被上锁,即同一时间只能被一个线程调用执行。当一个线程执行到pthread_mutex_lock处时,如果该锁此时被另一个线程使用,那此线程被阻塞,即程序将等待到另一个线程释放此互斥锁。在上面的例子中,我们使用了pthread_delay_np函数,让线程睡眠一段时间,就是为了防止一个线程始终占据此函数。
4.饥饿和死锁的情形
当一个互斥量已经被别的线程锁定后,另一个线程调用pthread_mutex_lock()函数去锁定它时,会挂起自己的线程等待这个互斥量被解锁。可能出现以下两种情况:
“饥饿状态”:这个互斥量一直没有被解锁,等待锁定它的线程将一直被挂着,即它请求某个资源,但永远得不到它。用户必须在程序中努力避免这种“饥饿”状态出现。Pthread函数库不会自动处理这种情况。
“死锁”:一组线程中的所有线程都在等待被同组中另外一些线程占用的资源,这时,所有线程都因等待互斥量而被挂起,它们中的任何一个都不可能恢复运行,程序无法继续运行下去。这时就产生了死锁。Pthread函数库可以跟踪这种情形,最后一个线程试图调用pthread_mutex_lock()时会失败,并返回类型为EDEADLK的错误。
更多相关内容 -
c语言跨平台互斥锁封装
2021-09-18 21:01:04在做多线程开发时,互斥锁是必不可少的。但c语言不像c++11有标准的线程库,在各种编译器支持的平台都可以使用。而且跨平台开发中,在业务逻辑里写不同平台的兼容代码,容易造成过多的冗余,以及代码结构过于复杂的... -
浅谈互斥锁为什么还要和条件变量配合使用
2021-01-10 16:36:33而条件变量通过允许线程阻塞和等待另一个线程发送信号的方法弥补了互斥锁的不足,它常和互斥锁一起配合使用。使用时,条件变量被用来阻塞一个线程,当条件不满足时,线程往往解开相应的互斥锁并等待条件发生变化。... -
Python多线程操作之互斥锁、递归锁、信号量、事件实例详解
2020-12-20 11:50:00本文实例讲述了Python多线程操作之互斥锁、递归锁、信号量、事件。分享给大家供大家参考,具体如下: 互斥锁: 为什么要有互斥锁:由于多线程是并行的,如果某一线程取出了某一个数据将要进行操作,但它还没有那么... -
自旋锁和互斥锁区别
2020-07-18 05:33:26线程同步(Thread Synchronization)是并行编程中非常重要的通讯手段,其中最典型的应用就是用Pthreads提供的锁机制(lock)来对多个线程之间共 享的临界区(Critical Section)进行保护(另一种常用的同步机制是barrier)。 -
Java使用synchronized实现互斥锁功能示例
2020-08-19 05:05:54主要介绍了Java使用synchronized实现互斥锁功能,结合实例形式分析了Java使用synchronized互斥锁功能简单实现方法与操作技巧,需要的朋友可以参考下 -
redis-mutex:使用Redis的分布式互斥锁
2021-08-04 14:31:15Redis 互斥锁 Ruby 中使用 Redis 的分布式互斥锁。 支持阻塞和非阻塞语义。 这个想法来自。 概要 在下面的例子中,一次只有一个线程/进程/服务器可以进入锁定块。 RedisMutex . with_lock ( :your_lock_name ) ... -
浅析Linux下一个简单的多线程互斥锁的例子
2020-09-15 21:47:52本篇文章是对Linux下一个简单的多线程互斥锁的例子进行了分析介绍,需要的朋友可以参考下 -
详解Golang互斥锁内部实现
2020-09-21 06:37:34本篇文章主要介绍了详解Golang互斥锁内部实现,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧 -
C++ 互斥锁源码
2017-09-01 11:33:02ConsoleApp_Mutex,C++互斥锁源码cpp,可在VC++6.0或VS下直接编译运行,演示结果,控制台程序,ConsoleApp_Mutex,C++互斥锁源码cpp,可在VC++6.0或VS下直接编译运行,演示结果,控制台程序, -
男女皆宜的pthreads:使用pthreads,互斥锁和条件变量解决了中性浴室问题。 用C写
2021-02-14 21:29:38使用pthreads,互斥锁和条件变量解决了中性浴室问题。 用C写 同步:互斥锁 我的代码中使用了互斥锁,以防止不同的线程同时访问代码的关键部分。 它们在代码中的目的是避免互斥问题,因为该程序是同时发生的多线程。 ... -
互斥锁+共享内存封装库,实现进程间通讯(Linux)
2022-05-04 18:16:06Linux下提供了多种共享内存的通讯机制,常用...1:使用进程间的互斥锁,实现了共享内存共享数据的互斥访问问题 2:有效数据部分,通过分配器来管理内存的开辟和释放 3:实现了一对多的模型(一端发送,多端接收的模型) -
简单的JavaScript互斥锁分享
2020-10-26 08:25:50主要介绍了简单的JavaScript互斥锁的相关资料,需要的朋友可以参考下 -
Java互斥锁简单实例
2020-09-03 13:27:19主要介绍了Java互斥锁,较为详细的分析了java互斥锁的概念与功能,并实例描述了java互斥锁的原理与使用技巧,具有一定参考借鉴价值,需要的朋友可以参考下 -
python互斥锁、加锁、同步机制、异步通信知识总结
2021-01-20 04:51:18互斥锁保证了每次只有一个线程进入写入操作,从而保证了多线程情况下数据的正确性。 采用f_flag的方法效率低 创建锁 mutex=threading.Lock() 锁定 mutex.acquire([blocking])#里面可以加blocking(等待的时间)或者... -
多线程freertos互斥锁
2021-10-03 15:30:16app可快速进行应用无需烦锁的创建 -
Linux C 多线程编程之互斥锁与条件变量实例详解
2020-07-14 18:16:49一、互斥锁 互斥量从本质上说就是一把锁, 提供对共享资源的保护访问。 1. 初始化: 在Linux下, 线程的互斥量数据类型是pthread_mutex_t. 在使用前, 要对它进行初始化: 对于静态分配的互斥量, 可以把它设置为... -
互斥锁、自旋锁、读写锁和文件锁
2022-02-28 15:51:06互斥锁、自旋锁、读写锁和文件锁互斥锁自旋锁自旋锁与互斥锁之间的区别读写锁文件锁乐观锁与悲观锁举个例子服务端是如何解决这种冲突的 互斥锁 互斥锁(mutex)又叫互斥量,从本质上说是一把锁,在访问共享资源之前...互斥锁、自旋锁、读写锁和文件锁
互斥锁
互斥锁(mutex)又叫互斥量,从本质上说是一把锁,在访问共享资源之前对互斥锁进行上锁,在访问完成后释放互斥锁(解锁);对互斥锁进行上锁之后,任何其它试图再次对互斥锁进行加锁的线程都会被阻塞,直到当前线程释放互斥锁。如果释放互斥锁时有一个以上的线程阻塞,那么这些阻塞的线程会被唤醒,它们都会尝试对互斥锁进行加锁,当有一个线程成功对互斥锁上锁之后,其它线程就不能再次上锁了,只能再次陷入阻塞,等待下一次解锁。
在我们的程序设计当中,只有将所有线程访问共享资源都设计成相同的数据访问规则,互斥锁才能正常工作。如果允许其中的某个线程在没有得到锁的情况下也可以访问共享资源,那么即使其它的线程在使用共享资源前都申请锁,也还是会出现数据不一致的问题。互斥锁使用 pthread_mutex_t 数据类型表示,在使用互斥锁之前,必须首先对它进行初始化操作。
自旋锁
自旋锁与互斥锁很相似,从本质上说也是一把锁,在访问共享资源之前对自旋锁进行上锁,在访问完成后释放自旋锁(解锁);事实上,从实现方式上来说,互斥锁是基于自旋锁来实现的,所以自旋锁相较于互斥锁更加底层。
如果在获取自旋锁时,自旋锁处于未锁定状态,那么将立即获得锁(对自旋锁上锁);如果在获取自旋锁时,自旋锁已经处于锁定状态了,那么获取锁操作将会在原地“自旋”,直到该自旋锁的持有者释放了锁。由此介绍可知,自旋锁与互斥锁相似,但是互斥锁在无法获取到锁时会让线程陷入阻塞等待状态;而自旋锁在无法获取到锁时,将会在原地“自旋”等待。“自旋”其实就是调用者一直在循环查看该自旋锁的持有者是否已经释放了锁,“自旋”一词因此得名。
自旋锁的不足之处在于:自旋锁一直占用的 CPU,它在未获得锁的情况下,一直处于运行状态(自旋),所以占着 CPU,如果不能在很短的时间内获取锁,这无疑会使 CPU 效率降低。
试图对同一自旋锁加锁两次必然会导致死锁,而试图对同一互斥锁加锁两次不一定会导致死锁,原因在于互斥锁有不同的类型,当设置为PTHREAD_MUTEX_ERRORCHECK 类型时,会进行错误检查,第二次加锁会返回错误,所以不会进入死锁状态。
因此我们要谨慎使用自旋锁,自旋锁通常用于以下情况:需要保护的代码段执行时间很短,这样就会使得持有锁的线程会很快释放锁,而“自旋”等待的线程也只需等待很短的时间;在这种情况下就比较适合使用自旋锁,效率高!
自旋锁与互斥锁之间的区别
- 实现方式上的区别:互斥锁是基于自旋锁而实现的,所以自旋锁相较于互斥锁更加底层;
- 开销上的区别:获取不到互斥锁会陷入阻塞状态(休眠),直到获取到锁时被唤醒;而获取不到自旋锁会在原地“自旋”,直到获取到锁;休眠与唤醒开销是很大的,所以互斥锁的开销要远高于自旋锁、自旋锁的效率远高于互斥锁;但如果长时间的“自旋”等待,会使得 CPU 使用效率降低,故自旋锁不适用于等待时间比较长的情况。
- 使用场景的区别:自旋锁在用户态应用程序中使用的比较少,通常在内核代码中使用比较多;因为自旋锁可以在中断服务函数中使用,而互斥锁则不行,在执行中断服务函数时要求不能休眠、不能被抢占(内核中使用自旋锁会自动禁止抢占),一旦休眠意味着执行中断服务函数时主动交出了CPU使用权,休眠结束时无法返回到中断服务函数中,这样就会导致死锁!
- 最底层的两种就是「互斥锁和自旋锁」,有很多高级的锁都是基于它们实现,可以认为它们是各种锁的地基,所以得清楚它俩之间的区别和应用。加锁的目的是保证共享资源在任意时间里,只有一个线程访问,这样就可以避免多线程导致共享数据错乱的问题。当已经有一个线程加锁后,其他线程加锁则会失败,互斥锁和自旋锁对于加锁失败后的处理方式是不一样的。
读写锁
互斥锁或自旋锁要么是加锁状态、要么是不加锁状态,而且一次只有一个线程可以对其加锁。读写锁有3 种状态:读模式下的加锁状态(以下简称读加锁状态)、写模式下的加锁状态(以下简称写加锁状态)和不加锁状态(见),一次只有一个线程可以占有写模式的读写锁,但是可以有多个线程同时占有读模式的读写锁。因此可知,读写锁比互斥锁具有更高的并行性!
读写锁有如下两个规则:- 当读写锁处于写加锁状态时,在这个锁被解锁之前,所有试图对这个锁进行加锁操作(不管是以读模式加锁还是以写模式加锁)的线程都会被阻塞。
- 当读写锁处于读加锁状态时,所有试图以读模式对它进行加锁的线程都可以加锁成功;但是任何以写模式对它进行加锁的线程都会被阻塞,直到所有持有读模式锁的线程释放它们的锁为止。
虽然各操作系统对读写锁的实现各不相同,但当读写锁处于读模式加锁状态,而这时有一个线程试图以写模式获取锁时,该线程会被阻塞;而如果另一线程以读模式获取锁,则会成功获取到锁,对共享资源进行读操作。
所以,读写锁非常适合于对共享数据读的次数远大于写的次数的情况。当读写锁处于写模式加锁状态时,它所保护的数据可以被安全的修改,因为一次只有一个线程可以在写模式下拥有这个锁;当读写锁处于读模式加锁状态时,它所保护的数据就可以被多个获取读模式锁的线程读取。所以在应用程序当中,使用读写锁实现线程同步,当线程需要对共享数据进行读操作时,需要先获取读模式锁(对读模式锁进行加锁),当读取操作完成之后再释放读模式锁(对读模式锁进行解锁);当线程需要对共享数据进行写操作时,需要先获取到写模式锁,当写操作完成之后再释放写模式锁。
读写锁也叫做共享互斥锁。当读写锁是读模式锁住时,就可以说成是共享模式锁住。当它是写模式锁住时,就可以说成是互斥模式锁住。
文件锁
现象一下,当两个人同时编辑磁盘中同一份文件时,其后果将会如何呢?在 Linux 系统中,该文件的最后状态通常取决于写该文件的最后一个进程。多个进程同时操作同一文件,很容易导致文件中的数据发生混乱,因为多个进程对文件进行 I/O 操作时,容易产生竞争状态、导致文件中的内容与预想的不一致!
对于有些应用程序,进程有时需要确保只有它自己能够对某一文件进行 I/O 操作,在这段时间内不允许其它进程对该文件进行 I/O 操作。为了向进程提供这种功能,Linux 系统提供了文件锁机制。
前面学习过互斥锁、自旋锁以及读写锁,文件锁与这些锁一样,都是内核提供的锁机制,锁机制实现用于对共享资源的访问进行保护;只不过互斥锁、自旋锁、读写锁与文件锁的应用场景不一样,互斥锁、自旋锁、读写锁主要用在多线程环境下,对共享资源的访问进行保护,做到线程同步。
而文件锁,顾名思义是一种应用于文件的锁机制,当多个进程同时操作同一文件时,我们怎么保证文件数据的正确性,linux 通常采用的方法是对文件上锁,来避免多个进程同时操作同一文件时产生竞争状态。譬如进程对文件进行 I/O 操作时,首先对文件进行上锁,将其锁住,然后再进行读写操作;只要进程没有对文件进行解锁,那么其它的进程将无法对其进行操作;这样就可以保证,文件被锁住期间,只有它(该进程)可以对其进行读写操作。
一个文件既然可以被多个进程同时操作,那说明文件必然是一种共享资源,所以由此可知,归根结底,文件锁也是一种用于对共享资源的访问进行保护的机制,通过对文件上锁,来避免访问共享资源产生竞争状态
乐观锁与悲观锁
前面提到的互斥锁、自旋锁、读写锁,都是属于悲观锁。悲观锁做事比较悲观,它认为多线程同时修改共享资源的概率比较高,于是很容易出现冲突,所以访问共享资源前,先要上锁。
相反的,如果多线程同时修改共享资源的概率比较低,就可以采用乐观锁。乐观锁做事比较乐观,它假定冲突的概率很低,它的工作方式是:先修改完共享资源,再验证这段时间内有没有发生冲突,如果没有其他线程在修改资源,那么操作完成,如果发现有其他线程已经修改过这个资源,就放弃本次操作。
放弃后如何重试,这跟业务场景息息相关,虽然重试的成本很高,但是冲突的概率足够低的话,还是可以接受的。可见,乐观锁的心态是,不管三七二十一,先改了资源再说。另外,你会发现乐观锁全程并没有加锁,所以它也叫无锁编程。
举个例子
在线文档是可以同时多人编辑,如果使用了悲观锁,只要有一个用户正在编辑文档,此时其他用户就无法打开相同的文档,用户体验当然不好。
实现多人同时编辑,实际上是用了乐观锁,它允许多个用户打开同一个文档进行编辑,编辑完提交之后才验证修改的内容是否有冲突。
怎么样才算发生冲突?这里举个例子,比如用户 A 先在浏览器编辑文档,之后用户 B 在浏览器也打开了相同的文档进行编辑,但是用户 B 比用户 A 提交改动,这一过程用户 A 是不知道的,当 A 提交修改完的内容时,那么 A 和 B 之间并行修改的地方就会发生冲突。
服务端是如何解决这种冲突的
由于发生冲突的概率比较低,所以先让用户编辑文档,但是浏览器在下载文档时会记录下服务端返回的文档版本号;当用户提交修改时,发给服务端的请求会带上原始文档版本号,服务器收到后将它与当前版本号进行比较,如果版本号一致则修改成功,否则提交失败。
实际上,常见的 SVN 和 Git 也是用了乐观锁的思想,先让用户编辑代码,然后提交的时候,通过版本号来判断是否产生了冲突,发生了冲突的地方,需要我们自己修改后,再重新提交。
乐观锁虽然去除了加锁解锁的操作,但一旦发生冲突,重试的成本非常高,所以只有在冲突概率非常低,且加锁成本非常高的场景时,才考虑使用乐观锁。
总结
互斥锁、自旋锁和读写锁用于解决多线程同步的问题
文件锁适用于多个进程同时操作同一文件,这时很容易导致文件中的数据发生混乱
-
C#多线程中如何运用互斥锁Mutex
2020-08-30 04:47:14主要为大家详细介绍了C#多线程中如何运用互斥锁Mutex,具有一定的参考价值,感兴趣的小伙伴们可以参考一下 -
linux无亲缘关系间进程同步通信实现(互斥锁+条件变量+共享内存模式)
2021-04-03 21:49:19linux无亲缘关系间进程同步通信实现(互斥锁+条件变量+共享内存模式) -
python并发编程多进程 互斥锁原理解析
2020-09-18 18:58:25主要介绍了python并发编程多进程 互斥锁原理解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 -
Python的互斥锁与信号量详解
2020-09-18 16:16:59主要介绍了Python的互斥锁与信号量详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 -
C++ 线程同步之互斥锁
2022-06-19 15:10:59解决多线程数据混乱的方案就是进行线程同步,最常用的就是互斥锁,在 C++11 中一共提供了四种互斥锁:不论是在 C 还是 C++ 中,进行线程同步的处理流程基本上是一致的,C++ 的 mutex 类提供了相关的 API 函数:成员...1.简介
进行多线程编程,如果多个线程需要对同一块内存进行操作,比如:同时读、同时写、同时读写,对于后两种情况来说,如果不做任何的人为干涉就会出现各种各样的错误数据。
- 这是因为线程在运行的时候需要先得到 CPU 时间片,时间片用完之后需要放弃已获得的 CPU 资源,就这样线程频繁地在就绪态和运行态之间切换,更复杂一点还可以在就绪态、运行态、挂起态之间切换,这样就会导致线程的执行顺序并不是有序的,而是随机的混乱的,就如同下图中的这个例子一样,理想很丰满现实却很残酷。
解决多线程数据混乱的方案就是进行线程同步,最常用的就是互斥锁,在 C++11 中一共提供了四种互斥锁:
- std::mutex:独占的互斥锁,不能递归使用
- std::timed_mutex:带超时的独占互斥锁,不能递归使用
- std::recursive_mutex:递归互斥锁,不带超时功能—不建议使用
- std::recursive_timed_mutex:带超时的递归互斥锁—不建议使用
互斥锁在有些资料中也被称之为互斥量,二者是一个东西。
2.std::mutex
不论是在 C 还是 C++ 中,进行线程同步的处理流程基本上是一致的,C++ 的 mutex 类提供了相关的 API 函数:
成员函数
- lock() 函数用于给临界区加锁,并且只能有一个线程获得锁的所有权,它有阻塞线程的作用,函数原型如下:
void lock();
- 独占互斥锁对象有两种状态:锁定和未锁定。
如果互斥锁是打开的,调用 lock() 函数的线程会得到互斥锁的所有权,并将其上锁,其它线程在调用该函数的时候由于得不到互斥锁的所有权,就会被 lock() 函数阻塞。
当拥有互斥锁所有权的线程将互斥锁解锁,此时被 lock() 阻塞的线程解除阻塞,抢到互斥锁所有权的线程加锁并继续运行,没抢到互斥锁所有权的线程继续阻塞。
- 除了使用 lock() 还可以使用 try_lock() 获取互斥锁的所有权并对互斥锁加锁,函数原型如下:
bool try_lock();
二者的区别在于 try_lock() 不会阻塞线程,lock() 会阻塞线程:
- 如果互斥锁是未锁定状态,得到了互斥锁所有权并加锁成功,函数返回 true
- 如果互斥锁是锁定状态,无法得到互斥锁所有权加锁失败,函数返回 false
当互斥锁被锁定之后可以通过 unlock() 进行解锁,但是需要注意的是只有拥有互斥锁所有权的线程也就是对互斥锁上锁的线程才能将其解锁,其它线程是没有权限做这件事情的。
- 该函数的函数原型如下:
void unlock();
通过介绍以上三个函数,使用互斥锁进行线程同步的大致思路差不多就能搞清楚了,主要分为以下几步:
- (1)找到多个线程操作的共享资源(全局变量、堆内存、类成员变量等),也可以称之为临界资源
- (2)找到和共享资源有关的上下文代码,也就是临界区(下图中的黄色代码部分)
- (3)在临界区的上边调用互斥锁类的 lock() 方法
- (4)在临界区的下边调用互斥锁的 unlock() 方法
线程同步的目的是让多线程按照顺序依次执行临界区代码,这样做线程对共享资源的访问就从并行访问变为了串行访问,访问效率降低了,但是保证了数据的正确性。
- 当线程对互斥锁对象加锁,并且执行完临界区代码之后,一定要使用这个线程对互斥锁解锁,否则最终会造成线程的死锁。
- 死锁之后当前应用程序中的所有线程都会被阻塞,并且阻塞无法解除,应用程序也无法继续运行。
3.线程同步
- eg:我们让两个线程共同操作同一个全局变量,二者交替数数,将数值存储到这个全局变量里边并打印出来
#include <iostream> #include <chrono> #include <thread> #include <mutex> using namespace std; int g_num = 0; // 为 g_num_mutex 所保护 mutex g_num_mutex; void slow_increment(int id) { for (int i = 0; i < 3; ++i) { g_num_mutex.lock(); ++g_num; cout << id << " => " << g_num << endl; g_num_mutex.unlock(); this_thread::sleep_for(chrono::seconds(1)); } } int main() { thread t1(slow_increment, 0); thread t2(slow_increment, 1); t1.join(); t2.join(); }
- 测试:
- 解释:
两个子线程执行的任务的一样的(其实也可以不一样,不同的任务中也可以对共享资源进行读写操作),在任务函数中把与全局变量相关的代码加了锁,两个线程只能顺序访问这部分代码(如果不进行线程同步打印出的数据是混乱且无序的)。
在所有线程的任务函数执行完毕之前,互斥锁对象是不能被析构的,一定要在程序中保证这个对象的可用性。
互斥锁的个数和共享资源的个数相等,也就是说每一个共享资源都应该对应一个互斥锁对象。互斥锁对象的个数和线程的个数没有关系。
4.std::lock_guard
lock_guard 是 C++11 新增的一个模板类,使用这个类,可以简化互斥锁 lock() 和 unlock() 的写法,同时也更安全。
- 这个模板类的定义和常用的构造函数原型如下:
// 类的定义,定义于头文件 <mutex> template< class Mutex > class lock_guard; // 常用构造函数 explicit lock_guard( mutex_type& m );
lock_guard 在使用上面提供的这个构造函数构造对象时,会自动锁定互斥量,而在退出作用域后进行析构时就会自动解锁,从而保证了互斥量的正确操作,避免忘记 unlock() 操作而导致线程死锁。
-
lock_guard 使用了 RAII 技术,就是在类构造函数中分配资源,在析构函数中释放资源,保证资源出了作用域就释放。
-
eg:使用 lock_guard 对上面的例子进行修改,代码如下
void slow_increment(int id) { for (int i = 0; i < 3; ++i) { // 使用哨兵锁管理互斥锁 lock_guard<mutex> lock(g_num_mutex); ++g_num; cout << id << " => " << g_num << endl; this_thread::sleep_for(chrono::seconds(1)); } }
- 说明:
通过修改发现代码被精简了,而且不用担心因为忘记解锁而造成程序的死锁,但是这种方式也有弊端,在上面的示例程序中整个for循环的体都被当做了临界区,多个线程是线性的执行临界区代码的,因此临界区越大程序效率越低, 还是需要根据实际情况选择最优的解决方案。
5.std::recursive_mutex-少用
递归互斥锁 std::recursive_mutex 允许同一线程多次获得互斥锁,可以用来解决同一线程需要多次获取互斥量时死锁的问题
- eg:在下面的例子中使用独占非递归互斥量会发生死锁
#include <iostream> #include <thread> #include <mutex> using namespace std; struct Calculate { Calculate() : m_i(6) {} void mul(int x) { lock_guard<mutex> locker(m_mutex); m_i *= x; } void div(int x) { lock_guard<mutex> locker(m_mutex); m_i /= x; } void both(int x, int y) { lock_guard<mutex> locker(m_mutex); mul(x); div(y); } int m_i; mutex m_mutex; }; int main() { Calculate cal; cal.both(6, 3); return 0; }
- 测试:
- 解释:上面的程序中执行了 cal.both(6, 3); 调用之后,程序就会发生死锁,在 both() 中已经对互斥锁加锁了,继续调用 mult() 函数,已经得到互斥锁所有权的线程再次获取这个互斥锁的所有权就会造成死锁(在 C++ 中程序会异常退出,使用 C 库函数会导致这个互斥锁永远无法被解锁,最终阻塞所有的线程)。
要解决这个死锁的问题,一个简单的办法就是使用递归互斥锁 std::recursive_mutex,它允许一个线程多次获得互斥锁的所有权。
-
修改之后的代码如下:
-
测试:
虽然递归互斥锁可以解决同一个互斥锁频繁获取互斥锁资源的问题,但是还是建议少用,主要原因如下:
- 使用递归互斥锁的场景往往都是可以简化的,使用递归互斥锁很容易放纵复杂逻辑的产生,从而导致bug的产生
- 递归互斥锁比非递归互斥锁效率要低一些。
- 递归互斥锁虽然允许同一个线程多次获得同一个互斥锁的所有权,但最大次数并未具体说明,一旦超过一定的次数,就会抛出std::system错误。
6.std::timed_mutex
std::timed_mutex 是超时独占互斥锁, 主要是在获取互斥锁资源时增加了超时等待功能,因为不知道获取锁资源需要等待多长时间,为了保证不一直等待下去,设置了一个超时时长,超时后线程就可以解除阻塞去做其他事情了。
- std::timed_mutex 比 std::_mutex 多了两个成员函数:try_lock_for() 和 try_lock_until():
void lock(); bool try_lock(); void unlock(); // std::timed_mutex比std::_mutex多出的两个成员函数 template <class Rep, class Period> bool try_lock_for (const chrono::duration<Rep,Period>& rel_time); template <class Clock, class Duration> bool try_lock_until (const chrono::time_point<Clock,Duration>& abs_time);
- try_lock_for 函数是当线程获取不到互斥锁资源的时候,让线程阻塞一定的时间长度
- try_lock_until 函数是当线程获取不到互斥锁资源的时候,让线程阻塞到某一个指定的时间点
- 关于两个函数的返回值:当得到互斥锁的所有权之后,函数会马上解除阻塞,返回 true,如果阻塞的时长用完或者到达指定的时间点之后(超时),函数也会解除阻塞,返回 false
- eg:std::timed_mutex 的使用:
#include <iostream> #include <thread> #include <mutex> using namespace std; timed_mutex g_mutex; void work() { chrono::seconds timeout(1); while (true) { // 通过阻塞一定的时长来争取得到互斥锁所有权 if (g_mutex.try_lock_for(timeout)) { cout << "当前线程ID: " << this_thread::get_id() << ", 得到互斥锁所有权..." << endl; // 模拟处理任务用了一定的时长 this_thread::sleep_for(chrono::seconds(10)); // 互斥锁解锁 g_mutex.unlock(); break; } else { cout << "当前线程ID: " << this_thread::get_id() << ", 没有得到互斥锁所有权..." << endl; // 模拟处理其他任务用了一定的时长 this_thread::sleep_for(chrono::milliseconds(50)); } } } int main() { thread t1(work); thread t2(work); t1.join(); t2.join(); return 0; }
- 测试:
- 解释:通过一个 while 循环不停的去获取超时互斥锁的所有权,如果得不到就阻塞 1 秒钟,1 秒之后如果还是得不到阻塞 50 毫秒,然后再次继续尝试,直到获得互斥锁的所有权,跳出循环体。
关于递归超时互斥锁 std::recursive_timed_mutex 的使用方式和 std::timed_mutex 是一样的,只不过它可以允许一个线程多次获得互斥锁所有权,而 std::timed_mutex 只允许线程获取一次互斥锁所有权。
-
另外,递归超时互斥锁 std::recursive_timed_mutex 也拥有和 std::recursive_mutex 一样的弊端,不建议频繁使用。
-
参考:C++ 线程同步之互斥锁
- 这是因为线程在运行的时候需要先得到 CPU 时间片,时间片用完之后需要放弃已获得的 CPU 资源,就这样线程频繁地在就绪态和运行态之间切换,更复杂一点还可以在就绪态、运行态、挂起态之间切换,这样就会导致线程的执行顺序并不是有序的,而是随机的混乱的,就如同下图中的这个例子一样,理想很丰满现实却很残酷。
-
Python实现的多线程同步与互斥锁功能示例
2020-09-21 00:53:11主要介绍了Python实现的多线程同步与互斥锁功能,涉及Python多线程及锁机制相关操作技巧,需要的朋友可以参考下 -
一种基于语法扩展的自动互斥锁实现机制 (2008年)
2021-04-24 15:34:50针对多道程序开发过程中存在的互斥锁标准不统一、使用复杂、易造成死锁等不足,提出扩展C/C++标准语法,增加临界资源定义及操作关键字的方案,通过隐藏互斥锁的概念及实现从而降低开发复杂度、实现平台无关.... -
对python多线程中互斥锁Threading.Lock的简单应用详解
2020-09-19 17:54:36今天小编就为大家分享一篇对python多线程中互斥锁Threading.Lock的简单应用详解,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧 -
Go互斥锁(Mutex)
2022-03-21 21:36:25其中 state 表示当前互斥锁的状态,而 sema 是用于控制锁状态的信号量。 type Mutex struct { state int32 sema uint32 } 上述两个加起来只占 8 字节空间的结构体表示了 Go 语言中的互斥锁。 状态 互斥锁的状态...Mutex
Go 语言的
sync.Mutex
由两个字段state
和sema
组成。其中state
表示当前互斥锁的状态,而sema
是用于控制锁状态的信号量。type Mutex struct { state int32 sema uint32 }
上述两个加起来只占 8 字节空间的结构体表示了 Go 语言中的互斥锁。
状态
互斥锁的状态比较复杂,如下图所示,最低三位分别表示
mutexLocked
、mutexWoken
和mutexStarving
,剩下的位置用来表示当前有多少个 Goroutine 在等待互斥锁的释放:互斥锁的状态
在默认情况下,互斥锁的所有状态位都是 0,
int32
中的不同位分别表示了不同的状态:mutexLocked
— 表示互斥锁的锁定状态;mutexWoken
— 表示从正常模式被从唤醒;mutexStarving
— 当前的互斥锁进入饥饿状态;waitersCount
— 当前互斥锁上等待的 Goroutine 个数;
正常模式和饥饿模式
sync.Mutex
有两种模式 — 正常模式和饥饿模式。我们需要在这里先了解正常模式和饥饿模式都是什么以及它们有什么样的关系。在正常模式下,锁的等待者会按照先进先出的顺序获取锁。但是刚被唤起的 Goroutine 与新创建的 Goroutine 竞争时,大概率会获取不到锁,为了减少这种情况的出现,一旦 Goroutine 超过 1ms 没有获取到锁,它就会将当前互斥锁切换饥饿模式,防止部分 Goroutine 被『饿死』。
互斥锁的正常模式与饥饿模式
饥饿模式是在 Go 语言在 1.9 中通过提交 sync: make Mutex more fair 引入的优化1,引入的目的是保证互斥锁的公平性。
在饥饿模式中,互斥锁会直接交给等待队列最前面的 Goroutine。新的 Goroutine 在该状态下不能获取锁、也不会进入自旋状态,它们只会在队列的末尾等待。如果一个 Goroutine 获得了互斥锁并且它在队列的末尾或者它等待的时间少于 1ms,那么当前的互斥锁就会切换回正常模式。
与饥饿模式相比,正常模式下的互斥锁能够提供更好地性能,饥饿模式的能避免 Goroutine 由于陷入等待无法获取锁而造成的高尾延时。
加锁和解锁
我们在这一节中将分别介绍互斥锁的加锁和解锁过程,它们分别使用
sync.Mutex.Lock
和sync.Mutex.Unlock
方法。互斥锁的加锁是靠
sync.Mutex.Lock
完成的,最新的 Go 语言源代码中已经将sync.Mutex.Lock
方法进行了简化,方法的主干只保留最常见、简单的情况 — 当锁的状态是 0 时,将mutexLocked
位置成 1:func (m *Mutex) Lock() { if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) { return } m.lockSlow() }
如果互斥锁的状态不是 0 时就会调用
sync.Mutex.lockSlow
尝试通过自旋(Spinnig)等方式等待锁的释放,该方法的主体是一个非常大 for 循环,这里将它分成几个部分介绍获取锁的过程:- 判断当前 Goroutine 能否进入自旋;
- 通过自旋等待互斥锁的释放;
- 计算互斥锁的最新状态;
- 更新互斥锁的状态并获取锁;
我们先来介绍互斥锁是如何判断当前 Goroutine 能否进入自旋等互斥锁的释放:
func (m *Mutex) lockSlow() { var waitStartTime int64 starving := false awoke := false iter := 0 old := m.state for { if old&(mutexLocked|mutexStarving) == mutexLocked && runtime_canSpin(iter) { if !awoke && old&mutexWoken == 0 && old>>mutexWaiterShift != 0 && atomic.CompareAndSwapInt32(&m.state, old, old|mutexWoken) { awoke = true } runtime_doSpin() iter++ old = m.state continue }
自旋是一种多线程同步机制,当前的进程在进入自旋的过程中会一直保持 CPU 的占用,持续检查某个条件是否为真。在多核的 CPU 上,自旋可以避免 Goroutine 的切换,使用恰当会对性能带来很大的增益,但是使用的不恰当就会拖慢整个程序,所以 Goroutine 进入自旋的条件非常苛刻:
-
互斥锁只有在普通模式才能进入自旋;
-
runtime.sync_runtime_canSpin
需要返回true:
- 运行在多 CPU 的机器上;
- 当前 Goroutine 为了获取该锁进入自旋的次数小于四次;
- 当前机器上至少存在一个正在运行的处理器 P 并且处理的运行队列为空;
一旦当前 Goroutine 能够进入自旋就会调用
runtime.sync_runtime_doSpin
和runtime.procyield
并执行 30 次的PAUSE
指令,该指令只会占用 CPU 并消耗 CPU 时间:func sync_runtime_doSpin() { procyield(active_spin_cnt) } TEXT runtime·procyield(SB),NOSPLIT,$0-0 MOVL cycles+0(FP), AX again: PAUSE SUBL $1, AX JNZ again RET
处理了自旋相关的特殊逻辑之后,互斥锁会根据上下文计算当前互斥锁最新的状态。几个不同的条件分别会更新
state
字段中存储的不同信息 —mutexLocked
、mutexStarving
、mutexWoken
和mutexWaiterShift
:new := old if old&mutexStarving == 0 { new |= mutexLocked } if old&(mutexLocked|mutexStarving) != 0 { new += 1 << mutexWaiterShift } if starving && old&mutexLocked != 0 { new |= mutexStarving } if awoke { new &^= mutexWoken }
计算了新的互斥锁状态之后,会使用 CAS 函数
sync/atomic.CompareAndSwapInt32
更新状态:if atomic.CompareAndSwapInt32(&m.state, old, new) { if old&(mutexLocked|mutexStarving) == 0 { break // 通过 CAS 函数获取了锁 } ... runtime_SemacquireMutex(&m.sema, queueLifo, 1) starving = starving || runtime_nanotime()-waitStartTime > starvationThresholdNs old = m.state if old&mutexStarving != 0 { delta := int32(mutexLocked - 1<<mutexWaiterShift) if !starving || old>>mutexWaiterShift == 1 { delta -= mutexStarving } atomic.AddInt32(&m.state, delta) break } awoke = true iter = 0 } else { old = m.state } } }
如果没有通过 CAS 获得锁,会调用
runtime.sync_runtime_SemacquireMutex
通过信号量保证资源不会被两个 Goroutine 获取。runtime.sync_runtime_SemacquireMutex
会在方法中不断尝试获取锁并陷入休眠等待信号量的释放,一旦当前 Goroutine 可以获取信号量,它就会立刻返回,sync.Mutex.Lock
的剩余代码也会继续执行。- 在正常模式下,这段代码会设置唤醒和饥饿标记、重置迭代次数并重新执行获取锁的循环;
- 在饥饿模式下,当前 Goroutine 会获得互斥锁,如果等待队列中只存在当前 Goroutine,互斥锁还会从饥饿模式中退出;
互斥锁的解锁过程
sync.Mutex.Unlock
与加锁过程相比就很简单,该过程会先使用sync/atomic.AddInt32
函数快速解锁,这时会发生下面的两种情况:- 如果该函数返回的新状态等于 0,当前 Goroutine 就成功解锁了互斥锁;
- 如果该函数返回的新状态不等于 0,这段代码会调用
sync.Mutex.unlockSlow
开始慢速解锁:
func (m *Mutex) Unlock() { new := atomic.AddInt32(&m.state, -mutexLocked) if new != 0 { m.unlockSlow(new) } }
sync.Mutex.unlockSlow
会先校验锁状态的合法性 — 如果当前互斥锁已经被解锁过了会直接抛出异常 “sync: unlock of unlocked mutex” 中止当前程序。在正常情况下会根据当前互斥锁的状态,分别处理正常模式和饥饿模式下的互斥锁:
func (m *Mutex) unlockSlow(new int32) { if (new+mutexLocked)&mutexLocked == 0 { throw("sync: unlock of unlocked mutex") } if new&mutexStarving == 0 { // 正常模式 old := new for { if old>>mutexWaiterShift == 0 || old&(mutexLocked|mutexWoken|mutexStarving) != 0 { return } new = (old - 1<<mutexWaiterShift) | mutexWoken if atomic.CompareAndSwapInt32(&m.state, old, new) { runtime_Semrelease(&m.sema, false, 1) return } old = m.state } } else { // 饥饿模式 runtime_Semrelease(&m.sema, true, 1) } }
- 在正常模式下,上述代码会使用如下所示的处理过程:
- 如果互斥锁不存在等待者或者互斥锁的
mutexLocked
、mutexStarving
、mutexWoken
状态不都为 0,那么当前方法可以直接返回,不需要唤醒其他等待者; - 如果互斥锁存在等待者,会通过
sync.runtime_Semrelease
唤醒等待者并移交锁的所有权;
- 如果互斥锁不存在等待者或者互斥锁的
- 在饥饿模式下,上述代码会直接调用
sync.runtime_Semrelease
将当前锁交给下一个正在尝试获取锁的等待者,等待者被唤醒后会得到锁,在这时互斥锁还不会退出饥饿状态;
小结
互斥锁的加锁过程比较复杂,它涉及自旋、信号量以及调度等概念:
- 如果互斥锁处于初始化状态,会通过置位
mutexLocked
加锁; - 如果互斥锁处于
mutexLocked
状态并且在普通模式下工作,会进入自旋,执行 30 次PAUSE
指令消耗 CPU 时间等待锁的释放; - 如果当前 Goroutine 等待锁的时间超过了 1ms,互斥锁就会切换到饥饿模式;
- 互斥锁在正常情况下会通过
runtime.sync_runtime_SemacquireMutex
将尝试获取锁的 Goroutine 切换至休眠状态,等待锁的持有者唤醒; - 如果当前 Goroutine 是互斥锁上的最后一个等待的协程或者等待的时间小于 1ms,那么它会将互斥锁切换回正常模式;
互斥锁的解锁过程与之相比就比较简单,其代码行数不多、逻辑清晰,也比较容易理解:
- 当互斥锁已经被解锁时,调用
sync.Mutex.Unlock
会直接抛出异常; - 当互斥锁处于饥饿模式时,将锁的所有权交给队列中的下一个等待者,等待者会负责设置
mutexLocked
标志位; - 当互斥锁处于普通模式时,如果没有 Goroutine 等待锁的释放或者已经有被唤醒的 Goroutine 获得了锁,会直接返回;在其他情况下会通过
sync.runtime_Semrelease
唤醒对应的 Goroutine;
-
详解java中的互斥锁信号量和多线程等待机制
2020-08-29 07:59:48主要介绍了Java编程中的互斥锁,信号量和多线程等待机制实例详解,简单介绍了互斥锁和信号量的区别,需要的朋友可以了解下。