跳到主要内容

线程同步

std::condition_variable​

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

std::queue<int> buffer;
std::mutex mtx;
std::condition_variable cv;
const unsigned int max_buffer_size = 5;

void producer() {
for (int i = 0; i < 10; ++i) {
std::unique_lock<std::mutex> lock(mtx);
// 等待直到缓冲区有空位
cv.wait(lock, [] { return buffer.size() < max_buffer_size; });
// 等价于:
// while (buffer.size() >= max_buffer_size) {
// cv.wait(lock);
// }

buffer.push(i);
std::cout << "Produced: " << i << std::endl;

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

void consumer() {
for (int i = 0; i < 10; ++i) {
std::unique_lock<std::mutex> lock(mtx);
// 等待直到缓冲区有数据
cv.wait(lock, [] { return !buffer.empty(); });
// 等价于:
// while (buffer.empty()) {
// cv.wait(lock);
// }

int value = buffer.front();
buffer.pop();
std::cout << "Consumed: " << value << std::endl;

// 通知生产者
cv.notify_one();
}
}

int main() {
std::thread producer_thread(producer);
std::thread consumer_thread(consumer);

producer_thread.join();
consumer_thread.join();

return 0;
}

虚假唤醒(spurious wakeup)​

“Spurious wakeup”指的是在多线程编程中,线程在没有任何条件被满足的情况下意外地被唤醒。这种现象常见于条件变量或信号量的使用中。

在使用条件变量时,线程通常会在某个条件不满足时等待。当另一个线程改变这个条件并通知等待的线程时,条件变量会将其唤醒。然而,有时线程可能会被唤醒而不满足条件,这就是“spurious wakeup”。

为了安全处理这种情况,通常建议在唤醒后,线程应该在循环中重新检查条件。例如:

std::unique_lock<std::mutex> lock(mutex);
while (!condition_met) {
condition_variable.wait(lock);
}
// 继续执行

此外 condition_variable.wait 第二个参数支持传入函数,该函数返回 true 表示通过检查。wait 前被调用检查,被唤醒后也会被调用检查是否是虚假唤醒。

读写锁使用场景​

读写锁不见得比普通互斥锁更快,因为读写锁要维护额外状态(当前的 reader 数目)。参考 Real-world Concurrency 中 Be wary of readers/writer locks 描述。

要想性能比普通互斥锁好,读写锁使用至少要满足2个条件:

  • 临界区域耗时长
  • 读多写少。
    • 这里的读写有2个含义:
      • 写写互斥,读写互斥
      • 读耗时短,写耗时长

用下面的代码可以简单测试互斥锁和读写锁性能:

读写锁和互斥锁性能测试
#include <chrono>
#include <iostream>
#include <mutex>
#include <shared_mutex>
#include <thread>
#include <vector>

int numReaders = 10; // 读线程数量
int numWriters = 1; // 写线程数量
int readIterations = 100; // 每个读线程的操作次数
int writeIterations = 10; // 每个写线程的操作次数
int readSleep = 1; // 读操作耗时(毫秒)
int writeSleep = 10; // 写操作耗时(毫秒)

// 共享数据
int sharedData = 0;

// 使用互斥锁
std::mutex mutexLock;

// 使用读写锁
std::shared_mutex rwLock;

void readerMutex() {
for (int i = 0; i < readIterations; ++i) {
mutexLock.lock();
volatile int data = sharedData; // 读取数据
if (readSleep > 0) {
std::this_thread::sleep_for(std::chrono::milliseconds(readSleep));
}
mutexLock.unlock();
}
}

void writerMutex() {
for (int i = 0; i < writeIterations; ++i) {
mutexLock.lock();
sharedData++; // 修改数据
if (writeSleep > 0) {
std::this_thread::sleep_for(std::chrono::milliseconds(writeSleep));
}
mutexLock.unlock();
}
}

void readerRWLock() {
for (int i = 0; i < readIterations; ++i) {
rwLock.lock_shared();
volatile int data = sharedData; // 读取数据
if (readSleep > 0) {
std::this_thread::sleep_for(std::chrono::milliseconds(readSleep));
}
rwLock.unlock_shared();
}
}

void writerRWLock() {
for (int i = 0; i < writeIterations; ++i) {
rwLock.lock();
sharedData++; // 修改数据
if (writeSleep > 0) {
std::this_thread::sleep_for(std::chrono::milliseconds(writeSleep));
}
rwLock.unlock();
}
}

int main(int argc, char *argv[]) {
if (argc < 5) {
std::cerr << "Usage: " << argv[0]
<< " numReaders numWriters readIterations writeIterations [readSleep writeSleep]\n";
std::cerr << "读多写少、临界区域耗时长: " << argv[0] << " 10 1 100 10\n";
std::cerr << "读少写多、临界区域耗时长: " << argv[0] << " 1 10 10 100\n";
std::cerr << "读多写少、临界区域耗时短: " << argv[0] << " 10 1 1000000 100000 0 0\n";
std::cerr << "全读场景、临界区域耗时短: " << argv[0] << " 100 0 1000000 0 0 0\n";
return 1;
}

numReaders = std::stoi(argv[1]);
numWriters = std::stoi(argv[2]);
readIterations = std::stoi(argv[3]);
writeIterations = std::stoi(argv[4]);

if (argc >= 6) {
readSleep = std::stoi(argv[5]);
}
if (argc >= 7) {
writeSleep = std::stoi(argv[6]);
}

// 测试互斥锁
auto startMutex = std::chrono::high_resolution_clock::now();

std::vector<std::thread> readers, writers;

for (int i = 0; i < numReaders; ++i) {
readers.emplace_back(readerMutex);
}
for (int i = 0; i < numWriters; ++i) {
writers.emplace_back(writerMutex);
}

for (auto &thread : readers) {
thread.join();
}
for (auto &thread : writers) {
thread.join();
}

auto endMutex = std::chrono::high_resolution_clock::now();
std::chrono::duration<double> elapsedMutex = endMutex - startMutex;
std::cout << "Mutex lock time: " << elapsedMutex.count() << " seconds\n";

// 测试读写锁
sharedData = 0; // 重置数据
auto startRWLock = std::chrono::high_resolution_clock::now();

readers.clear();
writers.clear();

for (int i = 0; i < numReaders; ++i) {
readers.emplace_back(readerRWLock);
}
for (int i = 0; i < numWriters; ++i) {
writers.emplace_back(writerRWLock);
}

for (auto &thread : readers) {
thread.join();
}
for (auto &thread : writers) {
thread.join();
}

auto endRWLock = std::chrono::high_resolution_clock::now();
std::chrono::duration<double> elapsedRWLock = endRWLock - startRWLock;
std::cout << "Read-Write lock time: " << elapsedRWLock.count() << " seconds\n";

return 0;
}
$ g++ -std=c++17 -pthread lock_perf.cc -o lock_perf -O2
$ ./lock_perf
Usage: ./lock_perf numReaders numWriters readIterations writeIterations [readSleep writeSleep]
读多写少、临界区域耗时长: ./lock_perf 10 1 100 10
读少写多、临界区域耗时长: ./lock_perf 1 10 10 100
读多写少、临界区域耗时短: ./lock_perf 10 1 1000000 100000 0 0
全读场景、临界区域耗时短: ./lock_perf 10 0 1000000 0 0 0

# 读多写少、临界区域耗时长
$ ./lock_perf 10 1 100 10
Mutex lock time: 1.19849 seconds
Read-Write lock time: 0.211016 seconds

# 读少写多、临界区域耗时长
$ ./lock_perf 1 10 10 100
Mutex lock time: 10.1363 seconds
Read-Write lock time: 10.1353 seconds

# 读多写少、临界区域耗时短
$ ./lock_perf 10 1 1000000 100000 0 0
Mutex lock time: 0.687213 seconds
Read-Write lock time: 1.03149 seconds

# 全读场景、临界区域耗时短
$ ./lock_perf 100 0 1000000 0 0 0
Mutex lock time: 4.77643 seconds
Read-Write lock time: 7.01287 seconds

可以看到当临界区域很小时,读写锁比互斥锁更慢。

锁争用(lock contention)​

锁不会导致性能显著下降,锁争用会。