首页 > 技术文章 > Linux系统编程3_条件变量与互斥锁

grooovvve 2020-05-14 00:10 原文

例子:

生产者,消费者问题;
消费者先进入临界区,条件变量未满足条件,阻塞等待;
生产者无法进入临界区,从而无法修改条件变量,也就产生死锁;

解决方法:
如果遇到条件变量未满足条件,消费者先释放锁,进入阻塞,等待条件变量得到满足;
然后生产者可以进入临界区修改条件变量,修改后通知消费者进入临界区,生产者释放锁
消费者接收到通知申请锁,得到锁后,发现条件变量得到满足,开始消费,消费完毕释放锁

流程结束,完美解决生产者消费者同步的问题;

这就是条件变量+互斥锁的应用;

 

mutex体现的是一种竞争,我离开了,通知你进来。
cond体现的是一种协作,我准备好了,通知你开始吧。



条件变量

条件变量使我们可以睡眠等待某种条件出现。

条件变量是利用线程间共享的全局变量,进行同步的一种机制,

主要包括两个动作:一个线程等待"条件变量的条件成立"而挂起;

另一个线程使"条件成立"(给出条件成立信号)。为了防止竞争,条件变量的使用总是和一个互斥锁结合在一起。

 

条件变量创建:

条件变量和互斥锁一样,都有静态动态两种创建方式;
静态方式使用PTHREAD_COND_INITIALIZER常量 pthread_cond_t cond = PTHREAD_COND_INITIALIZER
动态方式调用函数int pthread_cond_init
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);
pthread_cond_signal函数的作用是发送一个信号给另外一个正在处于阻塞等待状态的线程,使其脱离阻塞状态,
继续执行,如果没有线程处在阻塞等待状态,pthread_cond_signal也会成功返回。

int pthread_cond_broadcast(pthread_cond_t *cond);
可以唤醒所有wait该cond的线程;

 



互斥锁的作用
保护共享数据: 在并发机制的情况下,有时候会有多个线程同时访问同一片数据,为了保护数据操作的准确性就需要通过加锁来进行保护。
保持操作互斥: 可能一个程序会有多个操作,但是同一个时间只能有一个操作被执行,例如a/b两个操作,如果a被执行,b就不能被执行,同理b被执行,a就不能执行

pthread_mutex_t lock; /* 互斥锁定义 */
pthread_mutex_init(&lock, NULL); /* 动态初始化, 成功返回0,失败返回非0 */
pthread_mutex_t thread_mutex = PTHREAD_MUTEX_INITIALIZER; /* 静态初始化 */
pthread_mutex_lock(&lock); /* 阻塞的锁定互斥锁 */
pthread_mutex_trylock(&thread_mutex);/* 非阻塞的锁定互斥锁,成功获得互斥锁返回0,如果未能获得互斥锁,立即返回一个错误码 */
pthread_mutex_unlock(&lock); /* 解锁互斥锁 */
pthread_mutex_destroy(&lock) /* 销毁互斥锁  但这有一个前提当前是没有被锁的状态*/

//获取或设置锁的类型的函数
pthread_mutexattr_settype(pthread_mutexattr_t *attr , int type)
pthread_mutexattr_gettype(pthread_mutexattr_t *attr , int *type)

 

互斥锁的类型:
  PTHREAD_MUTEX_TIMED_NP,这是缺省值,也就是普通锁。当一个线程加锁以后,其余请求锁的线程将形成一个等待队列,并在解锁后按优先级获得锁。这种锁策略保证了资源分配的公平性。
  PTHREAD_MUTEX_RECURSIVE_NP,嵌套锁,允许同一个线程对同一个锁成功获得多次,并通过多次unlock解锁。如果是不同线程请求,则在加锁线程解锁时重新竞争。
  PTHREAD_MUTEX_ERRORCHECK_NP,检错锁,如果同一个线程请求同一个锁,则返回EDEADLK,否则与PTHREAD_MUTEX_TIMED_NP类型动作相同。这样就保证当不允许多次加锁时不会出现最简单情况下的死锁。
  PTHREAD_MUTEX_ADAPTIVE_NP,适应锁,动作最简单的锁类型,仅等待解锁后重新竞争。



 

pthread_key_t
线程私有存储空间--pthread_key_t:https://blog.csdn.net/yusiguyuan/article/details/21785641
在单线程程序中,我们经常要用到"全局变量"以实现多个函数间共享数据。在多线程环境下,由于数据空间是共享的,因此全局变量也为所有线程所共有。
但有时应用程序设计中有必要提供线程私有的全局变量,仅在某个线程中有效,但却可以跨多个函数访问,比如程序可能需要每个线程维护一个链表,而使用相同的函数操作,
最简单的办法就是使用同名而不同变量地址的线程相关数据结构。这样的数据结构可以由Posix线程库维护,称为线程私有数据(Thread- specific Data,或TSD)。
熟悉linux线程开发的人都清楚,一个进程中线程直接除了线程自己的栈和寄存器之外,其他几乎都是共享的,如果线程想维护一个只属于线程自己的全局变量怎么办?线程的私有存储解决了这个问题。

 

pthread_once_t
深入Pthread(四):一次初始化-pthread_once_t:https://www.cnblogs.com/mywolrd/archive/2009/02/16/1930699.html
有些事需要一次且仅需要一次执行。通常当初始化应用程序时,可以比较容易地将其放在main函数中。但当你写一个库时,就不能在main里面初始化了,你可以用静态初始化,但使用一次初始化(pthread_once_t)会比较容易些。

 



多线程
pthread_t 用于声明线程ID unsigned long int
4,(不同环境大小不一)x86_64=8


多线程相关函数介绍:

线程创建//pthread_create是UNIX环境创建线程函数
原型:
int pthread_create(pthread_t *restrict tidp,const pthread_attr_t *restrict_attr,void*(*start_rtn)(void*),void *restrict arg);
若成功则返回0,否则返回出错编号;
第一个参数为指向线程标识符的指针。
第二个参数用来设置线程属性。
第三个参数是线程运行函数的地址。
最后一个参数是运行函数的参数。

在编译时注意加上-lpthread参数,以调用静态链接库。因为pthread并非Linux系统的默认库。

 


函数pthread_join用来等待一个线程的结束。
extern int pthread_join __P (pthread_t __th, void **__thread_return);
第一个参数为被等待的线程标识符
第二个参数为一个用户定义的指针,它可以用来存储被等待线程的返回值。
pthread_join()函数,以阻塞的方式等待thread指定的线程结束。当函数返回时,被等待线程的资源被收回。
如果线程已经结束,那么该函数会立即返回。并且thread指定的线程必须是joinable的。
返回值 : 0代表成功。 失败,返回的则是错误号。

另外需要说明的是,一个线程不能被多个线程等待,
也就是说对一个线程只能调用一次pthread_join,否则只有一个能正确返回,其他的将返回ESRCH 错误。

在Linux中,默认情况下是在一个线程被创建后,必须使用此函数对创建的线程进行资源回收,
但是可以设置Threads attributes来设置当一个线程结束时,直接回收此线程所占用的系统资源,
详细资料查看Threads attributes。

 

线程属性,使用pthread_attr_t类型表示;需要对此结构体进行初始化;
初始化后使用,使用后还要进行去除初始化!
int pthread_attr_init(pthread_attr_t *attr);
int pthread_attr_destroy(pthread_attr_t *attr);
若成功返回0,若失败返回-1。

pthread_create 创建线程时,若不指定分配堆栈大小,系统会分配默认值
int pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize);
attr 是线程属性变量;stacksize 则是设置的堆栈大小。 返回值0,-1分别表示成功与失败。

 


线程私有变量://一键多值,表面上是全局变量,实际上是线程私有的数据空间;
int pthread_key_create(pthread_key_t *key, void (*destructor)(void*));
第二个参数是一个清理函数,用来在线程释放该线程存储的时候被调用。该函数指针可以设成 NULL ,这样系统将调用默认的清理函数。

int pthread_setspecific(pthread_key_t key, const void *value);
当线程中需要存储特殊值的时候,可以调用 pthread_setspcific() 。
该函数有两个参数,第一个为前面声明的pthread_key_t变量,第二个为void*变量,这样你可以存储任何类型的值。

如果需要取出所存储的值,调用pthread_getspecific()。
该函数的参数为前面提到的pthread_key_t变量,该函数返回void *类型的值。
void *pthread_getspecific(pthread_key_t key);

 

 



参考资料

 

 



 

推荐阅读