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

news/2024/9/23 14:25:56/

文章目录

  • 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.ppmy.cn/news/1510960.html

相关文章

浅谈企业数字化转型的认知、价值及策略

2024年作为不寻常的一年&#xff0c;企业的经营环境发生了显著变化&#xff0c;复杂、不确定、不可预测成为常态。在新常态下&#xff0c;野蛮生长模式转向更务实的精耕细作。 同时&#xff0c;在诸多不确定的因素中&#xff0c;数字化加速推进的趋势是确定无疑的。数字化以前…

声明serialVersionUID进行Serializable接口版本控制

本文目标&#xff1a;开发人员&#xff0c;在了解serialVersionUID作用的条件下&#xff0c;进行序列化相关类的定义操作&#xff0c;达到版本可控的程度。 文章目录 1 场景1.1 Apple类定义1.2 apple对象序列化并存入文件1.3 读取文件反序列化得到apple对象 2 要点2.1 类要实现…

JaCoCo 命令行界面 (CLI) 详细分析与总结

概述 JaCoCo 提供了一个命令行界面&#xff0c;允许用户在命令行中执行基本操作。命令行工具及其所有依赖项打包在 jacococli.jar 中&#xff0c;并随 JaCoCo 下载提供。执行这些工具需要 Java 1.5 或更高版本。 注意事项 虽然提供了 instrument 命令&#xff0c;但 JaCoCo …

使用 Python 进行 PDF 文件加密

使用 Python 解密加密的 PDF 文件-CSDN博客定义一个名为的函数&#xff0c;该函数接受三个参数&#xff1a;输入的加密 PDF 文件路径input_pdf、输出的解密 PDF 文件路径output_pdf和密码password。https://blog.csdn.net/qq_45519030/article/details/141256661 在数字化时代…

vue前端可以完整的显示编辑子级部门,用户管理可以为用户分配角色和部门?

用户和角色是一对多的关系用户和部门是多对多得关系<template><div class="s"><!-- 操作按钮 --><div class="shang"><el-input v-model="searchText" placeholder="请输入搜索关键词" style="width:…

iOS在设置css的filter属性不生效

问题 blur值太大 // css相关代码 {width: 875px;height: 875px;border-radius: 875px;background: #ffda10;filter: blur(250px); }解决 // 启用硬件加速 // 1 {width: 875px;height: 875px;border-radius: 875px;background: #ffda10;filter: blur(250px);will-change: fil…

CSS方向选择的艺术:深入探索:horizontal和:vertical伪类

CSS&#xff08;层叠样式表&#xff09;是构建网页视觉表现的核心工具。随着CSS规范的不断更新&#xff0c;我们拥有了更多的选择器来精确控制网页元素的样式。其中&#xff0c;:horizontal和:vertical伪类是CSS Level 4中引入的两个实验性选择器&#xff0c;它们允许开发者根据…

Unified 阻抗控制 architecture、framework、approach

Unified 阻抗控制&#xff08;Unified Impedance Control&#xff09;作为一种控制策略&#xff0c;其architecture&#xff08;架构&#xff09;、framework&#xff08;框架&#xff09;和approach&#xff08;方法&#xff09;为&#xff1a; 一、Unified 阻抗控制 Archite…