同步操作就像排队一样,你做一件事,得等到它完成才能继续做下一件事。比如你在电商网站购物,点了“购买”按钮后,系统会马上查库存,这时候你会看到一个加载的提示,必须等到库存查完才能继续支付。如果库存不够,系统会报错,你就不能继续操作了。
异步操作则不同,你做一件事,不需要一直等着,可以继续做其他事情。比如你付完钱下单后,系统会给你发订单确认邮件。这个过程是异步的,你不用等着邮件发完,可以继续浏览网站或者做其他事情。邮件在后台自己发,就算发邮件出问题,也不影响你购物或者订单的有效性。异步
在多线程编程中,常常需要确保多个线程对共享资源的访问不会产生数据竞争。为此,我们使用同步机制来保证线程安全。
在Qt/C++中,常见的同步机制包括互斥锁(QMutex、std::mutex)、信号量(QSemaphore)、读写锁(QReadWriteLock)、原子操作(QAtomicInt 等)条件变量(QWaitCondition、std::condition_variable)将详细介绍这些机制,配合代码示例和注释,帮助你理解这些工具在多线程中的应用。
互斥锁是一种常见的同步工具,用于防止多个线程同时进入临界区(共享资源的代码段)。
在任何时刻,只有一个线程可以持有互斥锁并进入临界区,其他线程必须等待锁被释放后才能继续执行。
示例代码及注释(QMutex)
#include
#include
#include
QMutex mutex;
int sharedResource = 0;
class Worker : public QThread
{
public:
void run() override {
mutex.lock();
std::cout << "Thread " << QThread::currentThreadId() << " is entering the critical section." << std::endl;
sharedResource++;
std::cout << "Shared Resource: " << sharedResource << std::endl;
mutex.unlock();
}
};
int main()
{
Worker worker1, worker2;
worker1.start();
worker2.start();
worker1.wait();
worker2.wait();
return 0;
}
互斥锁的锁定与解锁:使用 mutex.lock() 锁定互斥锁,保证同一时刻只有一个线程能够进入修改 sharedResource 的临界区。执行完临界区的代码后,必须调用 mutex.unlock() 解锁。
线程竞争:两个线程 worker1 和 worker2 竞争访问共享资源 sharedResource,通过互斥锁保证安全。
信号量是一种用于控制多个线程访问有限资源的同步机制。它允许多个线程进入临界区,但总数受到信号量的限制。可以看作是一个资源计数器,线程需要“获取”信号量才能继续执行,并在完成后“释放”信号量。
示例代码及注释(QSemaphore)
#include
#include
#include
QSemaphore semaphore(3); // 允许同时有3个线程进入
class Worker : public QThread
{
public:
void run() override {
semaphore.acquire(); // 获取信号量
std::cout << "Thread " << QThread::currentThreadId() << " is entering." << std::endl;
QThread::sleep(1); // 模拟工作
std::cout << "Thread " << QThread::currentThreadId() << " is leaving." << std::endl;
semaphore.release(); // 释放信号量
}
};
int main()
{
Worker worker1, worker2, worker3, worker4;
worker1.start();
worker2.start();
worker3.start();
worker4.start();
worker1.wait();
worker2.wait();
worker3.wait();
worker4.wait();
return 0;
}
信号量的获取与释放:semaphore.acquire() 减少可用资源计数器,semaphore.release() 增加资源计数器。只有计数器大于零时,线程才能继续执行。并发限制:最多有3个线程可以同时进入临界区,超过的线程必须等待其他线程释放资源。
读写锁允许多个线程同时读取共享资源,但写操作是互斥的,即在写入时其他线程不能进行读或写操作。这适合多读少写的场景,提升了性能。
示例代码及注释(QReadWriteLock)
#include
#include
#include
QReadWriteLock lock;
int sharedResource = 0;
class Reader : public QThread {
public:
void run() override {
lock.lockForRead(); // 获取读锁
std::cout << "Reader thread " << QThread::currentThreadId() << " reading: " << sharedResource << std::endl;
lock.unlock(); // 释放读锁
}
};
class Writer : public QThread {
public:
void run() override {
lock.lockForWrite(); // 获取写锁
sharedResource++;
std::cout << "Writer thread " << QThread::currentThreadId() << " writing: " << sharedResource << std::endl;
lock.unlock(); // 释放写锁
}
};
int main()
{
Reader reader1, reader2;
Writer writer1;
reader1.start();
reader2.start();
writer1.start();
reader1.wait();
reader2.wait();
writer1.wait();
return 0;
}
读写锁:lock.lockForRead() 允许多个线程同时读取,lock.lockForWrite() 使得写入操作期间其他读写线程必须等待。读写互斥:写线程 writer1 阻塞其他读线程,直到写操作完成后其他线程才能继续读。
原子操作是最轻量的同步机制之一,它通过硬件保证增减操作的原子性,不需要加锁。适合计数器等简单的共享数据操作。
示例代码及注释(QAtomicInt)
#include
#include
#include
QAtomicInt atomicCounter = 0;
class Worker : public QThread {
public:
void run() override {
for (int i = 0; i < 1000; ++i)
{
atomicCounter.ref(); // 原子增操作
}
}
};
int main()
{
Worker worker1, worker2;
worker1.start();
worker2.start();
worker1.wait();
worker2.wait();
std::cout << "Final counter: " << atomicCounter.load() << std::endl; // 输出最终计数值
return 0;
}
原子性操作:atomicCounter.ref() 是一个原子操作,无需加锁。硬件保证它在多个线程间的安全性。轻量同步:适合简单的增减计数操作,避免了使用锁带来的性能开销。
5、条件变量(QWaitCondition /std::condition_variable)
条件变量用于线程间的同步,它允许线程等待某个条件的满足。当条件满足时,其他线程会被唤醒并继续执行。示例代码及注释(QWaitCondition)
#include
#include
#include
#include
QMutex mutex;
QWaitCondition condition;
bool ready = false;
class Worker : public QThread
{
public:
void run() override {
mutex.lock();
while (!ready) {
condition.wait(&mutex); // 等待条件满足
}
std::cout << "Thread " << QThread::currentThreadId() << " is processing." << std::endl;
mutex.unlock();
}
};
int main() {
Worker worker1, worker2;
worker1.start();
worker2.start();
QThread::sleep(1); // 模拟准备时间
mutex.lock();
ready = true;
condition.wakeAll(); // 唤醒所有等待线程
mutex.unlock();
worker1.wait();
worker2.wait();
return 0;
}
条件变量等待:condition.wait(&mutex) 会使线程等待,并在等待期间释放互斥锁。一旦条件满足,线程会被唤醒并重新获取锁。条件变量唤醒:condition.wakeAll() 唤醒所有等待线程,线程会检查条件是否已满足并继续执行。
在多线程编程中,选择合适的同步机制非常重要。根据不同场景和需求,互斥锁、信号量、读写锁、原子操作、条件变量各有其适用范围:互斥锁 用于保护临界区,保证同一时刻只有一个线程访问共享资源。信号量 适合控制对有限资源的并发访问。读写锁 在多读少写的情况下能提供更好的性能。原子操作 是轻量级的同步方式,适合简单的计数操作。条件变量 则用于等待某个条件满足的线程间同步。