Linux 多线程编程(实现生产者消费者模型)

Linux 多线程编程

线程分类

线程按照其调度者可以分为用户级线程和内核级线程两种。

内核级线程

在一个系统上实现线程模型的方式有好几种,因内核和用户空间提供的支持而有一定程度的级别差异。最简单的模型是在内核为线程提供了本地支持的情况,每个内核线程直接转换成用户空间的线程。这种模型称为“1:1线程模型(threading)”,因为内核提供的线程和用户的线程的数量是1:1。该模型也称为“内核级线程模型(kernel-level threading)”,因为内核是系统线程模型的核心。

Linux 中的线程就是“1:1线程模型”。在linux内核中只是简单地将线程实现成能够共享资源的进程。线程库通过系统调用 clone() 创建一个新的线程,返回的“进程”直接作为用户空间的线程。也就是说,在Linux上,用户调用线程和内核调用线程基本一致。

Linux的线程实现是在核外进行的,核内提供的是创建进程的接口do_fork()。内核提供了两个系统调用clone()和fork(),最终都用不同的参数调用do_fork()核内API。当然,要想实现线程,没有核心对多进程(其实是轻量级进程)共享数据段的支持是不行的,因此,do_fork()提供了很多参数,包括CLONE_VM(共享内存空间)、CLONE_FS(共享文件系统信息)、 CLONE_FILES(共享文件描述符表)、CLONE_SIGHAND(共享信号句柄表)和CLONE_PID(共享进程ID,仅对核内进程,即0号进程有效)。当使用fork系统调用时,内核调用do_fork()不使用任何共享属性,进程拥有独立的运行环境,而使用 pthread_create()来创建线程时,则最终设置了所有这些属性来调用__clone(),而这些参数又全部传给核内的do_fork(),从而创建的“进程”拥有共享的运行环境,只有栈是独立的,由__clone()传入。

Linux线程在核内是以轻量级进程的形式存在的,拥有独立的进程表项,而所有的创建、同步、删除等操作都在核外pthread库中进行。pthread 库使用一个管理线程(__pthread_manager(),每个进程独立且唯一)来管理线程的创建和终止,为线程分配线程ID,发送线程相关的信号(比如Cancel),而主线程(pthread_create())的调用者则通过管道将请求信息传给管理线程。

用户级线程

用户级线程主要解决的是上下文切换的问题,它的调度算法和调度过程全部由用户自行选择决定,在运行时不需要特定的内核支持。在这里,操作系统往往会提供一个用户空间的线程库,该线程库提供了线程的创建、调度、撤销等功能,而内核仍然仅对进程进行管理。如果一个进程中的某一个线程调用了一个阻塞的系统调用,那么该进程包括该进程中的其他所有线程也同时被阻塞。这种用户级线程的主要缺点是在一个进程中的多个线程的调度中无法发挥多处理器的优势。

混合线程模型

如果我们把内核级线程模型和用户级线程模型结合起来,结果会怎样呢?是否可以实现“1:1线程模型”带来的真正并行性,同时还可以利用“N:1线程模型”的零成本切换?确实可以,只不过这个模型很复杂。“N:M线程模型”也称为“混合式线程模型”,正是希望能够充分利用前两种模型的优点:内核提供一个简单的线程概念,而用户空间也实现用户线程。然后,用户空间(可能和内核结合起来)决定任何把N个用户线程映射到M个内核线程中,其中 N>=M。

条件变量

  1. //pthread_cond_signal 只发信号,内部不会解锁,在Linux 线程中,有两个队列,分别是cond_wait队列和mutex_lock队列, cond_signal只是让线程从cond_wait队列移到mutex_lock队列,而不用返回到用户空间,不会有性能的损耗。(pthread_cond_signal unlock后pthread_cond_wait才能上锁)
  2. //pthread_cond_wait 先解锁,等待,有信号来,上锁,执行while检查防止另外的线程更改条件
    //循环判断的原因如下:假设2个线程在getq阻塞,然后两者都被激活,而其中一个线程运行比较块,快速消耗了2个数据,另一个线程醒来的时候已经没有新 数据可以消耗了。另一点,man pthread_cond_wait可以看到,该函数可以被信号中断返回,此时返回EINTR。为避免以上任何一点,都必须醒来后再次判断睡眠条件。

介绍:

  1. pthread_cond_wait() 用于阻塞当前线程,等待别的线程使用pthread_cond_signal()或pthread_cond_broadcast来唤醒它。
  2. pthread_cond_wait() 必须与pthread_mutex 配套使用。pthread_cond_wait()函数一进入wait状态就会自动release mutex。当其他线程通过pthread_cond_signal()或pthread_cond_broadcast,把该线程唤醒,使pthread_cond_wait()通过(返回)时,该线程又自动获得该mutex。
  3. pthread_cond_signal函数的作用是发送一个信号给另外一个正在处于阻塞等待状态的线程,使其脱离阻塞状态,继续执行.如果没有线程处在阻塞等待状态,pthread_cond_signal也会成功返回。
  4. 使用pthread_cond_signal一般不会有“惊群现象”产生,他最多只给一个线程发信号。假如有多个线程正在阻塞等待着这个条件变量的话,那么是根据各等待线程优先级的高低确定哪个线程接收到信号开始继续执行。如果各线程优先级相同,则根据等待时间的长短来确定哪个线程获得信号。但无论如何一个pthread_cond_signal调用最多发信一次。
  5. 但是pthread_cond_signal在多处理器上可能同时唤醒多个线程,而如果你同时只允许一个线程访问的话,就必须要使用while来进行条件判断,以保证临界区内只有一个线程在处理。

以下就是一个来自MAN的示例

//Consider two shared variables x and y, protected by the mutex mut, and a condition variable cond that is to be signaled whenever x becomes greater than y.

int x, y;
pthread_mutex_t mut = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
// Waiting until x is greater than y is performed as follows:
pthread_mutex_lock(&mut);
while (x <= y) {
    pthread_cond_wait(&cond, &mut);
}
/* operate on x and y */
pthread_mutex_unlock(&mut);
// Modifications on x and y that may cause x to become greater than y should signal the condition if needed:
pthread_mutex_lock(&mut);
/* modify x and y */
if (x > y) pthread_cond_broadcast(&cond);
pthread_mutex_unlock(&mut);

生产者消费者模型(使用互斥锁和条件变量)

#include 
#include 
#include 
#include 


#define BUFFER_SIZE 8

struct Products {
    int buffer[BUFFER_SIZE];
    /*保证存取操作的原子性 互斥性*/
    pthread_mutex_t locker;
    /*是否可读*/
    pthread_cond_t notEmpty;
    /*是否可写*/
    pthread_cond_t notFull;
    int posReadFrom;
    int posWriteTo;
};

int BufferIsFull(struct Products* products) {
    if ((products->posWriteTo + 1) % BUFFER_SIZE == products->posReadFrom) {
        return (1);
    }
    return (0);
}

int BufferIsEmpty(struct Products* products) {
    if (products->posWriteTo == products->posReadFrom) {
        return (1);
    }
    return (0);
}

/*制造产品*/void Produce(struct Products* products, int item) {
    /*原子操作*/
    pthread_mutex_lock(&products->locker);
    /*无空间可写入*/
    while (BufferIsFull(products)) {
        pthread_cond_wait(&products->notFull, &products->locker);
    }

    /*写入数据*/
    products->buffer[products->posWriteTo] = item;
    products->posWriteTo++;
    if (products->posWriteTo >= BUFFER_SIZE)
        products->posWriteTo = 0;
    /*发信*/
    pthread_cond_signal(&products->notEmpty);
    /*解锁*/
    pthread_mutex_unlock(&products->locker);
}

int Consume(struct Products* products) {
    int item;
    pthread_mutex_lock(&products->locker);
    /*为空时持续等待,无数据可读*/
    while (BufferIsEmpty(products)) {
        pthread_cond_wait(&products->notEmpty, &products->locker);
    }
    /*提取数据*/
    item = products->buffer[products->posReadFrom];
    products->posReadFrom++;
    /*如果到末尾,从头读取*/
    if (products->posReadFrom >= BUFFER_SIZE)
        products->posReadFrom = 0;
    pthread_cond_signal(&products->notFull);
    pthread_mutex_unlock(&products->locker);
    return item;
}




#define END_FLAG (-1)
struct Products products;

void* ProducerThread(void* data) {
    int i;
    for (i = 0; i < 16; ++i) {
        printf("producer: %d\n", i);
        Produce(&products, i);
    }
    Produce(&products, END_FLAG);
    return NULL;
}

void* ConsumerThread(void* data) {
    int item;
    while (1) {
        item = Consume(&products);
        if (END_FLAG == item)
            break;
        printf("consumer: %d\n", item);
    }
    return (NULL);
}

int main(int argc, char* argv[]) {
    pthread_t producer;
    pthread_t consumer;
    int result;
    pthread_create(&producer, NULL, &ProducerThread, NULL);
    pthread_create(&consumer, NULL, &ConsumerThread, NULL);
    pthread_join(producer, (void *) &result);
    pthread_join(consumer, (void *) &result);
    exit(EXIT_SUCCESS);
}

信号量

如同进程一样,线程也可以通过信号量来实现通信,虽然是轻量级的。
信号量函数的名字都以”sem_”打头。线程使用的基本信号量函数有四个。

#include 
int sem_init(sem_t *sem , int pshared, unsigned int value);

这是对由sem指定的信号量进行初始化,设置好它的共享选项(linux只支持为0,即表示它是当前进程的局部信号量),然后给它一个初始值VALUE。

两个原子操作函数:这两个函数都要用一个由sem_init调用初始化的信号量对象的指针做参数。

int sem_wait(sem_t *sem); //给信号量减1,对一个值为0的信号量调用sem_wait,这个函数将会等待直到有其它线程使它不再是0为止。
int sem_post(sem_t *sem); //给信号量的值加1
int sem_destroy(sem_t *sem);

这个函数的作用是再我们用完信号量后都它进行清理。归还自己占有的一切资源。

生产者消费者模型(使用信号量)

这里使用4个信号量,其中两个信号量occupied和empty分别用于解决生产者和消费者线程之间的同步问题,pmut用于多个生产者之间互斥问题,cmut是用于多个消费者之间互斥问题。其中empty初始化为N(有界缓区的空间元数),occupied初始化为0,pmut和cmut初始化为1。

#define BSIZE 64

typedef struct {
    char buf[BSIZE];
    sem_t occupied;
    sem_t empty;
    int nextin;
    int nextout;
    sem_t pmut;
    sem_t cmut;
} buffer_t;
buffer_t buffer;

void init(buffer_t * b) {
    sem_init(&b->occupied, 0, 0);
    sem_init(&b->empty, 0, BSIZE);
    sem_init(&b->pmut, 0, 1);
    sem_init(&b->cmut, 0, 1);
    b->nextin = b->nextout = 0;
}

void producer(buffer_t *b, char item) {
    sem_wait(&b->empty);
    sem_wait(&b->pmut);
    b->buf[b->nextin] = item;
    b->nextin++;
    b->nextin %= BSIZE;
    sem_post(&b->pmut);
    sem_post(&b->occupied);
}

char consumer(buffer_t *b) {
    char item;
    sem_wait(&b->occupied);
    sem_wait(&b->cmut);
    item = b->buf[b->nextout];
    b->nextout++;
    b->nextout %= BSIZE;
    sem_post(&b->cmut);
    sem_post(&b->empty);
    return item;
}

参考文章:

POSIX 线程详解
Linux 线程实现机制分析

你可能感兴趣的:(操作系统)