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

文章目录

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

1. Linux线程同步

1.1 条件变量

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

1.2 同步概念与竞态条件

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

1.3 条件变量函数

① 初始化:

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

② 销毁:

cpp 复制代码
int pthread_cond_destroy(pthread_cond_t *cond)

③ 等待条件满足:

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

④ 唤醒等待:

cpp 复制代码
int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);

示例代码1:

cpp 复制代码
#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);
}

执行函数,有以下输出:

cpp 复制代码
[root@localhost linux]# ./test.out
活动
活动
活动

示例代码2

直接利用condition_variable:

cpp 复制代码
#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 条件变量使用规范

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

等待条件代码:

cpp 复制代码
pthread_mutex_lock(&mutex);
while (条件为假)
pthread_cond_wait(cond, mutex);
修改条件
pthread_mutex_unlock(&mutex);

给条件发信号代码:

cpp 复制代码
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 读写锁的相关接口

设置读写优先:

cpp 复制代码
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 写者优先,但写者不能递归加锁
*/

初始化:

cpp 复制代码
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,const pthread_rwlockattr_t *restrict attr)

销毁

cpp 复制代码
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);

加锁 / 解锁

cpp 复制代码
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);

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

cpp 复制代码
#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;
}

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

cpp 复制代码
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 是许多无锁算法的核心操作。

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

cpp 复制代码
#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的使用

cpp 复制代码
#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 总结

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

相关推荐
谢尔登16 分钟前
【Webpack】Hash 码
算法·webpack·哈希算法
程序和我有一个能跑就行。20 分钟前
【Python】递归
数据结构·python·算法·递归
Grayson_Zheng20 分钟前
【数据结构】环形队列(循环队列)学习笔记总结
c语言·数据结构·算法
背水36 分钟前
BFS之最短路径模型
算法·宽度优先
乐思智能科技有限公司37 分钟前
C语言编写一个五子棋游戏-代码实例讲解与分析
c语言·开发语言·嵌入式硬件·算法·游戏
漂流瓶jz38 分钟前
UVA-690 流水线调度 题解答案代码 算法竞赛入门经典第二版
c++·算法·深度优先·aoapc·算法竞赛入门经典·uva
CopyLower1 小时前
什么东西可以当做GC Root,跨代引用如何处理?
java·jvm·算法
咖啡里的茶i1 小时前
C++之Swap类
开发语言·c++·算法
Neituijunsir1 小时前
2024.09.18 校招 实习 内推 面经
人工智能·python·算法·面试·自动驾驶·汽车·求职招聘
AlenTech1 小时前
CentOS 替换 yum源 经验分享
linux·运维·centos