当前位置:   article > 正文

经典并发问题的深度分析与实现【c++与golang】【万字分析】

并发问题


前言

前置知识点:
锁与信号量
经典的多线程并发问题,需要考虑线程之间的同步和互斥,常用的解决方法包括互斥锁、条件变量、信号量等。针对不同的问题,需要选择合适的解决方法,保证线程之间的正确同步。
经典的并发问题有以下这几个:
生产者-消费者问题:有一组生产者线程/进程和一组消费者线程/进程,它们共享一个有限容量的缓冲区。生产者负责将数据项放入缓冲区,消费者则从缓冲区中取出数据项进行处理
哲学家就餐问题:涉及到多个哲学家和多个餐叉,每个哲学家需要持有两个餐叉才能进餐。当多个哲学家同时想进餐时,可能会出现死锁。
读者-写者问题:涉及到多个线程对共享数据的读取和写入操作。读者线程可以并发地读取共享数据,写者线程需要独占地写入共享数据,读者和写者之间需要保持互斥。

本文分别
利用两种语言分别来解决这些并发问题。

一、生产者-消费者问题

生产者消费者问题是一个经典的并发问题,用于描述多线程或多进程间的同步和互斥问题。在生产者消费者问题中,有一组生产者线程/进程和一组消费者线程/进程,它们共享一个有限容量的缓冲区。生产者负责将数据项放入缓冲区,消费者则从缓冲区中取出数据项进行处理。问题的核心在于如何实现对共享缓冲区的同步访问,确保数据不会丢失或被重复处理。
生产者消费者问题的一些关键点:
同步:需要确保当缓冲区为空时,消费者不能从中取出数据;当缓冲区已满时,生产者不能向其中添加数据。这需要使用同步原语(如互斥锁、信号量或条件变量)来实现。
**互斥:**多个生产者和消费者线程/进程需要互斥地访问共享缓冲区,防止同时修改缓冲区导致的数据不一致问题。这通常使用互斥锁(Mutex)来实现。
缓冲区管理:需要实现一个适当的数据结构来存储缓冲区中的数据项,例如队列、栈或循环缓冲区。同时,需要考虑缓冲区的容量限制。

C++ 和 Golang 都提供了多种同步机制,可以用来实现生产者-消费者问题。C++ 中常用的同步设施包括互斥锁、条件变量、信号量等;Golang 中常用的同步设施包括 chan、sync.Mutex、sync.WaitGroup等。
在 C++ 中,需要使用 std::mutex 和 std::condition_variable 实现生产者-消费者问题。当生产者向队列中添加数据时,需要获取互斥锁;当消费者从队列中取出数据时,也需要获取互斥锁,并且需要使用条件变量来等待生产者添加数据的通知。
在 Golang 中,可以使用 chan 实现生产者-消费者问题。当生产者向 chan 中添加数据时,会自动阻塞,直到有消费者从 chan 中取出数据;当消费者从 chan 中取出数据时,会自动阻塞,直到有生产者向 chan 中添加数据

1、c++版本

使用互斥锁 mtx 和条件变量 cv 实现了一个生产者-消费者模型
生产者线程 t1 负责向队列 q 中添加数据,消费者线程 t2 和 t3 负责从队列 q 中取出数据。当队列 q 中有数据时,消费者线程可以立即消费;当队列 q 中没有数据时,消费者线程需要等待条件变量 cv,直到生产者线程添加数据并通知条件变量。

#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>

std::mutex mtx;
std::condition_variable cv;
std::queue<int> q;

void producer(int id) {
   
    for (int i = 0; i < 10; i++) {
   
        std::unique_lock<std::mutex> lock(mtx);
        q.push(i);
        std::cout << "Producer " << id << " produced " << i << std::endl;
        cv.notify_one();
    }
}

void consumer(int id) {
   
    for (int i = 0; i < 10; i++) {
   
        std::unique_lock<std::mutex> lock(mtx);
        while (q.empty()) {
   
            cv.wait(lock);
        }
        int val = q.front();
        q.pop();
        std::cout << "Consumer " << id << " consumed " << val << std::endl;
    }
}

int main() {
   
    std::thread t1(producer, 1);
    std::thread t2(consumer, 1);
    std::thread t3(consumer, 2);
    t1.join();
    t2.join();
    t3.join();
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46

2、golang版本

在 Golang 中,可以使用 chan 实现生产者-消费者问题。
使用 chan int 类型实现了一个生产者-消费者模型。生产者协程 producer 负责向 ch 中添加数据,消费者协程 consumer 负责从 ch 中取出数据。生产者协程和消费者协程之间通过 ch 通信。当 ch 中有数据时,消费者协程可以立即消费;当 ch 中没有数据时,消费者协程会阻塞,直到生产者协程添加数据。

package main

import "fmt"

func producer(id int, ch chan<- int) {
   
    for i := 0; i < 10; i++ {
   
        ch <- i
        fmt.Printf("Producer %d produced %d\n", id, i)
    }
}

func consumer(id int, ch <-chan int) {
   
    for i := 0; i < 10; i++ {
   
        val := <-ch
        fmt.Printf("Consumer %d consumed %d\n", id, val)
    }
}

func main() {
   
    ch := make(chan int)
    go producer(1, ch)
    go consumer(1, ch)
    go consumer(2, ch)
    select {
   }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31

二、哲学家就餐问题

有五位哲学家围坐在一个圆桌上,他们之间共享五根筷子。哲学家的生活包括两个行为:思考和进餐。当哲学家饿了,他们需要拿起左右两边的筷子才能开始进餐,进餐完毕后放下筷子继续思考。问题的关键在于如何设计一个并发算法,使得哲学家们能够同时进餐而不发生死锁或饿死的情况。
本文主要是对leedcode关于这个问题的题解展开。
leedcode哲学家进餐问题
首先我们要知道死锁的四个条件;
互斥条件:指进程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个进程占用。
请求与保持条件:指进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源已被其他进程占用,此时请求进程阻塞,但又对自己已获得的其它资源保持不放。
不剥夺条件:指进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。
循环等待条件:指若干进程间形成一种头尾相接的循环等待资源的关系。
解决办法:
1、同一时间只允许一个人进餐,想要进餐得获得互斥锁 (破坏请求与保持条件)
从效率角度上,这不是最好的解决方案。
2、同时拿起左右的叉子 (破坏请求与保持条件)
仅当哲学家的左,右两只筷子均可用时,才允许他拿起筷子进餐。
3、控制哲学家就餐数量 (这里有五个哲学家, 所以允许最多4个哲学可以进餐) (破坏循环等待条件)
4、限定就餐策略 (破坏死锁的循环等待条件)
规定奇数号的哲学家先拿起他左边的筷子,然后再去拿他右边的筷子;而偶数号的哲学家则先拿起他右边的筷子,然后再去拿他左边的筷子。
由于第一种方法效率过低。下面使用c++与g

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/我家自动化/article/detail/876738
推荐阅读
相关标签
  

闽ICP备14008679号