Linux C互斥锁和条件变量(POSIX标准)

       与进程类似,线程也存在同步的问题,当多个控制线程共享相同的内存时,需要确保每个线程看到一致的数据视图,如果每个线程使用的变量都是其他线程不会读取或修改的(比如线程私有数据),就不会存在一致性问题。通常来说用户可以使用互斥量(互斥锁)或者的条件变量(条件锁)的方式来解决线程的同步问题。

互斥锁

       互斥锁是一个简单的锁定命令,它可以用来锁定共享资源使得其他线程无法访问。互斥锁具有以下特点:

       ·原子性:把一个互斥锁定义为一个原子操作,这意味着操作系统保证了如果一个线程锁定了互斥锁,则没有其他线程可以在同一时间成功锁定这个互斥量。

       ·唯一性:如果一个线程锁定一个互斥量,在它接触锁定之前,没有其他线程可以锁定这个互斥量。

       ·非繁忙等待:如果一个线程已经锁定了一个互斥锁,第二个线程又试图去锁定这个互斥锁,则第二个线程将被挂起(不占用CPU资源),直到第一个线程解锁,第二个线程则被唤醒并继续执行,同时锁定这个互斥量。

锁类型

互斥锁类型
锁类型 初始化方式    
普通锁 PTHREAD_MUTEX_INITIALIZER    
嵌套锁 PTHREAD_RECURSIVE_MUTEX_INITIALIZER_NP    
纠错锁 PTHREAD_ERRORCHECK_MUTEX_INITIALIZER_NP    
自适应锁 PTHREAD_ADAPTIVE_MUTEX_INITIALIZER_NP    

锁操作函数

创建与销毁

       有两种方法创建互斥锁,静态方式和动态方式。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指向的条件变量阻塞的线程退出阻塞状态,如果没有阻塞线程,则函数无效。

                                                                                            本文部分内容摘自《Linux C编程从基础到实践》,有改动

你可能感兴趣的