在使用互斥量的时候,最好使用RAII进行封装,使用非递归的互斥量,尽量在一个函数内进行lock、unlock。
常有的互斥量对象,简单的互斥对象std::mutex,带有超时机制的互斥对象std::timed_mutex,一般使用RAII来避免死锁的情况。
std::lock_guard,对象生存期内是不允许手动加锁解锁的。构造时可选是否加锁(不加锁时假定当前线程已经获得锁的所有权),析构时自动释放锁,所有权不可转移。
std::unique_lock,对象生存期可以进行手动加锁解锁。比lock_guard更加灵活。
lock_guard只支持std::lock和std::adopt_lock。
unique_lock则支持:std:lock、std::defer_lock、std::try_lock、std::adopt_lock。
defer_lock 不取得互斥量的所有权;当多个线程都使用同样N个互斥量的时候,必须保证其加锁的顺序是一致的,这种情况下使用try_lock更好。
try_lock 则会在没有阻塞的时候取得互斥量的所有权;其变种try_lock_for(duration)和try_lock_until(timepoint)。
adopt_lock 即使互斥量已经被另外的线程加锁,也会夺取互斥量的所有权进而在该线程加锁。
条件变量,是一个或多个线程等待某个布尔表达式为真,即等待别的线程“唤醒”它。
对于wait()端,为了防止虚假唤醒,必须配合锁一起使用:
1. 必须与mutex 一起使用,该布尔表达式的读写需受此mutex 保护
2. 在mutex 已上锁的时候才能调用wait()
3. 把判断布尔条件和wait() 放到while 循环中
示例:
MutexLock mutex;
Condition cond(mutex);
std::deque queue;
int dequeue()
{
MutexLockGuard lock(mutex);
while (queue.empty()) { // 必须用循环;必须在判断之后再 wait()
cond.wait(); // 这一步会原子地 unlock mutex 并进入 blocking,不会与 enqueue 死锁
}
assert(!queue.empty());
int top = queue.front();
queue.pop_front();
return top;
}
对于 signal/broadcast 端:
1. 不一定要在mutex 已上锁的情况下调用signal
2. 在signal 之前一般要修改布尔表达式
3. 修改布尔表达式通常要用mutex保护
void enqueue(int x)
{
MutexLockGuard lock(mutex);
queue.push_back(x);
cond.notify();
}
实现了一个简单的unbounded BlockingQueue。
条件变量是较底层的同步原语,很少直接使用,一般都是用它来实现高层的同步措施,如 BlockingQueue 或CountDownLatch
临界区
临界区是值一个访问共享资源的代码段。当有一个线程访问了临界区之后,其他线程想要访问临界区时会被挂起,直到进入临界区的线程离开。windows API提供了临界区对象结构体CRITICAL_SECTION,常用API有:
1. 申请一个临界区变量 CRITICAL_SECTION gSection;
2.InitializeCriticalSection(&gSection),初始化临界区,唯一的参数是指向结构体CRITICAL_SECTION的指针变量(LPCRITICAL_SECTION lpCriticalSection)。
3.EnterCriticalSection(&gSection),线程进入已经初始化的临界区,并拥有该临界区的所有权。这是一个阻塞函数,如果线程获得临界区的所有权成功,则该函数将返回,调用线程继续执行,否则该函数将一直等待,这样会造成该函数的调用线程也一直等待。如果不想让调用线程等待(非阻塞),则应该使用TryEnterCriticalSection(&gSection)。
4.LeaveCriticalSection(&gSection),线程离开临界区并释放对该临界区的所有权,以便让其他线程也获得访问该共享资源的机会。一定要在程序不适用临界区时调用该函数释放临界区所有权,否则程序将一直等待造成程序假死。
5.DeleteCriticalSection(&gSection),该函数的作用是删除程序中已经被初始化的临界区。如果函数调用成功,则程序会将内存中的临界区删除,防止出现内存错误。
未完待续。。。