线程同步(三)

目录

条件变量

条件变量操作函数函数原型:

线程阻塞函数:

唤醒阻塞线程: 

生产者和消费者模型

信号量函数

生产者和消费者模型

 总结


条件变量

        条件变量是一种线程间同步的机制,用于协调线程之间的操作。当一个线程正在等待某个条件变成真,而另一个线程修改了该条件时,条件变量就可以通知等待的线程。这样,等待的线程就可以继续执行,而不必浪费时间轮询条件是否成立。

        在使用条件变量时,通常需要先定义一个互斥量,保护共享资源的访问。然后,等待线程通过调用条件变量的wait函数来等待条件的成立。当其他线程修改了条件并通过调用条件变量的signal函数或broadcast函数来通知时,等待线程就会被唤醒,重新获取互斥量并检查条件是否成立。如果条件仍然不成立,等待线程就会再次进入等待状态。

        条件变量通常与互斥量一起使用,以确保线程安全,并避免条件竞争的发生。

条件变量操作函数函数原型:

#include 
pthread_cond_t cond;
// 初始化
int pthread_cond_init(pthread_cond_t *restrict cond,
      const pthread_condattr_t *restrict attr);
// 销毁释放资源        
int pthread_cond_destroy(pthread_cond_t *cond);

//cond: 条件变量的地址

//attr: 条件变量属性,一般使用默认属性,指定为 NULL

线程阻塞函数:

// 线程阻塞函数, 哪个线程调用这个函数, 哪个线程就会被阻塞
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);

        通过函数原型可以看出,该函数在阻塞线程的时候,需要一个互斥锁参数,这个互斥锁主要功能是进行线程同步,让线程顺序进入临界区,避免出现数共享资源的数据混乱。该函数会对这个互斥锁做以下几件事情:

        在阻塞线程时候,如果线程已经对互斥锁 mutex 上锁,那么会将这把锁打开,这样做是为了避免死锁
        当线程解除阻塞的时候,函数内部会帮助这个线程再次将这个 mutex 互斥锁锁上,继续向下访问临界区

// 表示的时间是从1971.1.1到某个时间点的时间, 总长度使用秒/纳秒表示
struct timespec {
	time_t tv_sec;      /* Seconds */
	long   tv_nsec;     /* Nanoseconds [0 .. 999999999] */
};
// 将线程阻塞一定的时间长度, 时间到达之后, 线程就解除阻塞了
int pthread_cond_timedwait(pthread_cond_t *restrict cond,
           pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime);

        这个函数的前两个参数和 pthread_cond_wait 函数是一样的,第三个参数表示线程阻塞的时长,但是需要额外注意一点:struct timespec 这个结构体中记录的时间是从1971.1.1到某个时间点的时间,总长度使用秒/纳秒表示。因此赋值方式相对要麻烦一点:

time_t mytim = time(NULL);	// 1970.1.1 0:0:0 到当前的总秒数
struct timespec tmsp;
tmsp.tv_nsec = 0;
tmsp.tv_sec = time(NULL) + 100;	// 线程阻塞100s

唤醒阻塞线程: 

// 唤醒阻塞在条件变量上的线程, 至少有一个被解除阻塞
int pthread_cond_signal(pthread_cond_t *cond);
// 唤醒阻塞在条件变量上的线程, 被阻塞的线程全部解除阻塞
int pthread_cond_broadcast(pthread_cond_t *cond);

        调用上面两个函数中的任意一个,都可以唤醒被 pthread_cond_wait 或者 pthread_cond_timedwait 阻塞的线程,区别就在于 pthread_cond_signal 是唤醒至少一个被阻塞的线程(总个数不定),pthread_cond_broadcast 是唤醒所有被阻塞的线程

生产者和消费者模型

        生产者消费者模型是一种经典的线程同步机制,用于解决多线程间共享资源的问题。

        在生产者消费者模型中,生产者线程负责生成数据并将其存入一个共享的缓冲区中,而消费者线程则负责从缓冲区中取出数据并对其进行处理。生产者和消费者通过共享的缓冲区来进行通信,因此需要进行线程同步来保证数据的正确性和一致性。

        生产者消费者模型通常使用一个有限大小的队列来作为共享的缓冲区。当队列已满时,生产者线程需要等待;当队列为空时,消费者线程需要等待。为了避免死锁和资源浪费,需要使用条件变量和互斥量来进行线程同步。

        一般来说,生产者线程将数据放入队列之前需要获取互斥量,以防止多个线程同时访问队列造成数据冲突。然后,如果队列已满,生产者线程就需等待条件变量发出队列有空位的信号。待收到信号后,生产者线程就会将生产的数据放入队列中,释放互斥量并通知等待的消费者线程。消费者线程也是类似的过程,只不过相反。

        生产者消费者模型是一种常见的多线程编程范例,被广泛应用于计算机系统、操作系统和数据库等领域中。它可以有效地解决多线程间共享资源的同步和协作问题。

代码:

        使用条件变量实现生产者和消费者模型,生产者有 5 个,往链表头部添加节点,消费者也有 5 个,删除链表头部的节点。

#include 
#include 
#include 
#include 
#include 

// 链表的节点
struct Node
{
    int number;
    struct Node* next;
};

// 定义条件变量, 控制消费者线程
pthread_cond_t cond;
// 互斥锁变量
pthread_mutex_t mutex;
// 指向头结点的指针
struct Node * head = NULL;

// 生产者的回调函数
void* producer(void* arg)
{
    // 一直生产
    while(1)
    {
        pthread_mutex_lock(&mutex);
        // 创建一个链表的新节点
        struct Node* pnew = (struct Node*)malloc(sizeof(struct Node));
        // 节点初始化
        pnew->number = rand() % 1000;
        // 节点的连接, 添加到链表的头部, 新节点就新的头结点
        pnew->next = head;
        // head指针前移
        head = pnew;
        printf("+++producer, number = %d, tid = %ld\n", pnew->number, pthread_self());
        pthread_mutex_unlock(&mutex);

        // 生产了任务, 通知消费者消费
        pthread_cond_broadcast(&cond);

        // 生产慢一点
        sleep(rand() % 3);
    }
    return NULL;
}

// 消费者的回调函数
void* consumer(void* arg)
{
    while(1)
    {
        pthread_mutex_lock(&mutex);
        // 一直消费, 删除链表中的一个节点
//        if(head == NULL)   // 这样写有bug
        while(head == NULL)
        {
            // 任务队列, 也就是链表中已经没有节点可以消费了
            // 消费者线程需要阻塞
            // 线程加互斥锁成功, 但是线程阻塞在这行代码上, 锁还没解开
            // 其他线程在访问这把锁的时候也会阻塞, 生产者也会阻塞 ==> 死锁
            // 这函数会自动将线程拥有的锁解开
            pthread_cond_wait(&cond, &mutex);
            // 当消费者线程解除阻塞之后, 会自动将这把锁锁上
            // 这时候当前这个线程又重新拥有了这把互斥锁
        }
        // 取出链表的头结点, 将其删除
        struct Node* pnode = head;
        printf("--consumer: number: %d, tid = %ld\n", pnode->number, pthread_self());
        head  = pnode->next;
        free(pnode);
        pthread_mutex_unlock(&mutex);        

        sleep(rand() % 3);
    }
    return NULL;
}

int main()
{
    // 初始化条件变量
    pthread_cond_init(&cond, NULL);
    pthread_mutex_init(&mutex, NULL);

    // 创建5个生产者, 5个消费者
    pthread_t ptid[5];
    pthread_t ctid[5];
    for(int i=0; i<5; ++i)
    {
        pthread_create(&ptid[i], NULL, producer, NULL);
    }

    for(int i=0; i<5; ++i)
    {
        pthread_create(&ctid[i], NULL, consumer, NULL);
    }

    // 释放资源
    for(int i=0; i<5; ++i)
    {
        // 阻塞等待子线程退出
        pthread_join(ptid[i], NULL);
    }

    for(int i=0; i<5; ++i)
    {
        pthread_join(ctid[i], NULL);
    }

    // 销毁条件变量
    pthread_cond_destroy(&cond);
    pthread_mutex_destroy(&mutex);

    return 0;
}

运行结果: 

线程同步(三)_第1张图片

       可以看到生产者将商品放到任务队列中,任务队列满了就阻塞,不满的时候就工作。消费者读任务队列,将任务或者数据取出,任务队列中有数据就消费,没有数据就阻塞。

信号量函数

信号量(Semaphore)是一种线程间同步的机制,它是一个整数,用于控制对共享资源的访问。信号量有两个基本操作:等待和通知(或称为 P 操作和 V 操作)。等待操作使信号量的值减 1,如果信号量的值为负,则进程或线程将阻塞,直到其它进程或线程释放资源并使信号量的值变为非负为止。通知操作则使信号量的值加 1,如果此时有其它进程或线程正在等待,则其中一个将被唤醒。

在 POSIX 标准中,提供了以下几个信号量函数:

sem_init()//初始化一个信号量。
sem_destroy()//销毁一个信号量。
sem_post()//执行 V 操作,使信号量的值加 1。
sem_wait()//执行 P 操作,如果信号量的值为正,则将其减 1;否则将当前线程或进程阻塞,直到其它线程或进程通知并唤醒它。
sem_trywait()//尝试执行 P 操作,如果信号量的值为正,则将其减 1 并立即返回;否则返回错误。

        这些函数可以用于构建更高级别的同步机制,如互斥量、条件变量、读写锁等。使用信号量函数时需要注意线程安全和死锁问题,以确保程序的正确性和健壮性。

生产者和消费者模型

#include 
#include 
#include 
#include 
#include 
#include 

// 链表的节点
struct Node
{
    int number;
    struct Node* next;
};

// 生产者线程信号量
sem_t psem;
// 消费者线程信号量
sem_t csem;

// 互斥锁变量
pthread_mutex_t mutex;
// 指向头结点的指针
struct Node * head = NULL;

// 生产者的回调函数
void* producer(void* arg)
{
    // 一直生产
    while(1)
    {
        // 生产者拿一个信号灯
        sem_wait(&psem);
        // 加锁, 这句代码放到 sem_wait()上边, 有可能会造成死锁
        pthread_mutex_lock(&mutex);
        // 创建一个链表的新节点
        struct Node* pnew = (struct Node*)malloc(sizeof(struct Node));
        // 节点初始化
        pnew->number = rand() % 1000;
        // 节点的连接, 添加到链表的头部, 新节点就新的头结点
        pnew->next = head;
        // head指针前移
        head = pnew;
        printf("+++producer, number = %d, tid = %ld\n", pnew->number, pthread_self());
        pthread_mutex_unlock(&mutex);

        // 通知消费者消费
        sem_post(&csem);
        
        // 生产慢一点
        sleep(rand() % 3);
    }
    return NULL;
}

// 消费者的回调函数
void* consumer(void* arg)
{
    while(1)
    {
        sem_wait(&csem);
        pthread_mutex_lock(&mutex);
        struct Node* pnode = head;
        printf("--consumer: number: %d, tid = %ld\n", pnode->number, pthread_self());
        head  = pnode->next;
        // 取出链表的头结点, 将其删除
        free(pnode);
        pthread_mutex_unlock(&mutex);
        // 通知生产者生成, 给生产者加信号灯
        sem_post(&psem);

        sleep(rand() % 3);
    }
    return NULL;
}

int main()
{
    // 初始化信号量
    sem_init(&psem, 0, 5);  // 生成者线程一共有5个信号灯
    sem_init(&csem, 0, 0);  // 消费者线程一共有0个信号灯
    // 初始化互斥锁
    pthread_mutex_init(&mutex, NULL);

    // 创建5个生产者, 5个消费者
    pthread_t ptid[5];
    pthread_t ctid[5];
    for(int i=0; i<5; ++i)
    {
        pthread_create(&ptid[i], NULL, producer, NULL);
    }

    for(int i=0; i<5; ++i)
    {
        pthread_create(&ctid[i], NULL, consumer, NULL);
    }

    // 释放资源
    for(int i=0; i<5; ++i)
    {
        pthread_join(ptid[i], NULL);
    }

    for(int i=0; i<5; ++i)
    {
        pthread_join(ctid[i], NULL);
    }

    sem_destroy(&psem);
    sem_destroy(&csem);
    pthread_mutex_destroy(&mutex);

    return 0;
}

运行结果: 

线程同步(三)_第2张图片

 总结:

信号量和条件变量都是用于线程间同步和互斥的机制,但它们的作用和使用方式略有不同:

        1. 信号量主要用于控制对共享资源的访问,它的值可以表示共享资源的数量或者可用的资源数量。信号量的增减操作是原子性的。P 操作和 V 操作可以分别用于申请和释放资源,从而控制线程的访问。信号量不关心具体的资源内容,只关心资源的可用性和数量。
        2. 条件变量用于等待某个条件的成立,通常和互斥量结合使用。条件变量的等待操作和通知操作分别对应于 wait 和 signal 函数。wait 函数将线程挂起,直到条件成立或者被其它线程唤醒;signal 函数则用于唤醒等待线程中的一个或多个,从而满足某个条件的成立。

        因此,信号量和条件变量有着不同的适用场景和用法,常常用于不同的同步和互斥问题中。一般来讲,如果只需要控制对共享资源的访问,可以使用信号量;如果需要等待某个条件的成立再进行操作,可以使用条件变量。实际上,在某些情况下,信号量和条件变量可以结合起来使用,实现更复杂的同步和互斥问题的解决。

你可能感兴趣的:(Linux,linux,c++)