并发控制(Concurrency Control)是指在多线程或多进程环境中,确保多个操作在共享资源上的访问不会发生冲突或产生不一致的情况。并发控制的核心目标是在允许并发操作的同时,保证系统的正确性、数据的一致性和完整性。
在并发环境下,不同的线程或进程可能会同时访问共享资源(例如变量、文件或数据库记录)。若没有适当的并发控制,可能会发生数据竞争(Race Condition)或死锁(Deadlock)等问题,导致系统出现错误或不稳定的状态。因此,并发控制是实现多线程或分布式系统时必须考虑的问题。
并发控制的主要问题
-
数据竞争(Race Condition):
当多个线程同时读取和修改共享变量,且访问顺序不确定时,可能会导致数据竞争。例如,一个线程正在写数据,另一个线程正在读取数据,读到的可能是不完整或错误的结果。 -
死锁(Deadlock):
当多个线程因相互等待对方释放资源而无限期地等待时,就会产生死锁。例如,线程A持有资源X并等待资源Y,而线程B持有资源Y并等待资源X,最终导致两个线程都无法继续。 -
资源饥饿(Starvation):
某些线程由于得不到所需资源而一直无法执行,称为资源饥饿。例如,优先级较低的线程可能由于高优先级线程的持续占用而一直得不到运行机会。 -
数据不一致:
由于缺乏正确的并发控制,可能导致多个线程读取和写入共享数据,最终数据出现不一致的问题。例如,多个线程同时更新一个计数器,可能会导致最终计数不准确。
并发控制的常见机制
并发控制的核心是通过协调多个线程或进程对共享资源的访问,确保操作的正确性。以下是常见的并发控制机制:
1. 锁(Lock)
锁是并发控制中最常用的工具,用于限制同一时间内只有一个线程访问某个资源。常见的锁类型包括:
-
互斥锁(Mutex):一种互斥机制,确保同一时间只有一个线程访问共享资源。其他线程必须等待,直到锁被释放。
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mtx;
void printSafe(const std::string& message) {
std::lock_guard<std::mutex> lock(mtx); // 自动加锁和解锁
std::cout << message << std::endl;
}
-
读写锁(Read-Write Lock):允许多个线程同时读取数据,但当一个线程写入数据时,其它读写线程都必须等待。
-
自旋锁(Spin Lock):一种简单的锁机制,线程在等待锁时会持续检查锁的状态,不进入休眠,适用于短时间的锁等待场景。
2. 信号量(Semaphore)
信号量是一种控制线程数目的同步机制。信号量有一个计数器,用于记录当前可用的资源数:
- 计数信号量:可以设置允许的最大并发线程数,适合控制对有限资源的访问。
- 二元信号量:与互斥锁相似,但可以实现更复杂的同步机制。
#include <iostream>
#include <thread>
#include <semaphore.h> // C++20 中引入
std::counting_semaphore<1> semaphore(1);
void sharedFunction() {
semaphore.acquire(); // 获取信号量
// 临界区代码
std::cout << "Executing critical section" << std::endl;
semaphore.release(); // 释放信号量
}
3. 条件变量(Condition Variable)
条件变量是一种同步机制,允许线程在特定条件下等待。条件变量通常与互斥锁一起使用,以便线程在条件不满足时进入等待状态,条件满足时被唤醒。
#include <iostream>
#include <thread>
#include <condition_variable>
#include <mutex>
std::mutex mtx;
std::condition_variable cv;
bool ready = false;
void waitFunction() {
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, [] { return ready; }); // 等待条件满足
std::cout << "Proceeding after condition met" << std::endl;
}
void signalFunction() {
{
std::lock_guard<std::mutex> lock(mtx);
ready = true; // 设置条件
}
cv.notify_all(); // 唤醒等待线程
}
4. 原子操作(Atomic Operation)
原子操作是一种不可中断的操作,即使在多线程环境下,也不会被其他线程干扰。C++提供std::atomic
来支持原子操作,可以有效避免数据竞争问题。
#include <iostream>
#include <atomic>
#include <thread>
std::atomic<int> counter(0);
void increment() {
counter.fetch_add(1); // 原子性增加1
}
并发控制的应用场景
- 银行账户系统:确保多个操作不会同时修改同一个账户的数据,防止因数据竞争导致的错误。
- 生产者-消费者模型:使用条件变量或信号量来控制生产者和消费者对缓冲区的访问,防止数据丢失或重复读取。
- 数据库系统:在多事务并发操作下确保数据的一致性和完整性,常使用锁和日志等方式实现并发控制。
并发控制的选择
- 锁机制适用于需要严格控制访问顺序的情况,但需小心避免死锁。
- 信号量更适合资源有限的场景,如限制同时访问某资源的线程数。
- 原子操作适用于简单的共享变量操作,代价低且操作简单,但仅限于一些基本操作。
- 条件变量适用于等待特定条件的场景,能够实现较高的线程协调效率。