多线程编程

Linux互斥锁、条件变量和信号量

进行多线程编程,最应该注意的就是那些共享的数据,因为无法知道哪个线程会在哪个时候对它进行操作,也无法得知哪个线程会先运行,哪个线程会后运行。所以,要对这些资源进行合理的分配和正确的使用。在Linux下,提供了互斥锁、条件变量和信号量来对共享资源进行保护。

一、互斥锁
互斥锁,是一种信号量,常用来防止两个进程或线程在同一时刻访问相同的共享资源。
需要的头文件:pthread.h
互斥锁标识符:pthread_mutex_t


如果一个线程已经给一个互斥量上锁了,后来在操作的过程中又再次调用了该上锁的操作,那么该线程将会无限阻塞在这个地方,从而导致死锁。这就需要互斥量的属性。

互斥量分为下面三种:
1、快速型。这种类型也是默认的类型。该线程的行为正如上面所说的。
2、递归型。如果遇到我们上面所提到的死锁情况,同一线程循环给互斥量上锁,那么系统将会知道该上锁行为来自同一线程,那么就会同意线程给该互斥量上锁。
3、错误检测型。如果该互斥量已经被上锁,那么后续的上锁将会失败而不会阻塞,pthread_mutex_lock()操作将会返回EDEADLK。


前面我们提到在调用pthread_mutex_lock()的时候,如果此时mutex已经被其他线程上锁,那么该操作将会一直阻塞在这个地方。如果我们此时不想一直阻塞在这个地方,那么可以调用下面函数:pthread_mutex_trylock。
如果此时互斥量没有被上锁,那么pthread_mutex_trylock将会返回0,并会对该互斥量上锁。如果互斥量已经被上锁,那么会立刻返回EBUSY。

二、条件变量
需要的头文件:pthread.h
条件变量标识符:pthread_cond_t

1、互斥锁的存在问题:
互斥锁一个明显的缺点是它只有两种状态:锁定和非锁定。设想一种简单情景:多个线程访问同一个共享资源时,并不知道何时应该使用共享资源,如果在临界区里加入判断语句,或者可以有效,但一来效率不高,二来复杂环境下就难以编写了,这是我们需要一个结构,能在条件成立时触发相应线程,进行变量修改和访问。

2、条件变量:
条件变量通过允许线程阻塞和等待另一个线程发送信号的方法弥补了互斥锁的不足,它常和互斥锁一起使用。使用时,条件变量被用来阻塞一个线程,当条件不满足时,线程往往解开相应的互斥锁并等待条件发生变化。一旦其它的某个线程改变了条件变量,它将通知相应的条件变量唤醒一个或多个正被此条件变量阻塞的线程。这些线程将重新锁定互斥锁并重新测试条件是否满足。一般说来,条件变量被用来进行线承间的同步。

pthread_cond_signal用来激活被阻塞并等待在该条件变量cond上的一个线程。存在多个线程阻塞在此条件变量上时,哪一个线程被唤醒是由线程的调度策略唤醒其中的一个。要注意的是,必须用保护条件变量的互斥锁来保护这个函数,否则条件满足信号又可能在测试条件和调用pthread_cond_wait函数之间被发出,从而造成无限制的等待。pthread_cond_broadcast()则激活所有等待线程。

等待条件有两种方式:无条件等待pthread_cond_wait()和计时等待pthread_cond_timedwait()。pthread_cond_wait使线程阻塞在一个条件变量上。线程解开mutex指向的锁并被条件变量cond阻塞。线程可以被函数pthread_cond_signal和函数 pthread_cond_broadcast唤醒,但是要注意的是,条件变量只是起阻塞和唤醒线程的作用,具体的判断条件还需用户给出。线程被唤醒后,它将重新检查判断条件是否满足,如果还不满足,一般说来线程应该仍阻塞在这里,被等待被下一次唤醒。这个过程一般用while语句实现。计时等待方式如果在给定时刻前条件没有满足,则返回ETIMEOUT,结束等待。
无论哪种等待方式,都必须和一个互斥锁配合,以防止多个线程同时请求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()前的加锁动作对应。

pthread_cond_wait()和pthread_cond_timedwait()都被实现为取消点,因此,在该处等待的线程将立即重新运行,在重新锁定mutex后离开 pthread_cond_wait(),然后执行取消动作。也就是说如果pthread_cond_wait()被取消,mutex是保持锁定状态的,因而需要定义退出回调函数来为其解锁。
pthread_cond_wait实际上可以看作是以下几个动作的合体:
解锁线程锁;
等待条件为true;
加锁线程锁;

使用形式:
// thread a
pthread_mutex_lock(&mutex);
if (condition is true)
pthread_cond_signal(&cond);
pthread_mutex_unlock(&mutex);

// thread b
pthread_mutex_lock(&mutex);
while (condition is false)
pthread_cond_wait(&cond, &mutex);
pthread_mutex_unlock(&mutex);
/*线程b中为什么使用while呢?因为在pthread_cond_signal和pthread_cond_wait返回之间,有时间差,假设在这个时间差内,条件改变了,显然需要重新检查条件。也就是说在pthread_cond_wait被唤醒的时候可能该条件已经不成立。*/

pthread_cond_destroy()只有在没有线程在该条件变量上等待的时候才能注销这个条件变量,否则返回 EBUSY。因为Linux实现的条件变量没有分配什么资源,所以注销动作只包括检查是否有等待线程。

三、信号量
信号量本质上是一个非负的整数计数器,它被用来控制对公共资源的访问。每一次调用wait操作将会使semaphore值减1,而如果semaphore值已经为0,则wait操作将会阻塞。每一次调用post操作将会使semaphore值加1。
需要的头文件:semaphore.h
信号量标识符:sem_t


信号量与线程锁、条件变量相比还有以下几点不同:
1)锁必须是同一个线程获取以及释放,否则会死锁。而条件变量和信号量则不必。
2)信号的递增与减少会被系统自动记住,系统内部有一个计数器实现信号量,不必担心会丢失,而唤醒一个条件变量时,如果没有相应的线程在等待该条件变量,这次唤醒将被丢失。



[NOTE]
线程数据
  在单线程的程序里,有两种基本的数据:全局变量和局部变量。但在多线程程序里,还有第三种数据类型:线程数据(TSD: Thread-Specific Data)。它和全局变量很象,在线程内部,各个函数可以象使用全局变量一样调用它,但它对线程外部的其它线程是不可见的。这种数据的必要性是显而易见的。例如我们常见的变量errno,它返回标准的出错信息。它显然不能是一个局部变量,几乎每个函数都应该可以调用它;但它又不能是一个全局变量,否则在 A线程里输出的很可能是B线程的出错信息。要实现诸如此类的变量,我们就必须使用线程数据。我们为每个线程数据创建一个键,它和这个键相关联,在各个线程里,都使用这个键来指代线程数据,但在不同的线程里,这个键代表的数据是不同的,在同一个线程里,它代表同样的数据内容。
  和线程数据pthread_key_t相关的函数主要有4个:创建一个键;为一个键指定线程数据;从一个键读取线程数据;删除键。






1 -- 关于pthread条件变量

man pthread_cond_init | col -b > pthread_cond.man得到manual中的描述:

A condition (short for ''condition variable'') is a synchronization device that allows threads to suspend execution and relinquish the processors until some predicate on shared data is satisfied. The basic operations on conditions are: signal the condition(when the predicate becomes true), and wait for the condition, suspending the thread execution until another thread signals the condition.

条件变量是同步线程的一种机制,它允许线程挂起,让出处理器等待其他线程向它发送信号,该线程收到该信号后被唤醒继续执行程序。对条件变量基本的操作就是:a)向条件变量发送信号,唤醒等待的线程;b)等待条件变量并挂起直至其他线程向该条件变量发送信号。为了防止竞争,条件变量总是和一个互斥锁同时使用。

2 -- 条件变量相关函数

phtread条件变量主要有如下的这些函数:

#include <pthread.h>
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *cond_attr);
int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond);
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 *abstime);
int pthread_cond_destroy(pthread_cond_t *cond);

返回0为成功,非0为失败。
注意:pthread_cond_init,pthread_cond_signal,pthread_cond_broadcast,pthread_cond_wait只返回0,不会返回其他错误码。也就是说这几个函数每次调用都会成功,编程时不用检查返回值。但pthread_cond_timedwait和pthread_cond_destroy会返回错误码,需要注意!

3 -- 条件变量创建

条件变量的初始化有两种方式:静态和动态方式。
1.静态方式
初始化方法:pthread_cond_t pcond = PTHREAD_COND_INITIALIZER;
对于静态分配的条件变量,如果使用默认的条件变量属性,可以直接使用PTHREAD_COND_INITIALIZER对条件变量进行赋值来初始化。pthread_cond_t是一个结构体,同样PTHREAD_COND_INITIALIZER是一个结构体常量。

2.动态方式
动态方式调用pthread_cond_init函数对条件变量初始化,该函数的第二个参数指向条件变量属性的结构体。尽管POSIX标准中为条件变量定义了属性,但在LinuxThreads中没有实现,因此cond_attr值通常设置为NULL。

4 -- 条件变量等待
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);

pthread_cond_wait调用相当复杂,它是如下执行序列的一个组合:
(1)释放互斥锁 并且 线程挂起(这两个操作是一个原子操作);
(2)线程获得信号,尝试获得互斥锁后被唤醒;

我们知道调用pthread_cond_signal给条件变量发送信号时,如果当时没有线程在等待这个条件变量,信号将被丢弃。如果"释放互斥锁"和"线程挂起"不是一个原子操作,那么pthread_cond_wait线程在"释放互斥锁"和"线程挂起"之间,如果有信号一旦发生,程序就会错失一次条件变量变化。

int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec *abstime)

返回错误码:ETIMEDOUT。如果在指定时刻前,没有信号发生。
返回错误码:EINTR。该函数被信号中断。

pthread_cond_timewait是等待条件变量的令一种形式,与前一种的区别是计时等待方式如果在给定时刻前条件没有满足,就返回ETIMEOUT结束等待。该函数在不同情况下会返回特定错误码,编程时请参照开发。

5 -- 条件变量触发
int pthread_cond_signal(pthread_cond_t *cond);

pthread_cond_signal唤醒等待改条件变量所有线程中的一个。如果此时没有线程在等待该条件变量,那么就丢弃该信号,当做什么也没发生。如果有多个线程在等待,精确保证只有一个线程被唤醒。

int pthread_cond_broadcast(pthread_cond_t *cond);

pthread_cond_broadcast唤醒等待该条件变量所有线程。如果此时没有线程在等待该条件变量,那么就丢弃该信号,当做什么也没发生。

6 -- 条件变量销毁
int pthread_cond_destroy(pthread_cond_t *cond);

返回错误码:EBUSY。当前还有线程在该条件变量上等待。

pthread_cond_destroy销毁一个条件变量,但只有在没有线程在该条件变量上等待的时候才能销毁,否则返回EBUSY。因为Linux实现的条件变量没有分配什么资源,所以注销动作只包括检查是否有等待线程。

7 -- memcached应用条件变量实例

背景:memcached是一个多线程结构的程序,主线程负责接收和分发请求,工作者线程实际处理请求。工作者线程在主线程中创建。创建线程后,工作者线程需要完成一些初始化工作,才允许主线程继续执行,所以主线程需要等待这些工作者线程全部初始化完毕。
这里就使用到了条件变量,大体的流程是这样的:
(1)主线程初始化一个条件变量和一个互斥锁;
(2)主线程创建n个工作者线程;
(3)主线程调用pthread_mutex_lock锁定互斥锁,然后调用pthread_cond_wait在条件变量上wait,等待被唤醒;
(4)子线程执行初始化代码,完毕后获取互斥锁,累加已初始化线程数量,调用pthread_cond_signal给该条件变量发送信号,同时释放互斥锁;
(5)线程调度唤醒主线程,主线程检查现在已经初始化的线程数目,如果都初始化了就释放互斥锁,顺序执行其他代码;如果还没初始化完毕,调用pthread_cond_wait再次等待。

主线程执行如下代码:

/*主线程创建nthreads个线程,线程创建后进行初始化,初始化完毕后累加init_count*/
for (i = 0; i < nthreads; i++) 
{
	create_worker(worker_libevent, &threads[i]);
}

/*主线程等待线程全部初始化,条件是已初始化量init_count等于线程数nthreads*/
pthread_mutex_lock(&init_lock);
while (init_count < nthreads) 
{
       	pthread_cond_wait(&init_cond, &init_lock);
}
pthread_mutex_unlock(&init_lock);

工作者线程执行如下代码:

//TODO:初始化代码
pthread_mutex_lock(&init_lock);
init_count++;	//累加已初始化线程数量
pthread_cond_signal(&init_cond);
pthread_mutex_unlock(&init_lock);

看完这段代码之后,估计都会有一个疑问:流程(3)中互斥锁已被主线程获取了,在线程全部初始化完毕之前,主线程并没有显式释放互斥锁,为什么在流程(4)中工作者线程还能获取到互斥锁呢?在讲解pthread_cond_wait函数时说明过这个问题,也可以从下图看出其中奥妙。

pthread_cond_wait


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值