MST

星途 面试题库

面试题:并发与同步之多线程同步机制在生产者 - 消费者模型中的应用

请描述如何使用多线程同步机制(如互斥锁、条件变量等)实现一个生产者 - 消费者模型。假设生产者线程不断生成数据并放入缓冲区,消费者线程从缓冲区取出数据进行处理,要求详细说明同步机制的使用场景和作用,以及可能出现的问题(如死锁)及解决办法。
31.2万 热度难度
操作系统并发与同步

知识考点

AI 面试

面试题答案

一键面试
  1. 生产者 - 消费者模型概述
    • 生产者 - 消费者模型是一种经典的并发设计模式,用于在多线程或多进程环境中协调数据的生产和消费。生产者线程负责生成数据并将其放入共享缓冲区,消费者线程从共享缓冲区取出数据进行处理。
  2. 同步机制使用场景和作用
    • 互斥锁(Mutex)
      • 使用场景:用于保护共享资源(这里指缓冲区),防止多个线程同时访问缓冲区导致数据不一致。
      • 作用:保证在同一时刻只有一个线程能够访问缓冲区。例如,当生产者线程要向缓冲区写入数据时,它首先获取互斥锁,写完数据后释放互斥锁;消费者线程读取数据时同理。
    • 条件变量(Condition Variable)
      • 使用场景:用于线程间的同步通信,当缓冲区为空(消费者等待数据)或缓冲区已满(生产者等待空间)时使用。
      • 作用:当缓冲区为空时,消费者线程可以等待在条件变量上,当生产者向缓冲区写入数据后,通过条件变量通知等待的消费者线程;同理,当缓冲区已满时,生产者线程等待在条件变量上,消费者取出数据后通知生产者。
  3. 代码示例(以C++为例)
#include <iostream>
#include <queue>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <chrono>

std::mutex mtx;
std::condition_variable cv_producer;
std::condition_variable cv_consumer;
std::queue<int> buffer;
const int buffer_size = 5;
int data_to_produce = 0;

void producer() {
    while (true) {
        std::unique_lock<std::mutex> lock(mtx);
        // 等待缓冲区有空间
        cv_producer.wait(lock, [] { return buffer.size() < buffer_size; });
        buffer.push(data_to_produce++);
        std::cout << "Produced: " << buffer.back() << std::endl;
        lock.unlock();
        cv_consumer.notify_one();
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
    }
}

void consumer() {
    while (true) {
        std::unique_lock<std::mutex> lock(mtx);
        // 等待缓冲区有数据
        cv_consumer.wait(lock, [] { return!buffer.empty(); });
        int data = buffer.front();
        buffer.pop();
        std::cout << "Consumed: " << data << std::endl;
        lock.unlock();
        cv_producer.notify_one();
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
    }
}
  1. 可能出现的问题及解决办法
    • 死锁
      • 原因:如果两个或多个线程相互等待对方释放资源,就会出现死锁。例如,生产者线程获取了互斥锁,然后等待条件变量通知缓冲区有空间,而此时消费者线程也获取了互斥锁,等待条件变量通知缓冲区有数据,这样两个线程就会一直等待下去,造成死锁。
      • 解决办法
        • 避免嵌套锁:尽量减少锁的嵌套使用,在上述代码中,我们在等待条件变量时,使用std::unique_lock,它会在等待时自动释放互斥锁,在条件满足后重新获取锁,避免了死锁。
        • 按照固定顺序获取锁:如果必须使用多个锁,确保所有线程按照相同的顺序获取锁。例如,在复杂场景下如果还有其他锁,所有线程都先获取mtx,再获取其他锁。

在Java中,也有类似的实现方式,使用synchronized关键字和wait()notify()方法来实现生产者 - 消费者模型:

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

class Producer implements Runnable {
    private BlockingQueue<Integer> queue;
    private int dataToProduce = 0;

    public Producer(BlockingQueue<Integer> queue) {
        this.queue = queue;
    }

    @Override
    public void run() {
        try {
            while (true) {
                queue.put(dataToProduce++);
                System.out.println("Produced: " + (dataToProduce - 1));
                Thread.sleep(100);
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}

class Consumer implements Runnable {
    private BlockingQueue<Integer> queue;

    public Consumer(BlockingQueue<Integer> queue) {
        this.queue = queue;
    }

    @Override
    public void run() {
        try {
            while (true) {
                int data = queue.take();
                System.out.println("Consumed: " + data);
                Thread.sleep(100);
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}

public class ProducerConsumerExample {
    public static void main(String[] args) {
        BlockingQueue<Integer> queue = new LinkedBlockingQueue<>(5);
        Thread producerThread = new Thread(new Producer(queue));
        Thread consumerThread = new Thread(new Consumer(queue));
        producerThread.start();
        consumerThread.start();
    }
}

这里BlockingQueue内部已经实现了同步机制,put()方法在队列满时会等待,take()方法在队列空时会等待,从而避免了手动管理锁和条件变量时可能出现的死锁等问题。