Linux程序设计学习笔记----多线程编程之线程同步之条件变量

转载请注明出处:http://blog.csdn.net/suool/article/details/38582521.

基本概念与原理

互斥锁能够解决资源的互斥访问,但是在某些情况下,互斥并不能解决问题,比如两个线程需 要互斥的处理各自的操作,但是一个线程的操作仅仅存在一种条件成立的情况下执行,一旦错过不可再重现,由于线程间相互争夺cpu资源,因此在条件成立的时候,该线程不一定争夺到cpu而错过,导致永远得不到执行.....

因此需要某个机制来解决此问题,更重要的是,线程仅仅只有一种情况需要执行操作,不停地申请资源导致资源浪费,而Linux提供了条件变量加互斥锁解决该问题.

条件变量变量也是出自POSIX线程标准,另一种线程同步机制,。主要用来等待某个条件的发生。可以用来同步同一进程中的各个线程。当然如果一个条件变量存放在多个进程共享的某个内存区中,那么还可以通过条件变量来进行进程间的同步。

每个条件变量总是和一个互斥量相关联,条件本身是由互斥量保护的,线程在改变条件状态之间必须要锁住互斥量。条件变量相对于互斥量最大的优点在于允许线程以无竞争的方式等待条件的发生。当一个线程获得互斥锁后,发现自己需要等待某个条件变为真,如果是这样,该线程就可以等待在某个条件上,这样就不需要通过轮询的方式来判断添加,大大节省了CPU时间。

在互斥量一文中说过:互斥量是用于上锁,而不是用于等待;现在这句话可以加强为:互斥量是用于上锁,条件变量用于等待

条件变量声明为pthread_cond_t数据类型,在<bits/pthreadtypes.h>中有具体的定义。

基本操作

条件变量初始化和销毁

/* Initialize condition variable  */
int pthread_cond_init (pthread_cond_t *__restrict __cond,   // 指向要初始化的变量的指针
                              __const pthread_condattr_t *__restrict __cond_attr) ;// 属性

/* Destroy condition variable */
int pt<span style="font-size:12px;">hread_cond_destroy (pthread_cond_t *__cond);</span>

上面两个函数分别由于条件变量的初始化和销毁。

和互斥量的初始化一样,如果条件变量是静态分配的,可以通过常量进行初始化,如下:

<span style="font-size:12px;">pthread_cond_t mlock = PTHREAD_COND_INITIALIZER;</span>

也可以通过pthread_cond_init()进行初始化,对于动态分配的条件变量由于不能直接赋值进行初始化,就只能采用这种方式进行初始化。那么当不在需要使用条件变量时,需要调用pthread_cond_destroy()销毁该条件所占用的资源。

条件变量的属性设置

/* 初始化条件变量属性对象  */
int pthread_condattr_init (pthread_condattr_t *__attr);

/* 销毁条件变量属性对象  */
int pthread_condattr_destroy (pthread_condattr_t *__attr);

/* 获取条件变量属性对象在进程间共享与否的标识  */
int pthread_condattr_getpshared (__const pthread_condattr_t * __restrict __attr,
                                        int *__restrict __pshared);

/* 设置条件变量属性对象,标识在进程间共享与否 */
int pthread_condattr_setpshared (pthread_condattr_t *__attr, int __pshared) ;

这个属性的设置和互斥量属性设置是一样的,具体使用可以参考互斥量的用法:互斥量的属性设置。

条件变量的申请使用

/<span style="font-family:SimSun;font-size:12px;">*  等待条件变为真 */
 int pthread_cond_wait (pthread_cond_t *__restrict __cond,
                              pthread_mutex_t *__restrict __mutex);

 /* 限时等待条件为真 */
 int pthread_cond_timedwait (pthread_cond_t *__restrict __cond,
                                   pthread_mutex_t *__restrict __mutex,
                                   __const struct timespec *__restrict __abstime);

 /* 唤醒一个等待条件的线程.  */
 int pthread_cond_signal (pthread_cond_t *__cond);

 /* 唤醒等待该条件的所有线程 */
 int pthread_cond_broadcast (pthread_cond_t *__cond);</span>

(1)pthread_cond_wait()函数用于等待条件被触发。该函数传入两个参数,一个条件变量一个互斥量,函数将条件变量和互斥量进行关联,互斥量对该条件进行保护,传入的互斥量必须是已经锁住的。调用pthread_cond_wait()函数后,会原子的执行以下两个动作:

  • 将调用线程放到等待条件的线程列表上,即进入睡眠;
  • 对互斥量进行解锁;

由于这两个操作时原子操作,这样就关闭了条件检查和线程进入睡眠等待条件改变这两个操作之间的时间通道,这样就不会错过任何条件的变化。

当pthread_cond_wait()返回后,互斥量会再次被锁住。

(2)pthread_cond_timedwait()函数和pthread_cond_wait()的工作方式相似,只是多了一个等待时间。等待时间的结构为struct timespec,

<span style="font-size:12px;">    struct timespec{
    time_t  tv_sec    //Seconds.
    long    tv_nsec   //Nanoseconds.
    };  </span>

函数要求传入的时间值是一个绝对值,不是相对值,例如,想要等待3分钟,必须先获得当前时间,然后加上3分钟。

要想获得当前系统时间的timespec值,没有直接可调用的函数,需要通过调用gettimeofday函数获取timeval结构,然后转换成timespec结构,转换公式就是:

<span style="font-size:12px;">timeSpec.tv_sec = timeVal.tv_sec;
timeSpec.tv_nsec = timeVal.tv_usec * 1000;</span>

所以要等待3分钟,timespec时间结构的获得应该如下所示:

<span style="font-size:12px;">    struct timeval now;
    struct timespec until;
    gettimeofday(&now);//获得系统当前时间  

    //把时间从timeval结构转换成timespec结构
    until.tv_sec = now.tv_sec;
    until.tv_nsec = now.tv_usec * 1000;  

    //增加min
    until.tv_sec += 3 * 60;  </span>

如果时间到后,条件还没有发生,那么会返回ETIMEDOUT错误。

从pthread_cond_wait()和pthread_cond_timewait()成功返回时,线程需要重新计算条件,因为其他线程可能在运行过程中已经改变条件。

(3)pthread_cond_signal() & pthread_cond_broadcast()

这两个函数都是用于向等待条件的线程发送唤醒信号,pthread_cond_signal()函数只会唤醒等待该条件的某个线程,pthread_cond_broadcast()会广播条件状态的改变,以唤醒等待该条件的所有线程。例如多个线程只读共享资源,这是可以将它们都唤醒。

这里要注意的是:一定要在改变条件状态后,再给线程发送信号。

考虑条件变量信号单播发送和广播发送的一种候选方式是坚持使用广播发送。只有在等待者代码编写确切,只有一个等待者需要唤醒,且唤醒哪个线程无所谓,那么此时为这种情况使用单播,所以其他情况下都必须使用广播发送。

使用示例

下面的程序使用条件变量和互斥锁实现生产者消费者问题.整个临时存储空间为2.因此,如果临时空间已满,则阻塞生产进程,若果没有产品则阻塞消费者线程.

应用程序申请了三个对象:

1)一个互斥锁对象,配合条件变量使用

2)一个空间非空条件变量,消费者线程在空间没有产品时等待这一条件变量;生产者在生成产品后通知此变量

3)一个空间非满条件变量,生成线程在空间满时等待着这一条件变量;消费者线程在消费产品后通知此变量

生产线流程如下:

1.申请互斥锁,如果互斥锁锁定,阻塞等待

2.测试空间是否已满

3.如条件满足(非满),执行操作,完成后解锁互斥锁

4.如果第二步不满足,阻塞,等待非满的条件变量

消费者线程如下:

1.申请互斥锁,如果已锁定,阻塞等待

2.测试空间是否为空

3.满足非空,则执行操作,完成后解锁

4.若第二步不满足,阻塞当前进程,等待空间非空变量.

源代码如下:

/*************************************************************************
> File Name: pthread_cond_exp.c
> Author:SuooL
> Mail:[email protected] || [email protected]
> Website:http://blog.csdn.net/suool | http://suool.net
> Created Time: 2014年08月15日 星期五 13时27分13秒
> Description:
************************************************************************/

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <pthread.h>

#define BUFFER_SIZE 2

/* Circular buffer of integers. */
struct prodcons
{
    int buffer[BUFFER_SIZE];      /* the actual data */
    pthread_mutex_t lock;         /* mutex ensuring exclusive access to buffer */
    int readpos, writepos;        /* positions for reading and writing */
    pthread_cond_t notempty;      /* signaled when buffer is not empty */
    pthread_cond_t notfull;       /* signaled when buffer is not full */
};

/* Initialize a buffer */
void init(struct prodcons *prod)
{
    pthread_mutex_init(&prod->lock,NULL);     // 初始化互斥锁
    pthread_cond_init(&prod->notempty,NULL);  // 初始化条件变量
    pthread_cond_init(&prod->notfull,NULL);   // ....
    prod->readpos = 0;
    prod->writepos = 0;                        // 初始化读写操作位置
}
/* Store an integer in the buffer */
void put(struct prodcons * prod, int data)   // 输入产品子函数
{
    pthread_mutex_lock(&prod->lock);       // 锁定互斥锁
    /* Wait until buffer is not full */
    while ((prod->writepos + 1) % BUFFER_SIZE == prod->readpos)  // 测试空间是否满
    {
        printf("producer wait for not full\n");
        pthread_cond_wait(&prod->notfull, &prod->lock);  // 等待空间有空
    }
    /* Write the data and advance write pointer */
    prod->buffer[prod->writepos] = data;     // 写数据
    prod->writepos++;                        // 写位置加一
    if (prod->writepos >= BUFFER_SIZE)       // 如写到尾部,返回
    prod->writepos = 0;
    /*Signal that the buffer is now not empty */
    pthread_cond_signal(&prod->notempty); // 发送有数据信号
    pthread_mutex_unlock(&prod->lock);          // 解锁
}
/* Read and remove an integer from the buffer */
int get(struct prodcons *prod)
{
    int data;
    pthread_mutex_lock(&prod->lock);       // 锁定
    /* Wait until buffer is not empty */
    while (prod->writepos == prod->readpos)           // 测试是否有数据
    {
        printf("consumer wait for not empty\n");
        pthread_cond_wait(&prod->notempty, &prod->lock); // 空则等待
    }
    /* Read the data and advance read pointer */
    data = prod->buffer[prod->readpos];     // 读数据
    prod->readpos++;                        // 读位置加一
    if (prod->readpos >= BUFFER_SIZE)
    prod->readpos = 0;          // 读到尾部,返回
    /* Signal that the buffer is now not full */
    pthread_cond_signal(&prod->notfull);  // 通知非满
    pthread_mutex_unlock(&prod->lock);      // 解锁
    return data;
}

#define OVER (-1)
struct prodcons buffer;
/*-------------------------------生产者-----------------------*/
void * producer(void * data)
{
    int n;
    for (n = 0; n < 5; n++)         //生产前五个产品
    {
        printf("producer sleep 1 second......\n");
        sleep(1);      // 每秒产一个
        printf("put the %d product\n", n);
        put(&buffer, n);
    }
    for(n=5; n<10; n++)          // 生产后五个
    {
        printf("producer sleep 3 second......\n");
        sleep(3);              // 每三秒产三个
        printf("put the %d product\n",n);
        put(&buffer,n);
    }
    put(&buffer, OVER);
    printf("producer stopped!\n");
    return NULL;
}
/*--------------------------消费者----------------------------*/
void * consumer(void * data)
{
    int d=0;
    while (1)
    {
        printf("consumer sleep 2 second......\n");
        sleep(2);             // 每2秒消费一个
        d=get(&buffer);
        printf("get the %d product\n", d);
        //		d = get(&buffer);
        if (d == OVER ) break;
    }
    printf("consumer stopped!\n");
    return NULL;
}
/*----------------------------生产者--------------------------*/
int main(int argc,char *argv[])
{
    pthread_t th_a, th_b;       // 线程定义
    void * retval;
    init(&buffer);
    pthread_create(&th_a, NULL, producer, 0);     // 创建生产线程
    pthread_create(&th_b, NULL, consumer, 0);       //  消费线程
    /* Wait until producer and consumer finish. */
    pthread_join(th_a, &retval);
    pthread_join(th_b, &retval);              // 等待线程结束
    return 0;
}

结果如下:

Linux程序设计学习笔记----多线程编程之线程同步之条件变量

时间: 2024-12-04 17:48:01

Linux程序设计学习笔记----多线程编程之线程同步之条件变量的相关文章

Linux程序设计学习笔记----多线程编程线程同步机制之互斥量(锁)与读写锁

互斥锁通信机制 基本原理 互斥锁以排他方式防止共享数据被并发访问,互斥锁是一个二元变量,状态为开(0)和关(1),将某个共享资源与某个互斥锁逻辑上绑定之后,对该资源的访问操作如下: (1)在访问该资源之前需要首先申请互斥锁,如果锁处于开状态,则申请得到锁并立即上锁(关),防止其他进程访问资源,如果锁处于关,则默认阻塞等待. (2)只有锁定该互斥锁的进程才能释放该互斥锁. 互斥量类型声明为pthread_mutex_t数据类型,在<bits/pthreadtypes.h>中有具体的定义. 互斥量

Linux程序设计学习笔记----多线程编程基础概念与基本操作

转载请注明出处,http://blog.csdn.net/suool/article/details/38542543,谢谢. 基本概念 线程和进程的对比 用户空间资源对比 每个进程在创建的时候都申请了新的内存空间以存储代码段\数据段\BSS段\堆\栈空间,并且这些的空间的初始化值是父进程空间的,父子进程在创建后不能互访资源. 而每个新创建的线程则仅仅申请了自己的栈,空间,与同进程的其他线程共享该进程的其他数据空间包括代码段\数据段\BSS段\堆以及打开的库,mmap映射的文件与共享的空间,使得

Linux程序设计学习笔记----网络通信编程API及其示例应用

转载请注明出处, http://blog.csdn.net/suool/article/details/38702855. BSD Socket 网络通信编程 BSD TCP 通信编程流程 图为面向连接的Socket通信的双方执行函数流程.使用TCP协议的通信双方实现数据通信的基本流程如下 建立连接的步骤 1.首先服务器端需要以下工作: (1)调用socket()函数,建立Socket对象,指定通信协议. (2)调用bind()函数,将创建的Socket对象与当前主机的某一个IP地址和TCP端口

Linux程序设计学习笔记----网络编程之网络数据包拆封包与字节顺序大小端

网络数据包的封包与拆包 过程如下: 将数据从一台计算机通过一定的路径发送到另一台计算机.应用层数据通过协议栈发到网络上时,每层协议都要加上一个数据首部(header),称为封装(Encapsulation),如下图所示: 不同的协议层对数据包有不同的称谓,在传输层叫做段(segment),在网络层叫做数据包(packet),在链路层叫做帧(frame).数据封装成帧后发到传输介质上,到达目的主机后每层协议再剥掉相应的首部,最后将应用层数据交给应用程序处理. 上图对应两台计算机在同一网段中的情况,

Linux程序设计学习笔记----Socket网络编程基础之TCP/IP协议簇

转载请注明出处: ,谢谢! 内容提要 本节主要学习网络通信基础,主要涉及的内容是: TCP/IP协议簇基础:两个模型 IPv4协议基础:IP地址分类与表示,子网掩码等 IP地址转换:点分十进制\二进制 TCP/IP协议簇基础 OSI模型 我们知道计算机网络之中,有各种各样的设备,那么如何实现这些设备的通信呢? 显然是通过标准的通讯协议,但是,整个网络连接的过程相当复杂,包括硬件.软件数据封包与应用程序的互相链接等等,如果想要写一支将联网全部功能都串连在一块的程序,那么当某个小环节出现问题时,整只

Linux 程序设计学习笔记----终端及串口编程基础之概念详解

转载请注明出处,谢谢! linux下的终端及串口的相关概念有: tty,控制台,虚拟终端,串口,console(控制台终端)详解 部分内容整理于网络. 终端/控制台 终端和控制台都不是个人电脑的概念,而是多人共用的小型中型大型计算机上的概念. 1.终端 一台主机,连很多终端,终端为主机提供了人机接口,每个人都通过终端使用主机的资源. 终端有字符哑终端和图形终端两种. 控制台是另一种人机接口, 不通过终端与主机相连, 而是通过显示卡-显示器和键盘接口分别与主机相连, 这是人控制主机的第一人机接口.

Linux 程序设计学习笔记----终端及串口编程及实例应用

转载请注明出处,http://blog.csdn.net/suool/article/details/38385355. 部分内容类源于网络. 终端属性详解及设置 属性 为了控制终端正常工作,终端的属性包括输入属性.输出属性.控制属性.本地属性.线路规程属性以及控制字符. 其在系统源代码的termios.h中定义(具体的说明文档http://pubs.opengroup.org/onlinepubs/7908799/xsh/termios.h.html),其结构体成员主要是 Thetermios

Linux 程序设计学习笔记----POSIX 文件及目录管理

转载请注明:http://blog.csdn.net/suool/article/details/38141047 问题引入 文件流和文件描述符的区别 上节讲到ANSI C 库函数的实现在用户态,流的相应资源也在用户空间,但无论如何实现最终都需要通过内核实现对文件的读写控制.因此fopen函数必然调用了对OS的系统调用.这一调用在LINUX下即为open, close, read, write等函数.这些都遵循POSIX标准. so,在linux系统中是如何通过POSIX标准实现对文件的操作和目

Linux 程序设计学习笔记----进程管理与程序开发(下)

转载请注明出处:http://blog.csdn.net/suool/article/details/38419983,谢谢! 进程管理及其控制 创建进程 fork()函数 函数说明具体参见:http://pubs.opengroup.org/onlinepubs/009695399/functions/fork.html 返回值:Upon successful completion, fork() shall return 0 to the child process and shall re