-
当多个控制线程共享相同的内存时,需要确保每个线程看到一致的数据视图,如果每个线程使用的变量都是其他线程不会读取或者修改的,就不会存在一致性的问题。通常来说用户可以使用互斥量(互斥锁)或者条件变量(条件锁)的方式来解决线程的同步问题。
-
互斥锁
-
互斥锁是一个简单的锁定命令,它可以用来锁定共享资源使得其他线程无法访问,具有以下特点
-
原子性:把一个互斥锁定义为一个原子操作,这意味着操作系统保证了不可被中断。如果一个线程锁定了互斥锁,则没有其他线程可以在同一时间成功锁定这个互斥量。
-
唯一性:如果一个线程锁定一个互斥量,在它接触锁定之前,没有其他线程可以锁定这个互斥量。
-
非繁忙等待:如果一个线程已经锁定了一个互斥锁,第二个线程又试图去锁定这个互斥锁,则第二个线程将会被挂起(不占用CPU资源),直到第一个线程解锁,第二个线程则被唤醒并继续执行,同时锁定这个互斥量。
-
-
锁类型
-
锁操作函数
-
创建与销毁
-
有两种方法创建互斥锁,静态方式和动态方式。POSIX定义了一个宏PTHREAD_MUTEX_INITIALIZER来静态初始化互斥锁,方法如下:
-
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
-
在LinuxThreads实现中,pthread_mutex_t是一个结构,而PTHREAD_MUTEX_INITIALIZER则是一个结构常量。
-
动态方式是调用pthread_mutex_init函数,其中参数attr用于指定锁的属性。
-
pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutex_attr
*attr);
-
-
不会出现多个线程同时初始化同一个互斥锁的情形,一个互斥锁在使用期间一定不会被重新初始化。如果函数执行成功,则返回0,并创建新建的互斥锁的ID值放到参数mutex中。如果执行失败,那么将返回一个错误编号。
-
注销锁的函数是pthread_mutex_destroy。
-
pthread_mutex_destroy(pthread_mutex_t *mutex);
-
存储互斥锁的内存并不被释放,如果pthread_mutex_destroy执行成功,则返回0,否则返回一个错误编号。另外,通过静态创建的锁不需要也不能使用这个函数注销锁。
-
-
锁定与解锁
-
pthread_mutex_lock函数用于锁定由参数mutex指向的互斥锁。
-
int pthread_mutex_lock(pthread_mutex_t *mutex);
-
如果mutex指向的锁已经被锁定,那么当前调用锁定函数的线程将阻塞直到互斥锁被其他线程释放(阻塞线程按照优先级等待)。当pthread_mutex_lock返回时,说明互斥锁已经被当前线程成功锁定。
-
pthread_mutex_trylock函数用于尝试给指定的互斥锁加锁。
-
int pthread_mutex_trylock(pthread_mutex_t *mutex);
-
该函数是pthread_mutex_lock的非阻塞版本。trylock在给一个互斥锁加锁时,如果互斥锁已经被锁定,那么函数将返回错误而不会阻塞线程。
-
-
-
int pthread_mutex_unlock(pthread_mutex_t *mutex);
-
解锁函数的前提是互斥锁处于锁定状态,而且调用本函数的线程必须是给这个互斥锁加锁的线程才能解锁(解铃还须系铃人)。解锁后,如果有其他线程在等待互斥锁,那么由调度程序决定哪个线程将获得互斥锁并脱离阻塞状态。
-
条件变量
-
使用互斥锁虽然可以解决一些资源竞争的问题,但互斥锁只有两种状态(加锁和解锁),这限制了互斥锁的用途。
-
条件变量(条件锁)也可以解决线程同步和共享资源访问的问题,条件变量是对互斥锁的补充,它允许一个线程阻塞并等待另一个线程发送的信号,当收到信号时,阻塞的线程被唤醒并试图锁定与之相关的互斥锁。
-
条件变量初始化
-
条件变量和互斥锁一样,都有静态动态两种创建方式,静态方式使用PTHREAD_COND_INITIALIZER常量,如下:
- pthread_cond_t cond = PTHREAD_COND_INITIALIZER
-
动态方式调用函数int pthread_cond_init,API定义如下:
- int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);
-
条件变量的属性由参数attr指定,如果参数attr为NULL,那么就使用默认的属性设置。尽管POSIX标准中为条件变量定义了属性,但在LinuxThreads中没有实现,因此cond_attr值通常为NULL,且被忽略。多线程不能同时初始化一个条件变量,因为这是原子操作。
-
如果函数调用成功,则返回0,并将新创建的条件变量的ID放在参数cond中。
-
解除条件变量:
-
int pthread_cond_destroy(pthread_cond_t *cond);
-
调用destroy函数解除条件变量并不会释放存储条件变量的内存空间。
-
-
条件变量阻塞(等待)
-
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
-
int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec *abtime);
-
等待有两种方式:条件等待pthread_cond_wait()和计时等待pthread_cond_timedwait(),其中计时等待方式如果在给定时刻前条件没有满足,则返回ETIMEDOUT,结束等待,其中abstime以与系统调用time相同意义的绝对时间形式出现,0表示格林尼治时间1970年1月1日0时0分0秒。
-
无论哪种等待方式,都必须和一个互斥锁配合,以防止多个线程同时请求pthread_cond_wait()或pthread_cond_timedwait()(下同)的竞争条件(Race Condition)。mutex互斥锁必须是普通锁(PTHREAD_MUTEX_TIMED_NP)或者自适应锁(PTHREAD_MUTEX_ADAPTIVE_NP),且在调用pthread_cond_wait()前必须由本线程加锁(pthread_mutex_lock()),而在更新条件等待队列以前,mutex保持锁定状态,并在线程挂起进入等待前解锁。在条件满足从而离开pthread_cond_wait()之前,mutex将被重新加锁,以与进入pthread_cond_wait()前的加锁动作对应。阻塞时处于解锁状态。
-
激活
-
int pthread_cond_signal(pthread_cond_t *cond);
-
int pthread_cond_broadcast(pthread_cond_t *cond);
-
pthread_cond_signal函数的作用是发送一个信号给另外一个正在处于阻塞等待状态的线程,使其脱离阻塞状态,继续执行,如果没有线程处在阻塞等待状态,pthread_cond_signal也会成功返回。
-
共享变量的状态改变必须遵守lock/unlock的规则:需要在同一互斥锁的保护下使用pthread_cond_signal(即pthread_cond_wait必须放在pthread_mutex_lock和pthread_mutex_unlock之间)否则条件变量可以在对关联条件变量的测试和pthread_cond_wait带来的阻塞之间获得信号,这将导致无限期的等待(死锁)。因为他要根据共享变量的状态来决定是否要等待,所以为了避免死锁,必须要在lock/unlock队中。
-
共享变量的状态改变必须遵守lock/unlock的规则:pthread_cond_signal即可以放在pthread_mutex_lock和pthread_mutex_unlock之间,也可以放在pthread_mutex_lock和pthread_mutex_unlock之后,但是各有优缺点。
-
若为前者,在某些线程的实现中,会造成等待线程从内核中唤醒(由于cond_signal)然后又回到内核空间(因为cond_wait返回后会有原子加锁的行为),所以一来一回会有性能的问题(上下文切换)。详细来说就是,当一个等待线程被唤醒的时候,它必须首先加锁互斥量(参见pthread_cond_wait()执行步骤)。如果线程被唤醒而此时通知线程任然锁住互斥量,则被唤醒线程会立刻阻塞在互斥量上,等待通知线程解锁该互斥量,引起线程的上下文切换。当通知线程解锁后,被唤醒线程继续获得锁,再一次的引起上下文切换。这样导致被唤醒线程不能顺利加锁,延长了加锁时间,加重了系统不必要的负担。但是在LinuxThreads或者NPTL里面,就不会有这个问题,因为在Linux 线程中,有两个队列,分别是cond_wait队列和mutex_lock队列, cond_signal只是让线程从cond_wait队列移到mutex_lock队列,而不用返回到用户空间,不会有性能的损耗,因此Linux推荐这种形式。
-
而后者不会出现之前说的那个潜在的性能损耗,因为在signal之前就已经释放锁了。但如果unlock和signal之前,有个低优先级的线程正在mutex上等待的话,那么这个低优先级的线程就会抢占高优先级的线程(cond_wait的线程)。而且,假设而这在上面的放中间的模式下是不会出现的。
-
而对于pthread_cond_broadcast函数,它使所有由参数cond指向的条件变量阻塞的线程退出阻塞状态,如果没有阻塞线程,则函数无效。
-
条件变量为什么要和互斥锁一起使用
-
mutex体现的是一种竞争,我离开了,通知你进来。
-
cond体现的是一种协作,我准备好了,通知你开始吧。
-
互斥锁一个明显的缺点是它只有两种状态:锁定和非锁定。而条件变量通过允许线程阻塞和等待另一个线程发送信号的方法弥补了互斥锁的不足,它常和互斥锁一起配合使用。使用时,条件变量被用来阻塞一个线程,当条件不满足时,线程往往解开相应的互斥锁并等待条件发生变化。一旦其他的某个线程改变了条件变量,他将通知相应的条件变量唤醒一个或多个正被此条件变量阻塞的线程。这些线程将重新锁定互斥锁并重新测试条件是否满足。一般说来,条件变量被用来进行线程间的同步。
-
两个线程操作同一临界区时,通过互斥锁保护,若A线程已经加锁,B线程再加锁时候会被阻塞,直到A释放锁,B再获得锁运行,进程B必须不停的主动获得锁、检查条件、释放锁、再获得锁、再检查、再释放,一直到满足运行的条件的时候才可以(而此过程中其他线程一直在等待该线程的结束),这种方式是比较消耗系统的资源的。而条件变量同样是阻塞,还需要通知才能唤醒,线程被唤醒后,它将重新检查判断条件是否满足,如果还不满足,该线程就休眠了,应该仍阻塞在这里,等待条件满足后被唤醒,节省了线程不断运行浪费的资源。这个过程一般用while语句实现。当线程B发现被锁定的变量不满足条件时会自动的释放锁并把自身置于等待状态,让出CPU的控制权给其它线程。其它线程此时就有机会去进行操作,当修改完成后再通知那些由于条件不满足而陷入等待状态的线程。这是一种通知模型的同步方式,大大的节省了CPU的计算资源,减少了线程之间的竞争,而且提高了线程之间的系统工作的效率。这种同步方式就是条件变量。
-
举例说明:
-
A线程从队列中取元素,B线程往队列中存放元素。不考虑免锁的实现。需要一个mutex用来保护队列的一致性,避免两个线程同时操作队列破坏数据结构。互斥锁
当队列为空的时候,A需要不断的探测队列状态 : -
可能在刚进入休眠时,B放入元素了,但仍然需要休眠完整个10s的时间。造成不必要的延迟。当然如果不sleep,也可以,但会造成不必要的CPU开销。使用基于条件变量的事件通知唤醒机制,就可以避免这些问题。一旦B放入元素完成后就执行pthread_cond_signal(),当前阻塞的线程就会立即被唤醒开始干活儿。
-
条件变量都用互斥锁进行保护,条件变量状态的改变都应该先锁住互斥锁,pthread_cond_wait()需要传入一个已经加锁的互斥锁,该函数把调用线程加入等待条件的调用列表中,然后释放互斥锁,在条件满足从而离开pthread_cond_wait()时,mutex将被重新加锁,这两个函数是原子操作。
-
为了避免因条件判断语句与其后的正文或wait语句之间的间隙而产生的漏判或误判,用一个mutex来保证。对于某个cond的判断,修改等操作某一时刻只有一个线程在访问。条件变量本身就是一个竞争资源,这个资源的作用是对其后程序正文的执行权,于是用一个锁来保护。
-
总结:条件变量用于某个线程需要在某种条件成立时才去保护它将要操作的临界区,这种情况从而避免了线程不断轮询检查该条件是否成立而降低效率的情况,这是实现了效率提高。。。在条件满足时,自动退出阻塞,再加锁进行操作。
-
互斥锁还有一个缺点就是会造成死锁。
-
例如线程A和线程B都需要独占使用2个资源,但是他们都分别先占据了一个资源,然后又相互等待另外一个资源的释放,这样就形成了一个死锁。
-
条件变量起到了阻塞和唤醒线程的作用,所以通常互斥锁要和条件变量配合。
-
为了解决以上问题,条件变量常和互斥锁一起使用,条件变量通过允许线程阻塞和等待另一个线程发送信号的方法弥补了互斥锁的不足。使用时,条件变量被用来阻塞一个线程,当条件不满足时,线程往往解开相应的互斥锁并等待条件发生变化。一旦其它的某个线程改变了条件变量,它将通知相应的条件变量唤醒一个或多个正被此条件变量阻塞的线程。这些线程将重新锁定互斥锁并重新测试条件是否满足。
-
在这个循环语句中,如果不满足条件,那么会不断地进行加锁,解锁,直到条件被满足。如果加入了条件变量,如果不满足条件,线程会阻塞直到条件发生变化而被唤醒,然后去查询是否满足条件。
-
原子性:一个或多个操作在CPU执行的过程中不被中断的特性称为原子性。
Linux互斥锁和条件变量、两者之间配合使用
最新推荐文章于 2023-04-09 22:00:01 发布