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

相关推荐
A小辣椒1 天前
TShark:Wireshark CLI 功能
linux
A小辣椒1 天前
TShark:基础知识
linux
AlfredZhao1 天前
OCI 明明分配了 200G 系统盘,为什么 df 只看到 30G?
linux·oci
AlfredZhao2 天前
vi 删除指定范围的行,不用再反复按 dd
linux·vi
用户9718356334662 天前
银河麒麟 KY10 申威(SW64) 安装 nginx-1.16.1-2.p01.ky10.sw_64.rpm 详细步骤
linux
猪脚踏浪2 天前
linux 拷贝文件或目录到指定的位置
linux
大树883 天前
金刚石散热越强,管路越先见顶
大数据·运维·服务器·人工智能·ai
摇滚侠3 天前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
霸道流氓气质3 天前
领域驱动设计(DDD)在 Spring Boot 微服务中的实践指南
运维·spring boot·微服务
bush43 天前
嵌入式linux学习记录十四、术语
linux·嵌入式