Linux | c&cpp | Email | github | QQ群:425043908 关注本站

itarticle.cc

您现在的位置是:网站首页 -> 后端开发 文章内容

详解linux互斥锁 pthread_mutex和条件变量pthread_cond-itarticl.cc-IT技术类文章记录&分享

发布时间: 9年前后端开发 136人已围观返回

=============================================================

int pthread_create(

pthread_t *tid,

const pthread_attr_t *attr,

void*(*start_routine)(void*),

void*arg

);

//参数tid 用于返回新创建线程的线程号;

//start_routine 是线程函数指针,线程从这个函数开始独立地运行;

//arg 是传递给线程函数的参数。由于start_routine 是一个指向参数类型为void*,返回值为void*的指针,

//所以如果需要传递或返回多个参数时,可以使用强制类型转化。

=============================================================

void pthread_exit(

void* value_ptr

);

// 参数value_ptr 是一个指向返回状态值的指针。

=============================================================

int pthread_join(

pthread_t tid ,

void**status

);

// 参数tid 是希望等待的线程的线程号,status 是指向线程返回值的指针,线程的返回值就是pthread_exit

// 中的value_ptr 参数,或者是return语句中的返回值。该函数可用于线程间的同步。

=============================================================

int pthread_mutex_init(

pthread_mutex_t *mutex,

constpthread_mutex_attr_t* attr

);

//该函数初始化一个互斥体变量,如果参数attr 为NULL,则互斥

//体变量mutex 使用默认的属性。

=============================================================

int pthread_mutex_lock(

pthread_mutex_t *mutex

);

// 该函数用来锁住互斥体变量。如果参数mutex 所指的互斥体已经

//被锁住了,那么发出调用的线程将被阻塞直到其他线程对mutex 解锁。

=============================================================

int pthread_mutex_trylock(

pthread_t *mutex

);

//该函数用来锁住mutex 所指定的互斥体,但不阻塞。如果该互斥

//体已经被上锁,该调用不会阻塞等待,而会返回一个错误代码。

=============================================================

int pthread_mutex_unlock(

pthread_mutex_t *mutex

);

//该函数用来对一个互斥体解锁。如果当前线程拥有参数mutex 所

//指定的互斥体,该调用将该互斥体解锁。

=============================================================

int pthread_mutex_destroy (

pthread_mutex_t *mutex

);

//该函数用来释放分配给参数mutex 的资源。调用成功时返回值为

//0, 否则返回一个非0 的错误代码。

=============================================================

intp thread_cond_init(

pthread_cond_t *cond,

const pthread_cond_attr_t*attr

);

//该函数按参数attr指定的属性创建一个条件变量。调用成功返回,

//并将条件变量ID 赋值给参数cond,否则返回错误代码。

=============================================================

int pthread_cond_wait (

pthread_cond_t *cond ,

pthread_mutex_t*mutex

);

// 该函数调用为参数mutex 指定的互斥体解锁,等待一个事件(由

//参数cond 指定的条件变量)发生。调用该函数的线程被阻塞直到有其他

//线程调用pthread_cond_signal 或pthread_cond_broadcast 函数置相应的条

//件变量,而且获得mutex 互斥体时才解除阻塞。

=============================================================

int pthread_cond_timewait(

pthread_cond_t *cond ,

pthread_mutex_t*mutex ,

const struct timespec *abstime

);

// 该函数与pthread_cond_wait 不同的是当系统时间到达abstime 参数指定的时间时,被阻塞线程也可以被唤起继续执行。

=============================================================

int pthread_cond_broadcast(

pthread_cond_t *cond

);

// 该函数用来对所有等待参数cond所指定的条件变量的线程解除阻塞,调用成功返回0,否则返回错误代码。

=============================================================

int pthread_cond_signal(

pthread_cond_t *cond

);

// 该函数的作用是解除一个等待参数cond所指定的条件变量的线程的阻塞状态。

// 当有多个线程挂起等待该条件变量,也只唤醒一个线程。

=============================================================

int pthread_cond_destroy(

pthread_cond_t *cond

);

// 该函数的作用是释放一个条件变量。释放为条件变量cond 所分配的资源。调用成功返回值为0,否则返回错误代码。

=============================================================

int pthread_key_create(

pthread_key_t key ,

void(*destructor(void*))

);

// 该函数创建一个键值,该键值映射到一个专有数据结构体上。如果第二个参数不是NULL,

// 这个键值被删除时将调用这个函数指针来释放数据空间。

=============================================================

int pthread_key_delete(

pthread_key_t *key

);

// 该函数用于删除一个由pthread_key_create 函数调用创建的TSD键。调用成功返回值为0,否则返回错误代码。

=============================================================

int pthread_setspecific(

pthread_key_t key ,

const void(value)

);

// 该函数设置一个线程专有数据的值,赋给由pthread_key_create 创建的TSD 键,调用成功返回值为0,否则返回错误代码。

=============================================================

void*pthread_getspecific(

pthread_key_t *key

);

// 该函数获得绑定到指定TSD 键上的值。调用成功,返回给定参数key 所对应的数据。如果没有数据连接到该TSD 键,则返回NULL。

=============================================================

int pthread_once(

pthread_once_t* once_control,

void(*init_routine)(void)

);

//该函数的作用是确保init_routine 指向的函数,在调用pthread_once的线程中只被运行一次。

//once_control 指向一个静态或全局的变量。

=============================================================

在code review中,我会发现很多人喜欢在pthread_mutex_lock()和pthread_mutex_unlock(()之间调用 pthread_cond_signal或者pthread_cond_broadcast函数,从逻辑上来说,这种使用方法是完全正确的。但是在多线程 环境中,这种使用方法可能是低效的。posix1标准说,pthread_cond_signal与pthread_cond_broadcast无需考 虑调用线程是否是mutex的拥有者,也就是所,可以在lock与unlock以外的区域调用。如果我们对调用行为不关心,那么请在lock区域之外调用 吧。这里举个例子:我们假设系统中有线程1和线程2,他们都想获取mutex后处理共享数据,再释放mutex。请看这种序列:

1)线程1获取mutex,在进行数据处理的时候,线程2也想获取mutex,但是此时被线程1所占用,线程2进入休眠,等待mutex被释放。

2)线程1做完数据处理后,调用pthread_cond_signal()唤醒等待队列中某个线程,在本例中也就是线程2。线程1在调用 pthread_mutex_unlock()前,因为系统调度的原因,线程2获取使用CPU的权利,那么它就想要开始处理数据,但是在开始处理之 前,mutex必须被获取,很遗憾,线程1正在使用mutex,所以线程2被迫再次进入休眠。

3)然后就是线程1执行pthread_mutex_unlock()后,线程2方能被再次唤醒。

从这里看,使用的效率是比较低的,如果再多线程环境中,这种情况频繁发生的话,是一件比较痛苦的事情。

所以觉得,如果程序不关心线程可预知的调度 行为,那么最好在锁定区域以外调用他们吧:-)

这 里罗嗦几句,对于

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);

,一定要在mutex的锁定区域内使用。


如果要正确的使用pthread_mutex_lock与pthread_mutex_unlock,请参考

pthread_cleanup_push 和pthread_cleanup_pop宏,它能够在线程被cancel的时候正确的释放mutex!

PTHREAD_COND(3)

//名称

pthread_cond_init, pthread_cond_destroy, pthread_cond_signal,

pthread_cond_broadcast, pthread_cond_wait, pthread_cond_timedwait - 状态操作。

//大纲

#include

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,conststruct timespec *abstime);

int pthread_cond_destroy(pthread_cond_t *cond);

描述

状态变量是一种同步设备,它允许线程挂起并放弃CPU时间以等待某些共享变量满足状态。操作状态变量的基本操作:当状态满足时发送信号,等待状态满足,在其它线程发送状态信号之前挂起线程。

状态变量通常和互斥体联系在一起,为了避免竞争状态出现,一个线程准备等待一个状态变量之前另外一个线程要先设置好状态信号。


pthread_cond_init初始化状态变量cond,使用cond_attr中定义了的状态属性,如果cond_attr为NULL,将会使用默认属性。LinuxThreads的实现支持没有任何属性的cond_attr,因此,cond_attr就是被忽略的。

pthread_cond_t类型的变量也可以使用PTHREAD_COND_INITIALIZER.常量进行静态的初始化。

pthread_cond_signal函数重新开始一个正在等待cond变量的线程。如果没有线程在等待cond变量,不执行任何操作。如果有多个线程都在等待,某个匹配的线程会被重新开始,但是不一定是哪个。

pthread_cond_broadcast函数重新开始所有在等待cond变量的线程。如果没有线程在等待cond变量,不执行任何操作。

pthread_cond_wait函数对互斥体进行原子的解锁工作(就像pthread_unlock_mutex),然后等待状态信号。线程被挂起并不消耗CPU时间,直到发送状态信号。互斥体必须被调用者锁定。在返回调用线程之前,互斥锁被pthread_cond_wait拥有。


释放互斥体和在状态变量上挂起是自动进行的。因此,如果所有的线程经常在状态信号之前要求互斥体,这会保证在线程在状态变量上锁定互斥体的期间状态变量不会触发信号。


pthread_cond_timedwait函数自动释放互斥体并且等待cond状态,就像pthread_cond_wait所做的一样,但是它限制了最大的等待时间。如果cond没有在一定时间内用abstime中没有指定的时间做标记,互斥体会重新获得,然后返回错误码ETIMEOUT。abstime参数指定绝对时间,和time(2)和gettimeofday(2)的一样:以格林威治时间1970年1月1日零点为起点。

pthread_cond_destroy函数销毁状态变量,释放它可能持有的资源。没有线程必须在这里等待状态变量。在LinuxThreads实现中,状态变量不与任何资源有关系,所以这个接口除了检查状态变量上是否有等待的线程之外不做任何事。


取消

pthread_cond_wait和pthread_cond_timedwait都是取消点。如果某个线程在某个这样的函数中挂起后又被取消,该线程会重新开始执行,然后再锁定互斥体。(尚未完成!)


同步信号安全

状态函数不是同步安全的,不应该出现在信号处理函数中。特别的,从信号处理函数中调用pthread_cond_signal或pthread_cond_broadcast会导致死锁。


返回值

所有状态变量函数在成功的时候返回0,在出错的时候返回出错码。


错误码

pthread_cond_init, pthread_cond_signal, pthread_cond_broadcast和pthread_cond_wait从来都不会返回出错码。

pthread_cond_timedwait函数在出错时返回下面的出错码:

ETIMEOUT:状态变量在abstime限定时间内没有激发信号。

EINTR:pthread_cond_timedwait被信号中断。

pthread_cond_destroy函数在出错时返回下面的出错码:

EBUSY:有线程正在cond上等待。


互斥量从本质上来说是一个锁,对互斥量加锁后任何其他试图给它加锁的线程都会被阻塞直至当前线程释放互斥量。

同样在设计时需要规定所有的线程必须遵守相同的数据访问规则,只有这样互斥机制才能正常工作(只要是锁都这样,这是锁工作的本质)


互斥量用pthread_mutex_t 数据类型表示,在使用互斥量之前我们必须对他进行初始话

pthread_mutex_init()函数可以帮你完成,同样释放前要调用pthread_mutex_destroy()函数。


对互斥量的操作:

int pthread_mutex_lock(); 对互斥量加锁,如若获取不到,则阻塞

int pthread_mutex_trylock();对互斥量加锁的非阻塞版本

int pthread_mutex_unlock();对互斥量解锁。


同样线程锁也存在死锁问题,这种低级问题不再讨论,下面重点说下条件变量:

开始条件变量把我也是搞的云里雾里,总感觉所谓的条件变量没有被用到,经过仔细的阅读终于感悟出其中真谛之一二


首先看一下数据类型及相关函数:

pthread_cond_t 数据结构用来定义条件变量的,同样他也有初始化和释放:

pthread_coud_init();

pthread_coud_destroy();


好了,条件变量要和互斥量一起使用,所以一般情况下会申明2个数据类型,在初始化他们2个,这个就不解释了

pthread_mutex_t count_lock;

pthread_cond_t count_ready;

pthread_mutex_init(&count_lock, NULL);

pthread_cond_init(&count_ready, NULL);


这四步是起手动作,就像70年代的防骑,起手3个buf,然后拿起盾向怪砸去。

起手动作做完了,就来分析一下条件变量的作用吧,其实这里的条件变量不是真正用来判断的条件,一般会定义一个变量

配合着while死循环来完成阻塞,而他本省则像一个桥梁,他通过这个桥梁等待另一个线程发送的信息,我们通过代码来分析一下,


void *A(void *arg)

{

pthread_mutex_lock(&count_lock);//在调用pthread_coud_wait之前必须先锁住互斥

/*等待满足条件,期间互斥量仍然可用*/

while(count == 0)

k = pthread_cond_wait(&count_ready, &count_lock);

//这个函数理解是关键,当这个函数调用的时候他会做2件事情,第一:他会把调用它的线程放到等待条件的线

//列表上,(即通过条件变量传递消息的线程列表,可能多个线程同时等待)第二:对互斥量解锁(这个很关

//哦,很多人不理解这个函数,就是因为不知到这个步骤),此时调用完后,线程就会阻塞在这里,知道通过

//件变量传过来的解锁信号:pthread_coud_signal or pthread_coud_broadcast (2者的区别不多解释了)

printf("decrement :k = %d cout = %d%s\n", k, count, strerror(k)); //打印返回值的类型,看是否成

count = count - 1;

printf("decrement:count = %d\n", count);

pthread_mutex_unlock(&count_lock);

pthread_exit(NULL);

}

从A线程来看,他已经基本死在那里了,除非有其他线程发送信号过来,下面B线程粉末登场:

void *B(void *arg)

{

intk;

pthread_mutex_lock(&count_lock);// 这句话看似平常 其实如果不理解A中所说,人然不知所意

//从A线程上看我们知道他在阻塞时已经对互斥量解锁了,所以这里才能加锁不然的话,B也要在这里死翘翘了

sleep(3);

睡眠3秒,这样容易分析结果

count = count + 1;//这个才是解除A线程死循环的关键,当然不是解除阻塞,阻塞还是得我们的条件变量上

//通知线程条件已满足,下面的2步顺序很关键,不然你可以打印A线程返回的K的错误类型

//会报time out,原因就是没有先解锁在发送信号*

pthread_mutex_unlock(&count_lock);

pthread_cond_signal(&count_ready);

printf("increment:count = %d\n", count);

pthread_exit(NULL);

}

互斥量:本质就是一把锁,所有锁类型都可能会产生死锁,所以在使用互斥量时程序员必须在逻辑上附加避免死锁的措施,这个典型避免死锁的措施就是:通过元素排序预防死锁,即多线程在互斥资源上必须按相同顺序加锁和解锁。

条件变量:必须与互斥量配合使用,它可以以无竞争的方式访问资源,即,条件变量本身会预防死锁的产生。

一部分朋友觉得用锁会影响性能,其实锁指令本身很简单,影响性能的是锁争用(Lock Contention),什么叫锁争用,就是你我都想进入临界区,但只能有一个线程能进去,这样就影响了并发度。可以去看看glibc中pthread_mutex_lock的源码实现,在没有contention的时候,就是一条CAS指令,内核都没有陷入;在contention发生的时候,就选择陷入内核然后睡觉,等待某个线程unlock后唤醒(详见Futex)。

“只有一个线程在临界区”这件事对lockfree也是成立的,只不过所有线程都可以进临界区,最后只有一个线程可以make progress,其它线程再做一遍。

所以contention在有锁和无锁编程中都是存在的,那为什么无锁有些时候会比有锁更快?他们的不同体现在拿不到锁的态度:有锁的情况就是睡觉,无锁的情况就不断spin。睡觉这个动作会陷入内核,发生context switch,这个是有开销的,但是这个开销能有多大呢,当你的临界区很小的时候,这个开销的比重就非常大。这也是为什么临界区很小的时候,换成lockfree性能通常会提高很多的原因。

再来看lockfree的spin,一般都遵循一个固定的格式:先把一个不变的值X存到某个局部变量A里,然后做一些计算,计算/生成一个新的对象,然后做一个CAS操作,判断A和X还是不是相等的,如果是,那么这次CAS就算成功了,否则再来一遍。如果上面这个loop里面“计算/生成一个新的对象”非常耗时并且contention很严重,那么lockfree性能有时会比mutex差。另外lockfree不断地spin引起的CPU同步cacheline的开销也比mutex版本的大。

lockfree的意义不在于绝对的高性能,它比mutex的优点是使用lockfree可以避免死锁/活锁,优先级翻转等问题。但是因为ABA problem、memory order等问题,使得lockfree比mutex难实现得多。

除非性能瓶颈已经确定,否则还是乖乖用mutex+condvar,等到以后出bug了就知道mutex的好了。如果一定要换lockfree,请一定要先profile,profile,profile!请确保时间花在刀刃上。


发布时间: 9年前后端开发136人已围观返回回到顶端

很赞哦! (1)

文章评论

  • 请先说点什么
    热门评论
    135人参与,0条评论

站点信息

  • 建站时间:2016-04-01
  • 文章统计:728条
  • 文章评论:82条
  • QQ群二维码:扫描二维码,互相交流