Linux同步机制(二) - 条件变量,信号量,文件锁,栅栏

1 条件变量

条件变量是一种同步机制,允许线程挂起,直到共享数据上的某些条件得到满足。

1.1 相关函数

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

1.2 说明

1. 条件变量是一种同步机制,允许线程挂起,直到共享数据上的某些条件得到满足。条件变量上的基本操作有:触发条件(当条件变为true时);等待条件,挂起线程直到其他线程触发条件。 2. 条件变量要和互斥量相联结,以避免出现条件竞争--一个线程预备等待一个条件变量,当它在真正进入等待之前,另一个线程恰好触发了该条件。 pthread_cond_init 使用 cond_attr指定的属性初始化条件变量 cond,当 cond_attr为 NULL 时,使用缺省的属性。LinuxThreads实现条件变量不支持属性,因此 cond_attr参数实际被忽略。 pthread_cond_t 类型的变量也可以用 PTHREAD_COND_INITIALIZER常量进行静态初始化。 pthread_cond_signal 使在条件变量上等待的线程中的一个线程重新开始。如果没有等待的线程,则什么也不做。如果有多个线程在等待该条件,只有一个能重启动,但不能指定哪一个。 pthread_cond_broadcast 重启动等待该条件变量的所有线程。如果没有等待的线程,则什么也不做。 pthread_cond_wait 自动解锁互斥量(如同执行了 pthread_unlock_mutex),并等待条件变量触发。这时线程挂起,不占用 CPU 时间,直到条件变量被触发。在调用 pthread_cond_wait 之前,应用程序必须加锁互斥量。pthread_cond_wait 函数返回前,自动重新对互斥量加锁(如同执行了 pthread_lock_mutex)。 互斥量的解锁和在条件变量上挂起都是自动进行的。因此,在条件变量被触发前,如果所有的线程都要对互斥量加锁,这种机制可保证在线程加锁互斥量和进入等待条件变量期间,条件变量不被触发。 pthread_cond_timedwait 和 pthread_cond_wait一样,自动解锁互斥量及等待条件变量,但它还限定了等待时间。如果在 abstime指定的时间内 cond 未触发,互斥量 mutex被重新加锁,且 pthread_cond_timedwait返回错误 ETIMEDOUT。abstime参数指定一个绝对时间,时间原点与 time和 gettimeofday相同:abstime = 0表示 1970年 1月 1日 00:00:00 GMT。 pthread_cond_destroy 销毁一个条件变量,释放它拥有的资源。进入 pthread_cond_destroy之前,必须没有在该条件变量上等待的线程。在 LinuxThreads的实现中,条件变量不联结资源,除检查有没有等待的线程外,pthread_cond_destroy实际上什么也不做。 3. 取消 pthread_cond_wait 和 pthread_cond_timedwait是取消点。如果一个线程在这些函数上挂起时被取消,线程立即继续执行,然后再次对 pthread_cond_wait和 pthread_cond_timedwait在 mutex参数加锁,最后执行取消。因此,当调用清除处理程序时,可确保,mutex是加锁的。 4. 异步信号安全(Async-signalSafety)                                                                                          条件变量函数不是异步信号安全的,不应当在信号处理程序中进行调用。特别要注意,如果在信号处理程序中调用 pthread_cond_signal或 pthread_cond_boardcast函数,可能导致调用线程死锁。 5. 返回值

在执行成功时,所有条件变量函数都返回 0,错误时返回非零的错误代码。 6. 错误代码 pthread_cond_init,   pthread_cond_signal, pthread_cond_broadcast,和 pthread_cond_wait从不返回错误代码。 pthread_cond_timedwait 函数出错时返回下列错误代码: ETIMEDOUT   abstime 指定的时间超时时,条件变量未触发 EINTR       pthread_cond_timedwait被触发中断 pthread_cond_destroy 函数出错时返回下列错误代码: EBUSY       某些线程正在等待该条件变量 7. 举例 设有两个共享的变量 x 和 y,通过互斥量 mut保护,当 x > y时,条件变量 cond被触发。

 int x,y;
 int x,y;
 pthread_mutex_t mut = PTHREAD_MUTEX_INITIALIZER;
 pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

等待直到 x > y 的执行流程:

 pthread_mutex_lock(&mut);
 while (x <= y) {
     pthread_cond_wait(&cond, &mut);
 }
 /* 对 x、y进行操作 */
 pthread_mutex_unlock(&mut);
 

对 x 和 y的修改可能导致 x > y,应当触发条件变量:

 pthread_mutex_lock(&mut);
 /* 修改 x、y */
 if (x > y) pthread_cond_broadcast(&cond);
 pthread_mutex_unlock(&mut);

如果能够确定最多只有一个等待线程需要被唤醒(例如,如果只有两个线程通过 x、y通信),则使用 pthread_cond_signal比 pthread_cond_broadcast 效率稍高一些。如果不能确定,应当用pthread_cond_broadcast。

要等待在 5 秒内 x > y,这样处理:

 struct timeval now;
 struct timespec timeout;
 int retcode;
 
 pthread_mutex_lock(&mut);
 gettimeofday(&now);
 timeout.tv_sec = now.tv_sec + 5;
 timeout.tv_nsec = now.tv_usec * 1000;
 retcode = 0;
 while (x <= y && retcode != ETIMEDOUT) {
     retcode =pthread_cond_timedwait(&cond, &mut, &timeout);
 }
 if (retcode == ETIMEDOUT) {
     /* 发生超时 */
 } else {
     /* 操作 x 和  y */
 }
 pthread_mutex_unlock(&mut);

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

2 信号量

Semaphore,即信号量(sem_init),用来保护多重资源的访问,它可以设置一个大于0的值,如N,任何访问者在该信号量大于1的情况下均可以获得资源的访问权,并将相应的信号量减1。一般在为了线程在某一定程度上的顺序执行才使用信号量,即线程A等待线程B执行完某些操作以后,才能继续往下执行,可以理解为,组装厂A需要等待(sem_wait)元件厂B交付元件以后(sem_post)才能继续生产。当信号量不再使用时,销毁它(sem_destroy)。

2.1 相关函数

sem_init(sem_t *sem, int pshared, unsignedint value):初始化一个信号量

sem_wait(sem_t *sem):一直等待信号量,直到信号量大于0

int sem_trywait(sem_t *sem):等待信号量,没有成功立即返回

sem_timedwait(sem_t *sem, const structtimespec *abs_timeout):设定超时等待。

sem_post(sem_t *sem):信号量加1

sem_destory(sem_t *sem):释放信号量。

2.2 代码讲解:信号量的使用

#include <stdio.h> 

#include <stdlib.h> 

#include <unistd.h>

#include <pthread.h> 

#include <semaphore.h> 

 

sem_t binSem;

 

void* helloWorld(void* arg) 

{

    while(1) 

    { 

       // Wait semaphore

       sem_wait(&binSem);

       printf("Hello World\n");

    }

}

 

int main(int argc, char** argv)
{

    // Result for System call

    int res = 0;

 

    // Initialize semaphore

    sem_init(&binSem, 0, 0);

 

    // Create thread

    pthread_t thdHelloWorld;

    pthread_create(&thdHelloWorld, NULL, helloWorld, NULL);

 

    while(1) 

    {

       // Post semaphore

       sem_post(&binSem);

       printf("In main, sleep several seconds.\n");

       sleep(1);

    }

 

   // Wait for thread synchronization

   void *threadResult;

   pthread_join(thdHelloWorld, &threadResult);

 

   return 0;

}

结果说明:

[root@rocket lock-free]# ./pthread_sem

In main, sleep several seconds.

Hello World

In main, sleep several seconds.

Hello World

In main, sleep several seconds.

Hello World

In main, sleep several seconds.

Hello World

In main, sleep several seconds.

Hello World

3 文件锁

3.1 文件锁介绍

linux下可以使用flock函数对文件进行加锁解锁等操作。简单介绍下flock()函数:

表头文件#include <sys/file.h>

定义函数 int flock(int fd,int operation);

函数说明 flock()会依参数operation所指定的方式对参数fd所指的文件做各种锁定或解除锁定的动作。此函数只能锁定整个文件,无法锁定文件的某一区域。

参数 operation有下列四种情况:

LOCK_SH 建立共享锁定。多个进程可同时对同一个文件作共享锁定。

LOCK_EX 建立互斥锁定。一个文件同时只有一个互斥锁定。

LOCK_UN 解除文件锁定状态。

LOCK_NB 无法建立锁定时,此操作可不被阻断,马上返回进程。通常与LOCK_SH或LOCK_EX做OR(|)组合。

单一文件无法同时建立共享锁定和互斥锁定,而当使用dup()或fork()时文件描述词不会继承此种锁定。

返回值返回0表示成功,若有错误则返回-1,错误代码存于errno。

3.2 代码讲解:文件锁的使用

file_lock_a.cpp

#include <stdlib.h>

#include <stdio.h>

#include <unistd.h>

#include <sys/file.h>

 

int main(int argc, char** argv)

{

   FILE *fp = NULL;

   int i = 20; 

   if ((fp = fopen("./file_lock.test", "r+b")) == NULL)//打开文件

   {

       printf("file open error!\n");

       exit(0);

   }

   if (flock(fp->_fileno, LOCK_EX) != 0) //给该文件加锁

       printf("file lock by others\n");

   while(1) //进入循环,加锁时间为20秒,打印倒计时

   {   

       printf("in a, %d\n", i--);

       sleep(1);

       if (i == 0)

           break;

   }   

   fclose(fp); //20秒后退出,关闭文件

   flock(fp->_fileno, LOCK_UN); //文件解锁

   return 0;

}

file_lock_b.cpp

#include <stdlib.h>

#include <stdio.h>

#include <unistd.h>

#include <sys/file.h>

 

int main(int argc, char** argv)

{

   FILE *fp = NULL;

   int i = 20; 

   if ((fp = fopen("./file_lock.test", "r+b")) == NULL)

   {

       printf("file open error!\n");

       exit(0);

   }

   flock(fp->_fileno, LOCK_EX);

   while(1) //进入循环,加锁时间为20秒,打印倒计时

   {   

       printf("in b, %d\n", i--);

       sleep(1);

       if(i == 0)

           break;

   }   

   fclose(fp); //20秒后退出,关闭文件

   flock(fp->_fileno, LOCK_UN); //文件解锁

   return 0;

}

结果说明:

先创建文件touch file_lock.test

先运行file_lock_a,20秒以内在另一个终端运行file_lock_b,可以看到file_lock_a打印结束了file_lock_b才开始打印的现象。

4 栅栏

4.1 相关函数

pthread_barrier 系列函数在<pthread.h>中定义,用于多线程的同步,它包含三个函数:

#include <pthread.h>

int pthread_barrier_init(pthread_barrier_t*restrict barrier, const pthread_barrierattr_t *restrict attr, unsigned count);

int pthread_barrier_wait(pthread_barrier_t*barrier);

intpthread_barrier_destroy(pthread_barrier_t *barrier);

参数解释:

pthread_barrier_t,是一个计数锁,对该锁的操作都包含在三个函数内部,我们不用关心也无法直接操作。只需要实例化一个对象丢给它就好。

pthread_barrierattr_t,锁的属性设置,设为NULL让函数使用默认属性即可。

count,你要指定的等待个数。

4.2 功能说明

那么pthread_barrier_*是用来做什么的?这三个函数又怎么配合使用呢?

pthread_barrier_*其实只做且只能做一件事,就是充当栏杆(barrier意为栏杆)。形象的说就是把先后到达的多个线程挡在同一栏杆前,直到所有线程到齐,然后撤下栏杆同时放行。

1)init函数负责指定要等待的线程个数;

2)wait()函数由每个线程主动调用,它告诉栏杆“我到起跑线前了”。wait()执行末尾栏杆会检查是否所有人都到栏杆前了,如果是,栏杆就消失所有线程继续执行下一句代码;如果不是,则所有已到wait()的线程停在该函数不动,剩下没执行到wait()的线程继续执行;

3)destroy函数释放init申请的资源。

4.3 使用场景举例

这种“栏杆”机制最大的特点就是最后一个执行wait的动作最为重要,就像赛跑时的起跑枪一样,它来之前所有人都必须等着。所以实际使用中,pthread_barrier_*常常用来让所有线程等待“起跑枪”响起后再一起行动。比如我们可以用pthread_create()生成100个线程,每个子线程在被create出的瞬间就会自顾自的立刻进入回调函数运行。但我们可能不希望它们这样做,因为这时主进程还没准备好,和它们一起配合的其它线程还没准备好,我们希望它们在回调函数中申请完线程空间、初始化后停下来,一起等待主进程释放一个“开始”信号,然后所有线程再开始执行业务逻辑代码。

解决方案:

为了解决上述场景问题,我们可以在init时指定n+1个等待,其中n是线程数。而在每个线程执行函数的首部调用wait()。这样100个pthread_create()结束后所有线程都停下来等待最后一个wait()函数被调用。这个wait()由主进程在它觉得合适的时候调用就好。最后这个wait()就是鸣响的起跑枪。

4.4 代码讲解:barrier的使用

#include <stdio.h>

#include <pthread.h>

#include <unistd.h>

#include <stdlib.h>

 

pthread_barrier_t barrier;

int thread_num = 10;

 

void* doSomething(void* arg)

{

         printf("beforewait %d\n", pthread_self());

         pthread_barrier_wait(&barrier);//所有线程都被阻塞在这里

         printf("I'min pthread %d\n", pthread_self());

         

         returnNULL;

}

 

int activate()

{ 

         //等一切都安排好了调用该函数。起跑枪“砰!”

         pthread_barrier_wait(&barrier);

         return0;

}

 

int main(int argc, char** argv)

{

         pthread_attr_tattr;

         pthread_attr_init(&attr);

         

         pthread_t*thread = (pthread_t*)malloc(thread_num * sizeof(pthread_t));

         pthread_barrier_init(&barrier,NULL, thread_num + 1);

         for(inti = 0; i < thread_num; i++)

         {

                   pthread_create(thread+i,&attr, doSomething, NULL);

         }

         

         activate();

         

         for(int i = 0; i < thread_num; i++)

         {

                   pthread_join(*(thread+i),NULL);

         }

 

         pthread_attr_destroy(&attr);

         

         return0;

}

代码说明:

无栅栏时候的运行结果(注释掉doSomething和activate中的pthread_barrier_wait)

before wait 169158400

I'm in pthread 169158400

before wait 158668544

I'm in pthread 158668544

before wait 190138112

I'm in pthread 190138112

before wait 179648256

I'm in pthread 179648256

before wait 148178688

I'm in pthread 148178688

before wait 137688832

I'm in pthread 137688832

before wait 127198976

I'm in pthread 127198976

before wait 106219264

I'm in pthread 106219264

before wait 116709120

I'm in pthread 116709120

before wait 95729408

I'm in pthread 95729408

有栅栏时候的运行结果

[root@rocket lock-free]# ./pthread_barrier

before wait 2137720576

before wait 2106251008

before wait 2127230720

before wait 2085271296

before wait 2095761152

before wait 2053801728

before wait 2074781440

before wait 2116740864

before wait 2043311872

before wait 2064291584

I'm in pthread 2127230720

I'm in pthread 2137720576

I'm in pthread 2095761152

I'm in pthread 2106251008

I'm in pthread 2085271296

I'm in pthread 2064291584

I'm in pthread 2053801728

I'm in pthread 2074781440

I'm in pthread 2043311872

I'm in pthread 2116740864

版权声明:本文为博主原创文章,未经博主允许不得转载。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Felix的技术分享

ELF文件及android hook原理

89080
来自专栏Coding01

看 Laravel 源代码了解 Container

自从上文《看 Laravel 源代码了解 ServiceProvider 的加载》,我们知道 Application (or Container) 充当 Lar...

41650
来自专栏python学习之旅

Python笔记(十四):操作excel openpyxl模块

48330
来自专栏编程心路

小司机带你学习单例模式的六种姿势!

单例模式是创建型模式的一种,下面总结一下在 Java 中实现单例模式的几种方法,并在多线程环境中进行了测试。

15330
来自专栏程序小工

【实战】Tp5+小程序(一)--数据库访问与ORM

ThinkPHP5 从入门到深入学习,结合实战项目深入理解 ThinkPHP5 的特性和使用方法,了解 ThinkPHP5 的数据库访问和 ORM 思想,学习使...

60720
来自专栏开发与安全

linux网络编程之socket(六):利用recv和readn函数实现readline函数

在前面的文章中,我们为了避免粘包问题,实现了一个readn函数读取固定字节的数据。如果应用层协议的各字段长度固定,用readn来读是非常方便的。例如设计一种客户...

28000
来自专栏PHP在线

了解PHP中Stream(流)的概念与用法

Stream是PHP开发里最容易被忽视的函数系列(SPL系列,Stream系列,pack函数,封装协议)之一,但其是个很有用也很重要的函数。Stream可以翻译...

51850
来自专栏fixzd

[代码结构设计]根据不同条件使用不同实现类的业务代码设计

这样大家可能不是太理解。举个例子,现在大街小巷上的商户都采用了聚合支付的支付方式,聚合支付也就是商户柜台前放了一个支持支付宝、微信、京东钱包、银联等等的二维码,...

14040
来自专栏Python、Flask、Django

TP踩过的坑【批量删除,(不涉及子栏目的批量删除)】

12920
来自专栏orientlu

Google 单元测试框架

到 github 拉取代码或者下载某个版本的 zip 包到本地目录,参考 gtest 中的 README.md 如何编译库和编译自己的代码,下面简单介绍下编译方...

22020

扫码关注云+社区

领取腾讯云代金券