linux用户态条件变量和内核态完成变量

如果我们的线程要等一个条件满足之后才可以继续向下执行,这个条件不满足的话,就要等待这个条件。这种场景经常见到,比如我们使用recv接收网络数据的时候,或者使用epoll_wait来等待事件的时候,在默认情况下,recv和epoll_wait都会等到有数据或者事件到来才会返回。

生产者和消费者的场景是条件变量典型的应用场景。一个队列,一个生产者,一个消费者,生产者向队列中生产数据,消费者从队列中消费数据。当队列满的时候,生产者就不能继续生产了,需要等有元素被消费之后才能继续生产;当队列空的时候,消费者不能继续消费数据了,需要有新的数据被生产才能继续消费。

具体怎么等待条件呢?有两种基本的方式:

轮询:

比如生产者在队列满的时候,通过轮询的方式不断地去确定队列是不是满,如果满,则继续等待;否则就继续生产数据。

条件:

当队列满的时候,生产者就睡眠,消费者消费数据之后,将生产者唤醒。

轮询和条件是两种基本的实现方式,这两种思想应用于很多场景,比如硬件和软件的通信方式:轮询和中断,中断就类似于这里的条件;比如多线程中使用的自旋锁和互斥体,前者是轮询的方式,后者是条件的方式。

本文讨论条件的方式。在linux中用户态和内核态均提供了这样的机制:用户态的条件变量,内核态的完成变量。

1用户态:条件变量

1.1使用

(1)条件变量要和互斥体mutex在一块使用

(2)wait的时候有两点需要注意

①wait之前要加锁,wait的时候会释放锁,wait返回之前会再次加锁。也就是说在函数pthread_cond_wait中,会有一次解锁和加锁的过程。

②wait返回之后要再次判断条件是不是满足,本例中消费者和生产者只有一个,而在实际使用中,生产者和消费者可能有多个。如果唤醒侧调用了pthread_cond_broadcast,那么等待者就会全部被唤醒,这样可能数据已经被先唤醒的线程消费了,后被唤醒的线程没有数据消费,所以需要加判断。这是条件变量的典型用法。

(3)wait侧使用方式

加锁

wait

解锁

(4)唤醒侧使用方式

加锁

signal/broadcast

解锁

唤醒侧其实也可以不加锁,也能起到唤醒的作用,如下man手册中所说。但是如果要求行为是可预知的,那么最好加锁。在实际应用中,我们当然需要代码的行为是可预知的,所以我们在使用中要加锁。

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>

#define BUFFER_SIZE 5

int buffer[BUFFER_SIZE];
int count = 0;

pthread_mutex_t mutex;
pthread_cond_t cond_producer;
pthread_cond_t cond_consumer;

void* producer(void* arg) {
    for (int i = 0; i < 10; i++) {
        pthread_mutex_lock(&mutex);

        while (count == BUFFER_SIZE) {
            pthread_cond_wait(&cond_producer, &mutex);
        }

        buffer[count] = i;
        count++;
        printf("Produced: %d\n", i);

        pthread_cond_signal(&cond_consumer);

        pthread_mutex_unlock(&mutex);
        sleep(1);
    }
    return NULL;
}

void* consumer(void* arg) {
    for (int i = 0; i < 10; i++) {
        pthread_mutex_lock(&mutex);

        while (count == 0) {
            pthread_cond_wait(&cond_consumer, &mutex);
        }

        count--;
        int item = buffer[count];
        printf("Consumed: %d\n", item);

        pthread_cond_signal(&cond_producer);

        pthread_mutex_unlock(&mutex);
        sleep(2);
    }
    return NULL;
}

int main() {
    pthread_t prod_thread, cons_thread;

    pthread_mutex_init(&mutex, NULL);
    pthread_cond_init(&cond_producer, NULL);
    pthread_cond_init(&cond_consumer, NULL);

    pthread_create(&prod_thread, NULL, producer, NULL);
    pthread_create(&cons_thread, NULL, consumer, NULL);

    pthread_join(prod_thread, NULL);
    pthread_join(cons_thread, NULL);

    pthread_mutex_destroy(&mutex);
    pthread_cond_destroy(&cond_producer);
    pthread_cond_destroy(&cond_consumer);
    return 0;
}

1.2惊群问题

惊群问题说的是,如果有多个线程wait阻塞,这个时候一个线程调用signal,可能会唤醒多个线程。如下man手册中也说的是,pthread_cond_signal会唤醒至少一个线程,也就是说至少一个,也可能大于一个,大于一个就是惊群**(你只扔了一个米粒,导致鸡群都跑了过来,但是只有一只鸡能够吃到米粒,其它的米都白跑一趟,惊动集群)**。

本人在在linux环境下测试,从来没有出现过惊群问题。即使有惊群问题,通过如下典型的使用方式,在wait返回之后再次判断,也能避免惊群问题引入其它问题。

cpp 复制代码
while (count == 0) {
    pthread_cond_wait(&cond_consumer, &mutex);
}

1.3destroy无法返回问题

如果一个条件变量,有线程在wait这个条件变量。这个时候调用pthread_cond_destroy销毁条件变量,那么pthread_cond_destroy是会阻塞住,直到pthread_cond_wait返回之后,destroy才会成功。

在实际应用中,在类的析构函数里,或者进程的main函数返回时,需要销毁资源,往往会调用pthread_cond_destroy销毁条件变量,此时就要注意是不是有线程在wait。如果遇到destroy不返回的情况,要排查是不是有线程wait的情况。也可以使用pthread_cond_timedwait这个api,这个api不会一直等待,如果条件不满足,超时的时候,也会返回。

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>

pthread_mutex_t mutex;
pthread_cond_t cond_var;

void *wait_thread(void *arg) {
    pthread_mutex_lock(&mutex);

    struct timespec ts;
    clock_gettime(CLOCK_REALTIME, &ts);
    ts.tv_sec += 5;
    pthread_cond_timedwait(&cond_var, &mutex, &ts);
    // pthread_cond_wait(&cond_var, &mutex);
    printf("Wait thread resumed.\n");
    pthread_mutex_unlock(&mutex);
    return NULL;
}

void *destroy_thread(void *arg) {
    printf("before destroy condition.\n");
    pthread_cond_destroy(&cond_var);
    printf("after destroy condition.\n");
    return NULL;
}

int main() {
    pthread_t wait_tid, destroy_tid;

    pthread_mutex_init(&mutex, NULL);
    pthread_cond_init(&cond_var, NULL);

    pthread_create(&wait_tid, NULL, wait_thread, NULL);
    sleep(1);
    pthread_create(&destroy_tid, NULL, destroy_thread, NULL);

    pthread_join(wait_tid, NULL);
    pthread_join(destroy_tid, NULL);
    pthread_mutex_destroy(&mutex);
    return 0;
}

2内核态:完成变量

如下是内核完成变量使用的例子。

struct completion声明一个完成变量,init_completion初始化完成变量,wait_for_completion等待完成,complete唤醒等待线程。另外,complete_all可以唤醒所有的等待线程。

|------------------------|---------------------|
| 用户态条件变量 | 内核态完成变量 |
| pthread_cont_t | struct completion |
| pthread_cond_init | init_completion |
| pthread_cond_wait | wait_for_completion |
| pthread_cond_signal | complete |
| pthread_cond_broadcast | complete_all |

源码:

cpp 复制代码
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/completion.h>
#include <linux/delay.h>
#include <linux/kthread.h>

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple completion example module");

static struct completion my_completion;
static struct task_struct *my_thread;

static int my_thread_fn(void *data)
{
    printk(KERN_INFO "Thread is sleeping for 5 seconds...\n");
    ssleep(5);

    printk(KERN_INFO "Thread is completing...\n");
    complete(&my_completion);

    return 0;
}

static int __init my_module_init(void)
{
    printk(KERN_INFO "Loading my completion module...\n");

    init_completion(&my_completion);

    my_thread = kthread_run(my_thread_fn, NULL, "my_thread");
    if (IS_ERR(my_thread)) {
        printk(KERN_ERR "Failed to create thread\n");
        return PTR_ERR(my_thread);
    }

    wait_for_completion(&my_completion);
    printk(KERN_INFO "Thread has completed!\n");

    return 0;
}

static void __exit my_module_exit(void)
{
    printk(KERN_INFO "Unloading my completion module...\n");
}

module_init(my_module_init);
module_exit(my_module_exit);

Makefile:

obj-m += comp.o

all:

make -C /lib/modules/(shell uname -r)/build M=(PWD) modules

clean:

make -C /lib/modules/(shell uname -r)/build M=(PWD) clean

相关推荐
s_little_monster21 分钟前
【Linux】线程互斥之线程加锁
linux·运维·经验分享·redis·笔记·学习·学习方法
❀͜͡傀儡师36 分钟前
Docker部署禅道21.6开源版本
运维·docker·容器
来鸟 鸣间1 小时前
pidstat指令分析
linux
骇客野人1 小时前
【软考】论devops在企业信息系统开发中的应用
运维·devops
wayuncn1 小时前
黑龙江 GPU 服务器租用:开启高效计算新征程
运维·服务器·云计算·gpu算力·算力
长流小哥1 小时前
Linux网络协议栈深度解析:从数据封装到子网划分的底层架构
linux·网络协议·架构
facaixxx20241 小时前
e实例性能测评:Intel Xeon Platinum处理器,经济型入门级服务器
运维·服务器
冰冷的bin1 小时前
【Linux】解决ssh连接失败问题
linux·ssh
MobiCetus1 小时前
Linux Kernel 9
java·linux·运维·服务器·windows·ubuntu·gnu
一个小白5551 小时前
Linux,redis群集模式,主从复制,读写分离
linux·运维·数据库·centos