【Linux多线程】线程同步 与 生产者消费者模型(无锁化模型)

news2024/9/20 18:54:40

文章目录

  • 1. Linux线程同步
    • 1.1 条件变量
    • 1.2 同步概念与竞态条件
    • 1.3 条件变量函数
      • 示例代码1:
      • 示例代码2
    • 1.4 为什么 pthread_ cond_ wait 需要互斥量
    • 1.5 条件变量使用规范
  • 2. 生产者消费者模型
  • 3. 读者 写者 问题
    • 3.1 读写锁
    • 3.2 读写锁的相关接口
  • 4. 扩展:无锁化模型
    • 4.1 基本概念
    • 4.2 常见的无锁数据结构和算法
    • 4.3 实现无锁编程的关键技术
      • ① 示例:无锁栈的简单实现
      • ② 原子操作 atomic的使用
    • 4.4 总结

1. Linux线程同步

1.1 条件变量

  • 当一个线程互斥地访问某个变量时,可能在其它线程改变状态之前,无法做任何操作。
  • 例如一个线程访问队列时,发现队列为空,它只能等待,只到其它线程将一个节点添加到队列中。这种情况就需要用到条件变量

1.2 同步概念与竞态条件

  • 同步:在保证数据安全的前提下,使线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题
  • 竞态条件因为时序问题,而导致程序异常。

1.3 条件变量函数

① 初始化:

int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);
  • 参数
    • cond:要初始化的条件变量
    • attr:NULL

② 销毁:

int pthread_cond_destroy(pthread_cond_t *cond)

③ 等待条件满足:

int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);
  • 参数
    • cond:要在这个条件变量上等待
    • mutex:互斥量,后面详细解释

④ 唤醒等待:

int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);

示例代码1:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>

pthread_cond_t cond;
pthread_mutex_t mutex;

void *r1( void *arg )
{
	while (true){
		pthread_cond_wait(&cond, &mutex);
		printf("活动\n");
	}
} 

void *r2(void *arg )
{
	while (true) {
		pthread_cond_signal(&cond);
		sleep(1);
	}
}

int main( void )
{
	pthread_t t1, t2;
	pthread_cond_init(&cond, NULL);
	pthread_mutex_init(&mutex, NULL);
	pthread_create(&t1, NULL, r1, NULL);
	pthread_create(&t2, NULL, r2, NULL);
	pthread_join(t1, NULL);
	pthread_join(t2, NULL);
	pthread_mutex_destroy(&mutex);
	pthread_cond_destroy(&cond);
}

执行函数,有以下输出:

[root@localhost linux]# ./test.out
活动
活动
活动

示例代码2

直接利用condition_variable:

#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable> // 条件变量

std::mutex mtx;
std::condition_variable cv;
bool ready = false;

void print_id(char id, bool flag)
{
    for(int i = 0; i < 10; ++i) {
        std::unique_lock<std::mutex> lck(mtx);
        cv.wait(lck, [&] { return ready == flag; });

        std::cout << "ThreadId: "<< id << std::endl;
        ready = !ready;

        cv.notify_all();
    }
}

int main() {
    std::thread t1(print_id, '1', true);
    std::thread t2(print_id, '2', false);
 
    t1.join();
    t2.join();
    
    return 0;
}

执行结果:

在这里插入图片描述


1.4 为什么 pthread_ cond_ wait 需要互斥量

  • 条件等待是线程间同步的一种手段,如果只有一个线程,条件不满足,则会一直等待下去
  • 所以必须有一个线程通过某些操作,改变共享变量,使原先不满足的条件变得满足,并且正常通知等待在条件变量上的线程
  • 条件不会无缘无故的突然满足,一定会关联到共享数据的变化。所以必须要用互斥锁保护。
  • 没有互斥锁就无法安全的获取和修改共享数据

1.5 条件变量使用规范

一般使用条件变量可以通过以下的方式:

等待条件代码:

pthread_mutex_lock(&mutex);
while (条件为假)
pthread_cond_wait(cond, mutex);
修改条件
pthread_mutex_unlock(&mutex);

给条件发信号代码:

pthread_mutex_lock(&mutex);
设置条件为真
pthread_cond_signal(cond);
pthread_mutex_unlock(&mutex);

2. 生产者消费者模型

关于生产者消费者模型的概念 与 代码实现,可以看下面的两篇文章:

  1. 基于互斥锁的生产者消费者模型
  2. 基于 BlockQueue(阻塞队列) 的 生产者消费者模型

3. 读者 写者 问题

3.1 读写锁

  • 在编写多线程时,有一种十分常见的情况:有些公共数据修改的机会较少。
  • 相比于改写,更多是执行读操作。 通常读的过程往往伴随着查找的操作,中间耗时长。给这种代码段加锁,会极大地降低程序效率。

有没有一种方法,可以更好的处理这种多读少写的情况呢?

有,即读写锁

在这里插入图片描述

对上图进行解释,即:

  • 读锁(Shared Lock):允许多个线程同时获得读锁,以进行读取操作,但在读锁被持有时,写锁无法获得
  • 写锁(Exclusive Lock)在写锁被持有时,其他线程既不能获得读锁也不能获得写锁。写锁保证独占访问以进行写操作。

3.2 读写锁的相关接口

设置读写优先:

int pthread_rwlockattr_setkind_np(pthread_rwlockattr_t *attr, int pref);
/*
pref 共有 3 种
Ⅰ PTHREAD_RWLOCK_PREFER_READER_NP (默认设置) 读者优先,可能会导致写者饥饿情况
Ⅱ PTHREAD_RWLOCK_PREFER_WRITER_NP 写者优先,目前有 BUG,导致表现行为和 Ⅰ 一致
Ⅲ PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP 写者优先,但写者不能递归加锁
*/

初始化:

int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,const pthread_rwlockattr_t *restrict attr)

销毁

int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);

加锁 / 解锁

int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);

下面写一个读写锁的简单示例:

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

// 共享资源
int shared_data = 0;

// 读写锁
pthread_rwlock_t rwlock;

// 读操作
void* read_operation(void* thread_id) {
    pthread_rwlock_rdlock(&rwlock);  // 获取读锁
    printf("Thread %ld reading data: %d\n", (long)thread_id, shared_data);
    // 模拟读取操作的延迟
    usleep(100000);
    pthread_rwlock_unlock(&rwlock);  // 释放锁
    return NULL;
}

// 写操作
void* write_operation(void* thread_id) {
    pthread_rwlock_wrlock(&rwlock);  // 获取写锁
    shared_data = (int)(long)thread_id * 10;
    printf("Thread %ld writing data: %d\n", (long)thread_id, shared_data);
    // 模拟写入操作的延迟
    usleep(100000);
    pthread_rwlock_unlock(&rwlock);  // 释放锁
    return NULL;
}

int main() {
    pthread_t threads[7];
    pthread_rwlockattr_t attr;

    // 初始化读写锁属性
    pthread_rwlockattr_init(&attr);
    // 设置读写锁属性(例如,锁的类型)
    pthread_rwlockattr_setkind_np(&attr, PTHREAD_RWLOCK_PREFER_WRITER_NP);

    // 初始化读写锁
    pthread_rwlock_init(&rwlock, &attr);

    // 启动几个读线程
    for (long i = 1; i <= 5; ++i) {
        pthread_create(&threads[i-1], NULL, read_operation, (void*)i);
    }

    // 启动几个写线程
    for (long i = 1; i <= 2; ++i) {
        pthread_create(&threads[i+4], NULL, write_operation, (void*)i);
    }

    // 等待所有线程完成
    for (int i = 0; i < 7; ++i) {
        pthread_join(threads[i], NULL);
    }

    // 销毁读写锁
    pthread_rwlock_destroy(&rwlock);
    pthread_rwlockattr_destroy(&attr);

    return 0;
}

执行上面的代码,会有以下的执行结果:

Thread 1 reading data: 0
Thread 2 reading data: 0
Thread 3 reading data: 0
Thread 4 reading data: 0
Thread 5 reading data: 0
Thread 6 writing data: 10
Thread 7 writing data: 20
Thread 1 reading data: 20
Thread 2 reading data: 20
Thread 3 reading data: 20
Thread 4 reading data: 20
Thread 5 reading data: 20

4. 扩展:无锁化模型

无锁化编程(Lock-Free Programming)是一种并发编程技术旨在避免使用传统的锁机制(如互斥锁、信号量等),从而减少锁带来的性能开销和复杂性

  • 无锁编程适用于高并发环境,因为它可以显著提高程序的吞吐量和响应性。

4.1 基本概念

  1. 无锁的定义

    • 在无锁编程中,算法或数据结构保证 至少有一个线程在有限时间内完成操作,而其他线程的操作不会导致全局进程的阻塞 。无锁化编程中的操作不使用传统的互斥锁机制,而是使用原子操作算法保证线程安全。
  2. 无锁的优势

    • 减少线程等待:由于不使用锁,线程不会因等待锁而被阻塞,从而减少了上下文切换的开销。
    • 提高并发性:无锁编程可以提高系统的吞吐量,因为多个线程可以并发地执行操作,不必等待其他线程完成。
    • 降低死锁风险:由于不使用锁,死锁和其他锁相关的问题(如优先级反转)不再是问题。
  3. 无锁编程的问题

  • 复杂性:编写无锁算法通常比传统的锁算法要复杂得多,需要对并发和原子操作有深入的理解。
  • 可见性问题:在多核系统中,需要确保线程对共享数据的修改对其他线程是可见的,这通常通过原子操作来保证。
  • 性能开销:无锁算法可能在某些情况下引入额外的性能开销,如自旋等待的开销。

4.2 常见的无锁数据结构和算法

  1. 无锁队列(Lock-Free Queue)

    • 无锁队列允许多个线程同时执行入队和出队操作,而不会导致阻塞。常用的实现方式包括基于环形缓冲区的队列和基于链表的队列。
  2. 无锁栈(Lock-Free Stack)

    • 无锁栈是一种支持并发推入(push)和弹出(pop)操作的数据结构,通常实现为链表结构,每个操作都通过原子操作来实现。
  3. 无锁哈希表(Lock-Free Hash Table)

    • 无锁哈希表允许并发地进行插入、删除和查找操作,常用的实现方式包括分段锁无锁哈希表和基于链表的无锁哈希表。
  4. 无锁计数器(Lock-Free Counter)

    • 无锁计数器用于支持高并发的增量操作,通常使用原子操作来保证每次对计数器的更新都是安全的。

4.3 实现无锁编程的关键技术

  1. 原子操作

    • 原子操作是无锁编程的基础,它保证了对共享数据的操作在执行时是不可分割的。在 C++ 中,std::atomic 提供了对原子操作的支持。
  2. 自旋锁(Spinlock)

    • 自旋锁是一种轻量级的锁机制,线程在尝试获取锁时,会在一个小的循环中反复检查锁的状态,而不是被阻塞。虽然自旋锁本身不是无锁的,但在无锁编程中,自旋锁可以用于实现某些无锁算法的关键部分。
  3. CAS(Compare-And-Swap)操作

    • CAS 是一种常用的原子操作,它用于比较一个值与预期值是否相等,如果相等则更新为新值。CAS 是许多无锁算法的核心操作。

① 示例:无锁栈的简单实现

#include <atomic>

template<typename T>
class LockFreeStack {
public:
    LockFreeStack() : head(nullptr) {}

    ~LockFreeStack() {
        while (Node* old_head = head.load()) {
            head.store(old_head->next);
            delete old_head;
        }
    }

    void push(const T& value) {
        Node* new_node = new Node(value);
        new_node->next = head.load();
        while (!head.compare_exchange_weak(new_node->next, new_node)) {
            // 如果 compare_exchange_weak 失败,说明 head 已经被其他线程修改了
            // 需要将 new_node->next 更新为当前 head 并重试
        }
    }

    bool pop(T& value) {
        Node* old_head = head.load();
        while (old_head && !head.compare_exchange_weak(old_head, old_head->next)) {
            // 如果 compare_exchange_weak 失败,说明 head 已经被其他线程修改了
            // 需要将 old_head 更新为当前 head 并重试
        }
        if (old_head) {
            value = old_head->data;
            delete old_head;
            return true;
        }
        return false;
    }

private:
    struct Node {
        T data;
        Node* next;
        Node(const T& value) : data(value), next(nullptr) {}
    };

    std::atomic<Node*> head;
};

在这个例子中,我们实现了一个无锁栈。std::atomic 用于保证对栈顶指针 head 的原子操作。compare_exchange_weak 是核心的无锁操作,用于在更新栈顶指针时避免数据竞争。


② 原子操作 atomic的使用

#include <iostream>
#include <thread>
#include <atomic>
#include <vector>

// 原子计数器
std::atomic<int> counter(0);

// 线程函数:增加计数器
void incrementCounter(int iterations) {
    for (int i = 0; i < iterations; ++i) {
        counter.fetch_add(1, std::memory_order_relaxed);
    }
}

int main() {
    const int numThreads = 4;
    const int iterationsPerThread = 1000;

    // 启动多个线程,每个线程增加计数器的值
    std::vector<std::thread> threads;
    for (int i = 0; i < numThreads; ++i) {
        threads.emplace_back(incrementCounter, iterationsPerThread);
    }

    // 等待所有线程完成
    for (auto& thread : threads) {
        thread.join();
    }

    // 输出最终计数器的值
    std::cout << "Final counter value: " << counter.load() << std::endl;

    return 0;
}

4.4 总结

无锁化编程通过避免传统锁机制,能够提高程序的并发性和性能。它的核心在于使用原子操作和无锁算法来确保线程安全,而不是依赖于锁来保护共享资源。虽然无锁编程可以提供显著的性能优势,但它也带来了更高的复杂性;

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2047838.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Python 如何创建和解析 XML 文件

XML&#xff08;可扩展标记语言&#xff09;是一种广泛使用的标记语言&#xff0c;主要用于存储和传输数据。它具有结构化、层次化的特点&#xff0c;常被用作数据交换格式。Python 提供了多种工具和库来处理 XML 文件&#xff0c;包括创建、解析和操作 XML 文档。 一、XML 简…

免费webp转jpeg或gif工具

1、”“添加webp文件&#xff1b;”-“移除webp文件&#xff1b;”>>“开始转换&#xff1b;”X“清空内容。 也可以把想要转换的文件全选&#xff0c;拖进窗口里。 2、默认将webp文件转换成同名的png文件放在原来的文件夹里。如果不是静态图片&#xff0c;则自动尝试转…

案例:ZooKeeper + Kafka消息队列集群部署

目录 消息队列 概念 使用场景 不适宜 适宜 消息队列的特征 存储 异步 异步的优点 同步 为什么需要消息队列 解耦 作用 冗余 扩展性 灵活性 峰值处理能力 可恢复性 顺序保证 Kafka 概念 Kafka技术名词 &#xff08;1&#xff09;Broker &#xff08;2&a…

基于Orangepi全志H616智能视觉垃圾分类系统

目录 一、功能需求 二、Python的安装和环境搭建 三、Python基础 3.1 Python的特点&#xff1a; 3.2 Python的基础学习&#xff1a; 3.3 字典的多层嵌套&#xff1a; 四、C语言调用Python 4.1 搭建编译环境&#xff1a; 4.2 C语言执行Python语句&#xff1a; 4.3 C语言…

22 注意力机制—Transformer

目录 TransformerTransformer 架构对比 seq2seq多头注意力(Multi-head attention)带掩码的多头注意力(Masked Multi-head attention)基于位置的前馈网络(Positionwise FFN)残差连接和归一化(Add & norm)(加 & 规范化)1、加入归一化能够更好地训练比较深的网络…

UE基础 —— 项目与模板

虚幻引擎 项目 包含游戏和应用程序的所有内容&#xff0c;并将所有内容联系在一起&#xff1b;包含磁盘上的许多文件夹和资产&#xff0c;如蓝图、材质、3D资产、动画等&#xff1b;内容浏览器与磁盘上的文件夹和文件夹结构相同&#xff1b; 每个项目都有与之关联的.uproject文…

性能优化理论篇 | 彻底弄懂系统平均负载

Linux 上的进程状态 要讨论系统平均负载&#xff0c;首先要了解Linux 上的进程状态。 标志名称内核名称及解释R运行中或可运行TASK_RUNNING。进程正在执行或等待执行。可以在用户空间&#xff08;用户代码&#xff09;或内核空间&#xff08;内核代码&#xff09;中运行。S可…

【项目】基于Vue3.2+ElementUI Plus+Vite 通用后台管理系统

构建项目 环境配置 全局安装vue脚手架 npm install -g vue/cli-init打开脚手架图形化界面 vue ui创建项目 在图形化界面创建项目根据要求填写项目相关信息选择手动配置勾选配置项目选择配置项目然后我们就搭建完成啦&#x1f973;&#xff0c;构建可能需要一点时间&#xff0…

Navicat Premium Lite For Linux,一款免费的专业可视化 SQL 数据库设计工具,支持各种数据库并行连接,在业界可是大名鼎鼎!

Navicat Premium Lite For Linux&#xff0c;一款免费的专业可视化 SQL 数据库设计工具&#xff0c;支持各种数据库并行连接&#xff0c;在业界可是大名鼎鼎&#xff01; Navicat 是一个可视化数据库、数据表设计软件&#xff0c;支持MySQL、MariaDB、SQLite、MongoDB、Redshi…

论文阅读笔记:ST-MetaNet-1

目录 前言 摘要 CCS 关键词 介绍 时空相关性的复杂组合 空间相关性 时间相关性 时空相关性的多样性 本篇博客结语 前言 读这篇论文边读边学&#xff0c;每天坚持发博客&#xff0c;看到哪学到哪&#xff0c;这系列文章既有翻译&#xff0c;又有深度详细解释&#xff…

Rust学习笔记1--下载安装和使用

一、下载和安装&#xff1a; 官网&#xff1a;https://www.rust-lang.org/ 直接下载即可&#xff0c;windows&#xff1a;按照教程执行步骤。 二、使用&#xff1a; 2.1 在vscode中安装rust 2.2 编译与运行rust文件&#xff1a; 后缀名rs&#xff1a; 编译&#xff1a; …

org.springframework.boot.autoconfigure.AutoConfiguration.imports 配置没有生效

在spring3.x以后&#xff0c;自动配置需要配置在org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件中 如果你配置了却没生效&#xff0c;有可能是创建的目录不对&#xff0c;正常情况下, META-INF.spring 是一个两层目录&#xff0c;如果是从别的地方复制…

第51集《大佛顶首楞严经》

请大家打开讲义第 111 页。癸三&#xff0c;结责迷情。 当我们在修学首楞严王三昧的时候&#xff0c;要把握两个很重要的原则&#xff1a;第一个就是它修学的方法&#xff0c;第二个就是它修学的目标。 那么&#xff0c;首楞严王的修学方法是什么呢&#xff08;这一点蕅益大师…

零基础读懂 DDPM 数学推导

零基础读懂 DDPM 数学推导 完整PDF文件可以在工坊获得&#xff0c;以下是内容截图。

为何显示keyerror fruit,如何解决??

&#x1f3c6;本文收录于《CSDN问答解惑-专业版》专栏&#xff0c;主要记录项目实战过程中的Bug之前因后果及提供真实有效的解决方案&#xff0c;希望能够助你一臂之力&#xff0c;帮你早日登顶实现财富自由&#x1f680;&#xff1b;同时&#xff0c;欢迎大家关注&&收…

【docker综合篇】关于我用docker搭建了6个应用服务的事

最近一直在捣鼓docker&#xff0c;利用测试服务器&#xff0c;本着犯错就重来(重装系统)的大无畏精神&#xff0c;不断尝试&#xff0c;总结经验&#xff0c;然后在网上搜寻一些关于docker有关的服务镜像&#xff0c;并搭建起来。看着一个个服务在我的服务器跑起来&#xff0c;…

【QT】基于UDP/TCP/串口 的Ymodom通讯协议客户端

【QT】基于UDP/TCP/串口的Ymodom通讯协议客户端 前言Ymodom实现QT实现开源库的二次开发-1开源库的二次开发-2 串口方式实现TCP方式实现UDP方式实现补充&#xff1a;文件读取补充&#xff1a;QT 封装成EXE 前言 Qt 运行环境 Desktop_Qt_5_11_2_MSVC2015_64bit &#xff0c;基于…

PowerShell自动化Windows系统管理任务

​ 大家好&#xff0c;我是程序员小羊&#xff01; 前言 Windows系统管理涉及许多繁琐的任务&#xff0c;如用户管理、文件操作、系统更新、网络配置等。PowerShell作为Windows的命令行工具和脚本语言&#xff0c;可以极大地简化这些管理任务。本文将探讨如何使用PowerShell自动…

【教学类-75-01】20240817“通义万相图片最大化+透明png”的修图流程

背景需求&#xff1a; 打印了袜子配对的PDF模版&#xff0c;做预测试 【教学类-74-02】彩色袜子配对02--左右配对-CSDN博客文章浏览阅读497次&#xff0c;点赞10次&#xff0c;收藏9次。【教学类-74-02】彩色袜子配对02--左右配对https://blog.csdn.net/reasonsummer/article…

09:链表的介绍

链表 1、算法的定义2、链表 1、算法的定义 通俗的定义&#xff1a;解题的方法与步骤。       狭义的定义&#xff1a;对存储的数据的操作。       广义的定义&#xff1a;无论数据是如何存储的&#xff0c;对数据从操作都是一样的。 到目前为止我们可以通过2种结构来存储…