系统编程之高效同步机制:条件变量

系统,编程,高效,同步,机制,条件,变量 · 浏览次数 : 178

小编点评

**条件变量**是一种用于阻塞线程等待条件表达式的机制。它允许系统把条件变量所在的线程挂起,直到在其它线程中通过相同的条件变量唤醒线程。 **条件变量的用途:** * 用于实现数据的生产消费模式或相关业务逻辑分布在不同的线程中,如果他们的执行顺序是有条件触发的。 * 在多个线程之间连接(join)并获取信息。 * 在数据处理完成后释放资源。 **条件变量的创建和销毁:** * 使用 `pthread_cond_init()` 初始化条件变量。 * 使用 `pthread_cond_destroy()` 释放条件变量所占用的资源。 **条件变量的用法:** 1. 在接口函数中使用 `pthread_mutex_lock()` 和 `pthread_cond_wait()` 来锁定锁,并阻塞线程直到条件变量进入等待状态。 2. 在唤醒线程中使用 `pthread_mutex_unlock()` 和 `pthread_cond_signal()` 来释放锁。 3. 在唤醒后再次使用 `pthread_mutex_lock()` 和 `pthread_cond_wait()` 来检查条件表达式的判断结果。 **条件变量的优缺点:** **优点:** * 可用于实现复杂的数据处理逻辑。 * 使用条件变量可以提高性能。 **缺点:** * 使用条件变量可能会阻塞线程,降低性能。 * 条件表达式的判断必须在锁 acquired 时进行,否则可能导致判断失效。

正文

以下内容为本人的学习笔记,如需要转载,请声明原文链接 微信公众号「ENG八戒」https://mp.weixin.qq.com/s/zy6Dmo_b3xMPPEO3HNxuuw

有一段时间没碰条件变量【condition variable】,快忘了它到底是啥。大概记得,之前是用来写底层接口,辅助实现安全的生产消费模式等等。

下面让我们来探讨一下条件变量的是非,简单起见接下来的所有接口函数和代码都基于 linux C。

用途

一般数据的生产消费或者相关业务逻辑分布在不同的线程中,如果他们的执行顺序是有条件触发的,那么就需要用到条件变量了。

条件变量允许系统把条件变量所在的线程 A 挂起,就是说条件变量阻塞了当前线程的执行,直到在其它线程中通过相同的条件变量唤醒线程 A.

这有点像俩小朋友在玩你追我赶的游戏,你不追过来,我就不动。

再比如,涉及到多线程的应用中,线程结束后资源是否会被自动回收,有赖于线程的属性配置。如果需要在一个线程里连接(join)另一个线程并获取信息,那么这个线程会被阻塞直到另一个线程结束。这种连接机制需要等待线程结束,所以也属于条件变量的特殊应用。

创建和销毁

使用 pthread_cond_t 类型定义的条件变量需要使用 pthread_cond_init 初始化。

int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *cond_attr);

使用 pthread_cond_init 初始化条件变量时,可以传入 pthread_condattr_t 类型变量指定条件变量的属性,如果属性是默认值可以传入 NULL,或者直接使用宏定义 PTHREAD_COND_INITIALIZER 代替 pthread_cond_init(此时,条件变量必须是静态变量)

static pthread_cond_t cond_variable = PTHREAD_COND_INITIALIZER;

条件变量在使用完毕后应该使用 pthread_cond_destroy 释放占用的资源。

int pthread_cond_destroy(pthread_cond_t *cond);

有个小细节,如果条件变量被分配在线程的栈上,该线程会维护一个条件变量的列表,那么在该线程被终止前必须先释放条件变量所占用的资源(调用 pthread_cond_destroy),否则会产生 memory corrupted 类似的错误。

等待和唤醒

条件变量说到底就是线程之间同步机制的众多方式中的一种,但是在使用过程中,必须搭配使用互斥锁。

为什么必须搭配使用互斥锁?

基本范式

先来看看一般的使用方式,比如,实现的数据队列中,推入数据的接口函数

void queue_push(void *data)
{
    pthread_mutex_lock(mutex);

    // 往队列中推入数据 data
    // ...

    pthread_cond_signal(cond_variable);

    pthread_mutex_unlock(mutex);
}

接着,提取数据的接口函数

void *queue_pop()
{
    pthread_mutex_lock(mutex);

    while (wait_condition) {
        pthread_cond_wait(cond_variable, mutex);
    }

    // 从队列中弹出数据 data
    // ...

    pthread_mutex_unlock(mutex);
}

上面代码中 cond_variable 是已初始化的条件变量。mutex 同样是经过初始化的互斥锁,类型是 pthread_mutex_t。wait_condition 是条件表达式,布尔类型,用于判断是否进入条件变量的等待。

提取数据的函数接口代码中,开始判断条件表达式之前,先占用互斥锁。当条件表达式为真,条件变量进入等待状态并且释放互斥锁(这是原子操作),所在线程就会被挂起,直到被其它线程通过条件变量 cond_variable 唤醒。唤醒后,再次尝试占用互斥锁,然后执行后续的数据处理(从队列中提取数据),在数据处理完成后释放互斥锁。

可以看到,条件表达式的使用要素有三个:条件变量、起保护作用的锁、仅起判断作用的条件表达式。

效能提升

其实,如果为了同步数据,单纯用锁也是能实现的,但是会长期占用系统资源,效率太低。比如下面把提取数据的接口函数写成

void *queue_pop_2()
{
    pthread_mutex_lock(mutex);

    while (wait_condition) {
        sleep(1);
    }

    // 从队列中弹出数据 data
    // ...

    pthread_mutex_unlock(mutex);
}

可见,如果仅用锁来实现接口,在每次提取数据之前都会空转固定的时间。如果数据队列中已经准备好数据,那么提取数据的操作需要等待最长可达一个周期(示例代码是 1 秒)。

条件变量和锁的配合充分利用了系统的能力,大大降低性能损耗,避免长时间占用锁。但要注意,使用锁不是为了保护条件变量自身,而是为了保护条件表达式的判断,防止在判断之后和条件变量进入等待状态之前,其它线程修改条件而导致判断失效,以及对目标数据逻辑的序列化执行(也就是同步)。

while (wait_condition) {
    pthread_cond_wait(cond_variable, mutex);
}

细看这段代码,发现唤醒后(也就是在条件变量退出等待和重新占用互斥锁之后),还要再次执行条件表达式的判断。这是因为唤醒之后等待的条件可能会被其它线程变更,为了安全起见需要重新检查条件,如果等待的条件为真,就再次进入等待状态直到下次被唤醒。

等待

条件变量等待的方式有两种,一种是持续等待,直到被唤醒

int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);

如果一直处于等待状态,如何退出?

pthread_cond_wait 提供了线程取消的功能,可通过 pthread_cancel 退出指定线程。

另一种是条件变量进入等待后,开始计时,在计时结束后仍然未被唤醒则主动退出等待并返回错误信息。

int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec *abstime);

计时结束指的是到达指定时刻。

唤醒

关于唤醒同样也有两种方式,其一是,仅唤醒一个正在等待的条件变量

int pthread_cond_signal(pthread_cond_t *cond);

如果有多个正在等待的条件变量,那么最终被唤醒的线程由系统调度策略确定。

其二是,唤醒所有正在等待的条件变量,这种方式非必要不使用,因为会对系统带来不必要的运行消耗,被称为惊群效应

int pthread_cond_broadcast(pthread_cond_t *cond);

希望以上的内容对你有所帮助,也欢迎联系我一起探讨


与系统编程之高效同步机制:条件变量相似的内容:

系统编程之高效同步机制:条件变量

让我们来探讨一下条件变量的是非,简单起见接下来的所有接口函数和代码都基于 linux C。

2.1 C/C++ 使用数组与指针

C/C++语言是一种通用的编程语言,具有高效、灵活和可移植等特点。C语言主要用于系统编程,如操作系统、编译器、数据库等;C语言是C语言的扩展,增加了面向对象编程的特性,适用于大型软件系统、图形用户界面、嵌入式系统等。C/C++语言具有很高的效率和控制能力,但也需要开发人员自行管理内存等底层资源,对于初学者来说可能会有一定的难度。

3.1 C/C++ 使用字符与指针

C/C++语言是一种通用的编程语言,具有高效、灵活和可移植等特点。C语言主要用于系统编程,如操作系统、编译器、数据库等;C语言是C语言的扩展,增加了面向对象编程的特性,适用于大型软件系统、图形用户界面、嵌入式系统等。C/C++语言具有很高的效率和控制能力,但也需要开发人员自行管理内存等底层资源,对于初学者来说可能会有一定的难度。

4.1 C/C++ 使用结构与指针

C/C++语言是一种通用的编程语言,具有高效、灵活和可移植等特点。C语言主要用于系统编程,如操作系统、编译器、数据库等;C语言是C语言的扩展,增加了面向对象编程的特性,适用于大型软件系统、图形用户界面、嵌入式系统等。C/C++语言具有很高的效率和控制能力,但也需要开发人员自行管理内存等底层资源,对于初学者来说可能会有一定的难度。

5.1 C/C++ 使用文件与指针

C/C++语言是一种通用的编程语言,具有高效、灵活和可移植等特点。C语言主要用于系统编程,如操作系统、编译器、数据库等;C语言是C语言的扩展,增加了面向对象编程的特性,适用于大型软件系统、图形用户界面、嵌入式系统等。C/C++语言具有很高的效率和控制能力,但也需要开发人员自行管理内存等底层资源,对于初学者来说可能会有一定的难度。

6.1 C/C++ 封装字符串操作

C/C++语言是一种通用的编程语言,具有高效、灵活和可移植等特点。C语言主要用于系统编程,如操作系统、编译器、数据库等;C语言是C语言的扩展,增加了面向对象编程的特性,适用于大型软件系统、图形用户界面、嵌入式系统等。C/C++语言具有很高的效率和控制能力,但也需要开发人员自行管理内存等底层资源,对于初学者来说可能会有一定的难度。

如何构建高效、可观的系统「GitHub 热点速览」

经典老项目 system-design 教你如何设计一个健壮的系统,新项目 noodle 教你如何提升教育效率,而后者甚至单日获得了 1,600 star,刚开源就获得了 6k+ 的 star。除了,新老项目的交锋,还有一些能帮上忙的周边工具,比如用来享受游戏编程的 raylib,搞定游戏系统妥妥的;清理的内存的 memreduct,则让你放心使用 Windows 系统。不想搬砖,又得实现需求?也许 MetaGPT 能帮上忙,内置多种工种,随时 cosplay 所需工种。

MES 与 PLC 的几种交互方式

在 MES 开发领域,想要从 PLC 获取数据就必须要和 PLC 有信号交互。高效准确的获取 PLC 数据一直是优秀 MES 系统开发的目标之一。初涉相关系统开发的工程师往往不能很好的理解 PLC 和 MES 之间编程逻辑的本质差别,在设计交互逻辑是难免顾此失彼。因此本文结合本人这些年来和 PLC

三个编程思想:面向对象编程、面向接口编程、面向过程编程【概念解析系列_1】【C# 基础】

对于 .Net 中的编程思想还是十分重要的,也是编码出高效的程序的基础!

西门子博途软件安装及使用

西门子博途软件安装及使用 一、博途软件的简介 博途软件可以对西门子300、400、1200及1500产品进行组态、编程和调试。TIA博途软件是一个系统,里面包含有多种软件,可以满足用户在不同自动化控制系统中的各种需求。因此,博途软件要求的电脑配置较高,且安装文件较大,但安装过程还算比较容易。以下为博