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

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 总结

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值