条件变量 vs 信号量: 如何选择适合你的多线程同步工具?

在多线程编程中, 条件变量(Condition Variable)和信号量(Semaphore)是常用的同步机制. 它们用于协调线程间的执行顺序, 避免竞争条件. 本文将介绍这两者的核心概念, 使用场景以及代码示例, 帮助读者更好地理解并在实际开发中使用.


条件变量: 等待/通知模式的核心

条件变量是一种线程同步机制, 用于实现线程间的等待/通知模式, 通常与互斥锁(std::mutex)一起使用, 用来协调线程的访问和等待.

核心功能

使用场景

  1. 线程间的通知机制: 当某个条件成立时, 通知其他线程继续执行.
  2. 生产者-消费者模型: 消费者线程等待资源, 生产者线程通知资源已准备好.
  3. 复杂条件同步: 当某些条件需要多个线程共同完成时, 条件变量可以用来协调.

常用方法

示例代码: 生产者-消费者模型

以下是使用条件变量实现生产者-消费者模型的代码示例:

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

class ThreadSafeQueue {
 private:
  std::queue<int> buffer;
  const unsigned int maxSize;
  std::mutex mtx;
  std::condition_variable notFull;
  std::condition_variable notEmpty;

 public:
  explicit ThreadSafeQueue(unsigned int size) : maxSize(size) {}

  void produce(int item) {
    std::unique_lock<std::mutex> lock(mtx);
    // 等待队列不满
    notFull.wait(lock, [this] { return buffer.size() < maxSize; });

    buffer.push(item);
    std::cout << "生产: " << item << " (缓冲区大小: " << buffer.size() << ")"
              << std::endl;

    // 通知消费者
    notEmpty.notify_one();
  }

  int consume() {
    std::unique_lock<std::mutex> lock(mtx);
    // 等待队列不空
    notEmpty.wait(lock, [this] { return !buffer.empty(); });

    int item = buffer.front();
    buffer.pop();
    std::cout << "消费: " << item << " (缓冲区大小: " << buffer.size() << ")"
              << std::endl;

    // 通知生产者
    notFull.notify_one();
    return item;
  }
};

int main() {
  ThreadSafeQueue queue(3);

  auto producer = [&queue]() {
    for (int i = 0; i < 10; ++i) {
      queue.produce(i);
      std::this_thread::sleep_for(std::chrono::milliseconds(100));
    }
  };

  auto consumer = [&queue]() {
    for (int i = 0; i < 10; ++i) {
      queue.consume();
      std::this_thread::sleep_for(std::chrono::milliseconds(150));
    }
  };

  std::jthread producerThread(producer);
  std::jthread consumerThread(consumer);
  return 0;
}

信号量: 资源计数的利器

信号量是一种计数器机制, 用于控制对共享资源的访问数量. C++20 引入了标准信号量类, 主要包括:

核心功能

使用场景

  1. 资源池管理: 限制同时访问共享资源的线程数量, 例如线程池.
  2. 任务队列: 控制多个线程对任务队列的并发访问.
  3. 简单的同步: 二值信号量可以用作轻量级的线程同步工具.
  4. 多线程限流: 在某些高并发场景中, 用信号量限制同时执行的线程数量.

常用方法

示例代码: 使用信号量实现生产者-消费者模型

#include <chrono>
#include <iostream>
#include <queue>
#include <semaphore>
#include <thread>

class ThreadSafeQueue {
 private:
  std::queue<int> buffer;
  std::mutex mtx;
  std::counting_semaphore<> notFull;
  std::counting_semaphore<> notEmpty;

 public:
  explicit ThreadSafeQueue(unsigned int size) : notFull(size), notEmpty(0) {}

  void produce(int item) {
    notFull.acquire();  // Wait until there's space in the queue
    {
      std::lock_guard<std::mutex> lock(mtx);
      buffer.push(item);
      std::cout << "生产: " << item << " (缓冲区大小: " << buffer.size() << ")"
                << std::endl;
    }
    notEmpty.release();  // Signal that an item is available
  }

  int consume() {
    notEmpty.acquire();  // Wait until there's an item in the queue
    int item;
    {
      std::lock_guard<std::mutex> lock(mtx);
      item = buffer.front();
      buffer.pop();
      std::cout << "消费: " << item << " (缓冲区大小: " << buffer.size() << ")"
                << std::endl;
    }
    notFull.release();  // Signal that space is available
    return item;
  }
};

int main() {
  ThreadSafeQueue queue(5);

  auto producer = [&queue]() {
    for (int i = 0; i < 10; ++i) {
      queue.produce(i);
      std::this_thread::sleep_for(std::chrono::milliseconds(100));
    }
  };

  auto consumer = [&queue]() {
    for (int i = 0; i < 10; ++i) {
      queue.consume();
      std::this_thread::sleep_for(std::chrono::milliseconds(150));
    }
  };

  std::jthread producerThread(producer);
  std::jthread consumerThread(consumer);

  return 0;
}

性能考虑

在选择同步机制时, 需要考虑以下性能因素:

性能因素条件变量信号量
上下文切换开销涉及线程阻塞和唤醒, 可能导致更多的上下文切换在简单场景下可能有更少的切换开销
内存开销需要额外的互斥锁, 内存开销较大内存占用相对较小
响应延迟适合需要精确控制的场景适合简单的计数场景
扩展性考虑更适合复杂的同步逻辑在高并发场景下可能更容易产生竞争

条件变量与信号量的区别

特性条件变量信号量
同步模式等待/通知模式计数器模式
依赖互斥锁需要与互斥锁一起使用不需要互斥锁
状态存储不存储状态, 通知后需配合条件判断自动维护计数器状态
适用场景线程间复杂的条件同步限制资源访问数量, 简单同步场景
通知行为只能通知当前等待的线程允许线程数量的动态调整

总结

条件变量和信号量是多线程编程中不可或缺的工具. 条件变量适用于复杂的条件判断和线程间的通知, 而信号量更适合资源访问限制和并发控制. 在实际开发中, 根据需求选择合适的工具, 并结合使用, 可以大幅提高程序的稳定性和效率. 希望通过本文的介绍, 您能更加熟练地运用这两种同步机制, 编写出高效, 可靠的多线程程序.

并发相关主题