Linux——互斥锁与条件变量(一)

为允许在线程或进程间共享数据,同步通常是必需的。互斥锁和条件变量是同步的基本组成部分。

使用互斥锁和条件变量的经典例子:生产者-消费者问题。在本例中,使用多个线程而不是多个进程,因为让多个线程共享本问题采用的公共数据缓冲区非常简单,而在多个进程间共享一个公共数据缓冲区却需要某种形式的共享内存区。

一、互斥锁:上锁与解锁

互斥锁指代相互排斥mutual exclusion),它是最基本的同步形式。互斥锁用于保护临界区(critical region),以保证任何时刻只有一个线程在执行其中的代码(假设互斥锁由多个线程共享),或者任何时刻只有一个进程在执行其中的代码(假设互斥锁由多个进程共享)。保护一个临界区的代码轮廓大体如下:

lock_the_mutex(...);
临界区
unlock_the_mutex(...);

既然任何时刻只有一个线程能够锁住一个给定的互斥锁,于是这样的代码保证在任何时刻只有一个线程在执行其临界区中的指令。

Posix互斥锁被声明为具有pthread_mutex_t数据类型的变量。如果互斥锁变量是静态分配的,那么我们可以把它初始化成常值PTHREAD_MUTEX_INITIALIZER,例如:

static pthread_mutex_t lock =  PTHREAD_MUTEX_INITIALIZER;

如果互斥锁是动态分配的,或者分配在共享内存区中,那么我们必须在运行之时,通过调用pthread_mutex_init函数来初始化它。后面有所介绍。

在Posix中,定义了三个函数来给一个互斥锁上锁和解锁:

#include<pthread.h>
int pthread_mutex_lock(pthread_mutex_t *mptr);
int pthread_mutex_trylock(pthread_mutex_t *mptr);
int pthread_mutex_unlock(pthread_mutex_t *mptr);
均返回:若成功则为0,若出错则为正的Exxx值

如果尝试给一个已有某个线程锁住的互斥锁上锁,那么pthread_mutex_lock将阻塞到该互斥锁解锁为止。pthread_mutex_trylock是对应的非阻塞函数,如果该互斥锁已锁住,它就返回一个EBUSY错误。

这里有一个问题:

如果有多个线程阻塞在等待同一个互斥锁上,那么当该互斥锁解锁时,哪一个线程会开始运行呢?

===>>>提供一个优先级调度选项,不同线程可被赋予不同的优先级,同步函数(互斥锁、读写锁、信号量)将唤醒优先级最高(队列实现?)的被阻塞线程。

临界区实际上保护的是在临界区中被操纵的数据,也就是说,互斥锁通常用于保护由多个线程或多个进程分享的共享数据(shared data)

互斥锁是协作性锁。这就是说,如果共享数据是一个链表,那么操纵该链表的所有线程都必须在实际操纵前获取该互斥锁。不过也没有办法防止某个线程不首先获取该互斥锁就操纵该链表。

二、生产者-消费者问题

同步中有一个称为“生产者-消费者(producer-consumer)”问题的经典问题,也称为有界缓冲区(bounded buffer)问题。一个或多个生产者(线程或进程)创建者一个个数据条目,然后这些条目由一个或多个消费者(线程或进程)处理。数据条目在生产者和消费者之间是使用某种类型的IPC(interprocess communication)传递的。

当共享存储区用作生产者和消费者之间的IPC形式时,生产者和消费者必须执行某种类型的显式(explicit)同步。我们使用互斥锁展示显式同步。

在这里,我们选择使用单个消费者线程、多个生产者线程的方法。首先,介绍一些相关信息:在单个进程中有多个生产者线程和单个消费者线程。整数数组buff含有被生产和消费的条目(也就是共享数据)。为简单起见,生产者只是把buff[0]设置为0,把buff[1]设置为1,如此等等。消费者只是沿着该数组行进,并验证每个数组元素都是正确的。

然后,写出main函数如下:

#include “unpipc.h”
 
#define MAXNITEMS 1000000
#define MAXNTHREADS 100
 
int nitems;
 
struct{
    pthread_mutex_t mutex;
    int buff[MAXNITEMS];
    int nput;
    int nval;  
}shared= {PTHREAD_MUTEX_INITIALIZER};
 
void *produce(void *),*consumer(void *);//生产者、消费者函数
 
int main(int argc , char **argv){
    int i, nthreads, count[MAXNTHREADS];
    pthread_t tid_produce[MAXNTHREADS],tid_consume;
    if(argc!=3){
        err_quit("usage:prodcons <#items> <#threads>"); 
    }
    nitems = min(atoi(argv[1]), MAXNITEMS);
    nthreads = min(atoi(argv[2]), MAXNTHREADS);
  
    Set_concurrency(nthreads);
  
    /*start all the producer threads*/
    for(i=0;i<nthreads;i++){
    count[i] = 0;
    pthread_create(&tid_produce[i],NULL,produce,&count[i]); 
    }

    /*wait for all producer threads  */
    for(i=0;i<nthreads;i++){
    pthread_join(tid_produce[i],NULL);
    printf("count[%d]=%d",i,count[i]); 
    }
  
    /*start, then wait for the consumer thread*/
    pthread_create(&tid_consume,NULL,consume,NULL);
    pthread_join(tid_consume,NULL);
  
    exit(0);
}
 
void *produce(void *arg){
    for(;;){
        pthread_mutex_lock(&shared.mutex);
        if(shared.nput >= nitems){
        pthread_mutex_unlock(&shared.mutex);
        return NULL; 
        /* array is full,we are done*/
        }
        shared.buff[shared.nput] = shared.nval;
        shared.nput++;
        shared.nval++;
        pthread_mutex_unlock(&shared.mutex);
        *((int *)arg)+=1;
    } 
}
 
void *consume(void *arg){
    int i;
    for(i=0;i<nitems;i++){
        if(shared.buff[i] != i)
        printf("buff[%d] = %d\n",i,shared.buff[i]);
    }
    return NULL;
}

在上面的main函数,我们需要注意这样几个方面:

1、struct shared的作用:

首先我们分析这些成员变量,mutex是互斥锁,buff数组是数据空间,nputbuff数组中下一次存放数据的元素下标,nval是下一次存放的值。我们将这些变量整合放在一个结构体中的目的是为了强调这些变量只应该在拥有互斥锁时访问。我们分配这个结构,并初始化其中用于生产者线程间同步的互斥锁。

对于这里,我们要考虑这样一个问题:将所有受互斥锁控制的临界区变量都整合放在一个结构体中是否十分完善?这是不完善的,把共享数据和它们的同步变量收集到一个结构体中,这是一个很好的编程技巧,然而在很多情况下共享数据是动态分配的,譬如说一个链表,我们可以把一个链表的头及该链表的同步变量存放到一个结构体中,但是其他共享数据(该链表的其他部分)却不在结构体中,因此这种方法是不完善的。

2、命令行输入参数分析:形如“prodcons items threads”,其中,items指定生产者存放的条目数,threads指定待创建生产者线程的数目。

3、Set_concurrency(nthreads)函数的作用:

告诉线程系统我们希望并发运行多少线程。

4、创建生产者线程:

在这里,我们用线程创建函数pthread_create(...)来创建生产者进程,每个线程执行produce。在tid_produce数组中保存每个生成线程的pid。传递给每个生产者线程的参数是指向count数组中某个元素的指针。那么count数组的作用是什么?代码中,我们首先把count计数器初始化为0,然后每个线程在每次往缓冲区中存放一个条目时给这个计数器加1(*(int*)arg)+=1),。当一切生产完毕时,我们输出这个计数器数组中各元素的值,以查看每个生产者线程分别存放了多少条目。

5、等待生产者线程,然后启动消费者线程:

等待所有生产者线程终止,同时输出每个线程的计数器值,此后才启动单个消费者线程。此处,我们实现了生产者与消费者之间的同步问题,接着等待消费者线程完成,然后终止进程。

6、Produce函数产生数据条目:

在这里,我们使用互斥锁来实现对临界区的保护,同时保证在一切生产完毕的情况下给该互斥锁解锁。注意count元素的增加不属于该临界区,因为每个线程有各自的计数器(main函数中的count)。既然如此,我们就不把这行代码包括在由互斥锁锁住的临界区中,因为作为一个通用的编程原则,我们总是应该努力减少由一个互斥锁锁住的代码量。

7、消费者验证数组内容:

这里不需要任何同步。

未完待续!

你可能感兴趣的:(Linux——互斥锁与条件变量(一))