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

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

上一篇:只有有lua编译能力不足200K代码吧?NO! Python 有可能。


下一篇:四十二、Linux 线程——线程同步之条件变量之线程状态转换