共享数据

为了更清楚地说明这一点,就需要考虑共享数据的同步问题,因为数据竞争很容易在共享数据上发生。如果并发地对数据进行非同步读写访问,则会产生未定义行为。

验证并发、未同步的读写操作的最简单方法,就是向std::cout写入一些内容。

让我们来看一下,使用不同步的方式进行std::cout打印输出。

  1. // coutUnsynchronised.cpp
  2. #include <chrono>
  3. #include <iostream>
  4. #include <thread>
  5. class Worker {
  6. public:
  7. Worker(std::string n) :name(n) {}
  8. void operator()() {
  9. for (int i = 1; i <= 3; ++i) {
  10. // begin work
  11. std::this_thread::sleep_for(std::chrono::microseconds(200));
  12. // end work
  13. std::cout << name << ": " << "Work " << i << " done !!!" << std::endl;
  14. }
  15. }
  16. private:
  17. std::string name;
  18. };
  19. int main() {
  20. std::cout << std::endl;
  21. std::cout << "Boss: Let's start working.\n\n";
  22. std::thread herb = std::thread(Worker("Herb"));
  23. std::thread andrei = std::thread(Worker(" Andrei"));
  24. std::thread scott = std::thread(Worker(" Scott"));
  25. std::thread bjarne = std::thread(Worker(" Bjarne"));
  26. std::thread bart = std::thread(Worker(" Bart"));
  27. std::thread jenne = std::thread(Worker(" Jenne"));
  28. herb.join();
  29. andrei.join();
  30. scott.join();
  31. bjarne.join();
  32. bart.join();
  33. jenne.join();
  34. std::cout << "\n" << "Boss: Let's go home." << std::endl;
  35. std::cout << std::endl;
  36. }

该程序描述了一个工作流程:老板有六个员工(第29 - 34行),每个员工必须处理3个工作包,处理每个工作包需要200毫秒(第13行)。当员工完成了他的所有工作包时,他向老板报告(第15行)。当老板收到所有员工的报告,老板就会把员工们送回家(第43行)。

这么简单的工作流程,输出却如此混乱。

互斥量的问题 - 图1

让输出变清晰的最简单解决方法,就是使用互斥量。

互斥量

Mutex是互斥(mutual exclusion)的意思,它确保在任何时候只有一个线程可以访问临界区。

通过使用互斥量,工作流程的混乱变的和谐许多。

  1. // coutSynchronised.cpp
  2. #include <chrono>
  3. #include <iostream>
  4. #include <mutex>
  5. #include <thread>
  6. std::mutex coutMutex;
  7. class Worker {
  8. public:
  9. Worker(std::string n) :name(n) {}
  10. void operator()() {
  11. for (int i = 1; i <= 3; ++i) {
  12. // begin work
  13. std::this_thread::sleep_for(std::chrono::microseconds(200));
  14. // end work
  15. coutMutex.lock();
  16. std::cout << name << ": " << "Work " << i << " done !!!" << std::endl;
  17. coutMutex.unlock();
  18. }
  19. }
  20. private:
  21. std::string name;
  22. };
  23. int main() {
  24. std::cout << std::endl;
  25. std::cout << "Boss: Let's start working.\n\n";
  26. std::thread herb = std::thread(Worker("Herb"));
  27. std::thread andrei = std::thread(Worker(" Andrei"));
  28. std::thread scott = std::thread(Worker(" Scott"));
  29. std::thread bjarne = std::thread(Worker(" Bjarne"));
  30. std::thread bart = std::thread(Worker(" Bart"));
  31. std::thread jenne = std::thread(Worker(" Jenne"));
  32. herb.join();
  33. andrei.join();
  34. scott.join();
  35. bjarne.join();
  36. bart.join();
  37. jenne.join();
  38. std::cout << "\n" << "Boss: Let's go home." << std::endl;
  39. std::cout << std::endl;
  40. }

第8行中coutMutex保护了std::cout,第19行中的lock()和第21行中的unlock()调用,确保工作人员不会同时进行报告。

互斥量的问题 - 图2

std:: cout是线程安全的

C++11标准中,std::cout不需要额外的保护,每个字符都是原子式书写的。可能会有更多类似示例中的输出语句交织在一起的情况,但这些只是视觉问题,而程序则是定义良好的。所有全局流对象都是线程安全的,并且插入和提取全局流对象(std::coutstd::cinstd::cerrstd::clog)也都是线程安全的。

更正式地说:写入std::cout并不是数据竞争,而是一个竞争条件。这意味着输出内容的情况,完全取决于交错运行的线程。

C++11有4个不同的互斥量,可以递归地、暂时地锁定,并且不受时间限制。

成员函数 mutex recursive_mutex timed_mutex recursive_timed_mutex
m.lock yes yes yes yes
m.try_lock yes yes yes yes
m.try_lock_for yes yes
m.try_lock_until yes yes
m.unlock yes yes yes yes

递归互斥量允许同一个线程多次锁定互斥锁。互斥量保持锁定状态,直到解锁次数与锁定次数相等。可以锁定互斥量的最大次数默认并未指定,当达到最大值时,会抛出std::system_error异常。

C++14中有std::shared_timed_mutex,C++17中有std::shared_mutexstd::shared_mutexstd::shared_timed_mutex非常相似,使用的锁可以是互斥或共享的。另外,使用std::shared_timed_mutex可以指定时间点或时间段进行锁定。

成员函数 shared_timed_mutex shared_mutex
m.lock yes yes
m.try_lock yes yes
m.try_lock_for yes
m.try_lock_until yes
m.unlock yes yes
m.lock_shared yes yes
m.try_lock_shared yes yes
m.try_lock_shared_for yes
m.try_lock_shared_until yes
m.unlock_shared yes yes

std::shared_timed_mutex(std::shared_mutex)可以用来实现读写锁,也就可以使用std::shared_timed_mutex(std::shared_mutex)进行独占或共享锁定。如果将std::shared_timed_mutex(std::shared_mutex)放入std::lock_guardstd::unique_lock中,就可实现独占锁;如果将std::shared_timed_mutex(std::shared_lock)放入std::shared_lock中,就可实现共享锁。m.try_lock_for(relTime)m.try_lock_shared_for(relTime)需要一个时间段;m.try_lock_until(absTime)m.try_lock_shared_until(absTime)需要一个绝对的时间点。

m.try_lock(m.try_lock_shared)尝试锁定互斥量并立即返回。成功时,它返回true,否则返回false。相比之下,m.try_lock_for(m.try_lock_shared_for)m.try_lock_until(m.try_lock_shared_until)也会尝试上锁,直到超时或完成锁定,这里应该使用稳定时钟来限制时间(稳定的时钟是不能调整的)。

不应该直接使用互斥量,应该将互斥量放入锁中,下面解释下原因。

互斥量的问题

互斥量的问题可以归结为一个:死锁。

死锁

两个或两个以上的个线程处于阻塞状态,并且每个线程在释放之前都要等待其他线程的释放。

结果就是程序完全静止。试图获取资源的线程,通常会永久的阻塞程序。形成这种困局很简单,有兴趣了解一下吗?

异常和未知代码

下面的代码段有很多问题。

  1. std::mutex m;
  2. m.lock();
  3. sharedVariable = getVar();
  4. m.unlock();

问题如下:

  1. 如果函数getVar()抛出异常,则互斥量m不会被释放。
  2. 永远不要在持有锁的时候调用函数。因为m不是递归互斥量,如果函数getVar试图锁定互斥量m,则程序具有未定义的行为。大多数情况下,未定义行为会导致死锁。
  3. 避免在持有锁时调用函数。可能这个函数来自一个库,但当这个函数发生改变,就有陷入僵局的可能。

程序需要的锁越多,程序的风险就越高(非线性)。

不同顺序锁定的互斥锁

下面是一个典型的死锁场景,死锁是按不同顺序进行锁定的。

互斥量的问题 - 图3

线程1和线程2需要访问两个资源来完成它们的工作。当资源被两个单独的互斥体保护,并且以不同的顺序被请求(线程1:锁1,锁2;线程2:锁2,锁1)时,线程交错执行,线程1得到互斥锁1,然后线程2得到互斥锁2,从而程序进入停滞状态。每个线程都想获得另一个互斥锁,但需要另一个线程释放其需要的互斥锁。“死亡拥抱”这个形容,很好地描述了这种状态。

将这上图转换成代码。

  1. // deadlock.cpp
  2. #include <iostream>
  3. #include <chrono>
  4. #include <mutex>
  5. #include <thread>
  6. struct CriticalData {
  7. std::mutex mut;
  8. };
  9. void deadLock(CriticalData& a, CriticalData& b) {
  10. a.mut.lock();
  11. std::cout << "get the first mutex" << std::endl;
  12. std::this_thread::sleep_for(std::chrono::microseconds(1));
  13. b.mut.lock();
  14. std::cout << "get the second mutext" << std::endl;
  15. // do something with a and b
  16. a.mut.unlock();
  17. b.mut.unlock();
  18. }
  19. int main() {
  20. CriticalData c1;
  21. CriticalData c2;
  22. std::thread t1([&] {deadLock(c1, c2); });
  23. std::thread t2([&] {deadLock(c2, c1); });
  24. t1.join();
  25. t2.join();
  26. }

线程t1t2调用死锁函数(第12 - 23行),向函数传入了c1c2(第27行和第28行)。由于需要保护c1c2不受共享访问的影响,它们在内部各持有一个互斥量(为了保持本例简短,关键数据除了互斥量外没有其他函数或成员)。

第16行中,约1毫秒的短睡眠就足以产生死锁。

互斥量的问题 - 图4

这时,只能按CTRL+C终止进程。

互斥量不能解决所有问题,但在很多情况下,锁可以帮助我们解决这些问题。

锁使用RAII方式处理它们的资源。锁在构造函数中自动绑定互斥量,并在析构函数中释放互斥量,这大大降低了死锁的风险。

锁有四种不同的形式:std::lock_guard用于简单程序,std::unique_lock用于高级程序。从C++14开始就可以用std::shared_lock来实现读写锁了。C++17中,添加了std::scoped_lock,它可以在原子操作中锁定更多的互斥对象。

首先,来看简单程序。

std::lock_guard

  1. std::mutex m;
  2. m.lock();
  3. sharedVariable = getVar();
  4. m.unlock();

互斥量m可以确保对sharedVariable = getVar()的访问是有序的。有序指的是,每个线程按照某种顺序,依次访问临界区。代码很简单,但是容易出现死锁。如果临界区抛出异常或者忘记解锁互斥量,就会出现死锁。使用std::lock_guard,可以很优雅的解决问题:

  1. {
  2. std::mutex m,
  3. std::lock_guard<std::mutex> lockGuard(m);
  4. sharedVariable = getVar();
  5. }

代码很简单,但是前后的花括号是什么呢?std::lock_guard的生存周期受其作用域的限制,作用域由花括号构成。生命周期在达到右花括号时结束,std::lock_guard析构函数被调用,并且互斥量被释放。这都是自动发生的,如果sharedVariable = getVar()中的getVar()抛出异常,释放过程也会自动发生。函数作用域和循环作用域,也会限制实例对象的生命周期。

std::scoped_lock

C++17中,添加了std::scoped_lock。与std::lock_guard非常相似,但可以原子地锁定任意数量的互斥对象。

  1. 如果std::scoped_lock调用一个互斥量,它的行为就类似于std::lock_guard,并锁定互斥量m: m.lock。如果std::scoped_lock被多个互斥对象调用std::scoped_lock(mutextypes&…),则使用std::lock(m…)函数进行锁定操作。
  2. 如果当前线程已经拥有了互斥量,但这个互斥量不可递归,那么这个行为就是未定义的,很有可能出现死锁。
  3. 只需要获得互斥量的所有权,而不需要锁定它们。这种情况下,必须将标志std::adopt_lock_t提供给构造函数:std::scoped_lock(std::adopt_lock_t, mutextypes&…m)

使用std::scoped_lock,可以优雅地解决之前的死锁问题。下一节中,将讨论如何杜绝死锁。

std::unique_lock

std::unique_lockstd::lock_guard更强大,也更重量级。

除了包含std::lock_guard提供的功能之外,std::unique_lock还允许:

  • 创建无需互斥量的锁
  • 不锁定互斥量的情况下创建锁
  • 显式地/重复地设置或释放关联的互斥锁量
  • 递归锁定互斥量
  • 移动互斥量
  • 尝试锁定互斥量
  • 延迟锁定关联的互斥量

下表展示了std::unique_lock lk的成员函数:

成员函数 功能描述
lk.lock() 锁定相关互斥量
lk.try_lock() 尝试锁定相关互斥量
lk.try_lock_for(relTime) 尝试锁定相关互斥量
lk.try_lock_until(absTime) 尝试锁定相关互斥量
lk.unlock() 解锁相关互斥量
lk.release() 释放互斥量,互斥量保持锁定状态
lk.swap(lk2)std::swap(lk, lk2) 交换锁
lk.mutex() 返回指向相关互斥量的指针
lk.owns_lock()和bool操作符 检查锁lk是否有锁住的互斥量

try_lock_for(relTime)需要传入一个时间段,try_lock_until(absTime)需要传入一个绝对的时间点。lk.try_lock_for(lk.try_lock_until)会调用关联的互斥量mut的成员函数mut.try_lock_for(mut.try_lock_until)。相关的互斥量需要支持定时阻塞,这就需要使用稳定的时钟来限制时间。

lk.try_lock尝试锁定互斥锁并立即返回。成功时返回true,否则返回false。相反,lk.try_lock_forlk.try_lock_until则会让锁lk阻塞,直到超时或获得锁为止。如果没有关联的互斥锁,或者这个互斥锁已经被std::unique_lock锁定,那么lk.try_locklk.try_lock_forlk.try_lock_for则抛出std::system_error异常。

lk.release()返回互斥量,必须手动对其进行解锁。

std::unique_lock在原子步骤中可以锁定多个互斥对象。因此,可以通过以不同的顺序锁定互斥量来避免死锁。还记得在互斥量中出现的死锁吗?

  1. // deadlock.cpp
  2. #include <iostream>
  3. #include <chrono>
  4. #include <mutex>
  5. #include <thread>
  6. struct CriticalData {
  7. std::mutex mut;
  8. };
  9. void deadLock(CriticalData& a, CriticalData& b) {
  10. a.mut.lock();
  11. std::cout << "get the first mutex" << std::endl;
  12. std::this_thread::sleep_for(std::chrono::microseconds(1));
  13. b.mut.lock();
  14. std::cout << "get the second mutext" << std::endl;
  15. // do something with a and b
  16. a.mut.unlock();
  17. b.mut.unlock();
  18. }
  19. int main() {
  20. CriticalData c1;
  21. CriticalData c2;
  22. std::thread t1([&] {deadLock(c1, c2); });
  23. std::thread t2([&] {deadLock(c2, c1); });
  24. t1.join();
  25. t2.join();
  26. }

让我们来解决死锁问题。死锁必须原子地锁定互斥对象,也正是下面的程序中所展示的。

  1. // deadlockResolved.cpp
  2. #include <iostream>
  3. #include <chrono>
  4. #include <mutex>
  5. #include <thread>
  6. using namespace std;
  7. struct CriticalData {
  8. mutex mut;
  9. };
  10. void deadLock(CriticalData& a, CriticalData& b) {
  11. unique_lock<mutex> guard1(a.mut, defer_lock);
  12. cout << "Thread: " << this_thread::get_id() << " first mutex" << endl;
  13. this_thread::sleep_for(chrono::milliseconds(1));
  14. unique_lock<mutex> guard2(b.mut, defer_lock);
  15. cout << " Thread: " << this_thread::get_id() << " second mutex" << endl;
  16. cout << " Thread: " << this_thread::get_id() << " get both mutex" << endl;
  17. lock(guard1, guard2);
  18. // do something with a and b
  19. }
  20. int main() {
  21. cout << endl;
  22. CriticalData c1;
  23. CriticalData c2;
  24. thread t1([&] {deadLock(c1, c2); });
  25. thread t2([&] {deadLock(c2, c1); });
  26. t1.join();
  27. t2.join();
  28. cout << endl;
  29. }

如果使用std::defer_lockstd::unique_lock进行构造,则底层的互斥量不会自动锁定。此时(第16行和第21行),std::unique_lock就是互斥量的所有者。由于std::lock是可变参数模板,锁操作可以原子的执行(第25行)。

使用std::lock进行原子锁定

std::lock可以在原子的锁定互斥对象。std::lock是一个可变参数模板,因此可以接受任意数量的参数。std::lock尝试使用避免死锁的算法,在一个原子步骤获得所有锁。互斥量会锁定一系列操作,比如:locktry_lockunlock。如果对锁或解锁的调用异常,则解锁操作会在异常重新抛出之前执行。

本例中,std::unique_lock管理资源的生存期,std::lock锁定关联的互斥量,也可以反过来。第一步中锁住互斥量,第二步中std::unique_lock管理资源的生命周期。下面是第二种方法的例子:

  1. std::lock(a.mut, b.mut);
  2. std::lock_guard<std::mutex> guard1(a.mut, std::adopt_lock);
  3. std::lock_guard<std::mutex> guard2(b.mut, std::adopt_lock);

这两个方式都能解决死锁。

互斥量的问题 - 图5

使用std::scoped_lock解决死锁

C++17中解决死锁非常容易。有了std::scoped_lock帮助,可以原子地锁定任意数量的互斥。只需使用std::scoped_lock,就能解决所有问题。下面是修改后的死锁函数:

  1. // deadlockResolvedScopedLock.cpp
  2. ...
  3. void deadLock(CriticalData& a, CriticalData& b) {
  4. cout << "Thread: " << this_thread::get_id() << " first mutex" << endl;
  5. this_thread::sleep_for(chrono::milliseconds(1));
  6. cout << " Thread: " << this_thread::get_id() << " second mutex" << endl;
  7. cout << " Thread: " << this_thread::get_id() << " get both mutex" << endl;
  8. std::scoped_lock(a.mut, b.mut);
  9. // do something with a and b
  10. }
  11. ...

std::shared_lock

C++14中添加了std::shared_lock

std::shared_lockstd::unique_lock的接口相同,但与std::shared_timed_mutexstd::shared_mutex一起使用时,行为会有所不同。许多线程可以共享一个std::shared_timed_mutex (std::shared_mutex),从而实现读写锁。读写器锁的思想非常简单,而且非常有用。执行读操作的线程可以同时访问临界区,但是只允许一个线程写。

读写锁并不能解决最根本的问题——线程争着访问同一个关键区域。

电话本就是使用读写锁的典型例子。通常,许多人想要查询电话号码,但只有少数人想要更改。让我们看一个例子:

  1. // readerWriterLock.cpp
  2. #include <iostream>
  3. #include <map>
  4. #include <shared_mutex>
  5. #include <string>
  6. #include <thread>
  7. std::map<std::string, int> teleBook{ {"Dijkstra", 1972}, {"Scott", 1976},
  8. {"Ritchie", 1983} };
  9. std::shared_timed_mutex teleBookMutex;
  10. void addToTeleBook(const std::string& na, int tele) {
  11. std::lock_guard<std::shared_timed_mutex> writerLock(teleBookMutex);
  12. std::cout << "\nSTARTING UPDATE " << na;
  13. std::this_thread::sleep_for(std::chrono::milliseconds(500));
  14. teleBook[na] = tele;
  15. std::cout << " ... ENDING UPDATE " << na << std::endl;
  16. }
  17. void printNumber(const std::string& na) {
  18. std::shared_lock<std::shared_timed_mutex> readerLock(teleBookMutex);
  19. std::cout << na << ": " << teleBook[na];
  20. }
  21. int main() {
  22. std::cout << std::endl;
  23. std::thread reader1([] {printNumber("Scott"); });
  24. std::thread reader2([] {printNumber("Ritchie"); });
  25. std::thread w1([] {addToTeleBook("Scott",1968); });
  26. std::thread reader3([] {printNumber("Dijkstra"); });
  27. std::thread reader4([] {printNumber("Scott"); });
  28. std::thread w2([] {addToTeleBook("Bjarne", 1965); });
  29. std::thread reader5([] {printNumber("Scott"); });
  30. std::thread reader6([] {printNumber("Ritchie"); });
  31. std::thread reader7([] {printNumber("Scott"); });
  32. std::thread reader8([] {printNumber("Bjarne"); });
  33. reader1.join();
  34. reader2.join();
  35. reader3.join();
  36. reader4.join();
  37. reader5.join();
  38. reader6.join();
  39. reader7.join();
  40. reader8.join();
  41. w1.join();
  42. w2.join();
  43. std::cout << std::endl;
  44. std::cout << "\nThe new telephone book" << std::endl;
  45. for (auto teleIt : teleBook) {
  46. std::cout << teleIt.first << ": " << teleIt.second << std::endl;
  47. }
  48. std::cout << std::endl;
  49. }

第9行中的电话簿是共享变量,必须对其进行保护。八个线程要查询电话簿,两个线程想要修改它(第31 - 40行)。为了同时访问电话簿,读取线程使用std::shared_lock<std::shared_timed_mutex>(第23行)。写线程需要以独占的方式访问临界区,第15行中的std::lock_guard<std::shared_timed_mutex>具有独占性。最后,程序显示了更新后的电话簿(第55 - 58行)。

互斥量的问题 - 图6

屏幕截图显示,读线程的输出是重叠的,而写线程是一个接一个地执行。这就意味着,读取操作应该是同时执行的。

这很容易让“电话簿”有未定义行为。

未定义行为

程序有未定义行为。更准确地说,它有一个数据竞争。啊哈!?在继续之前,停下来想几秒钟。

数据竞争的特征是,至少有两个线程同时访问共享变量,并且其中至少有一个线程是写线程,这种情况很可能在程序执行时发生。使用索引操作符读取容器中的值,并可以修改它。如果元素在容器中不存在,就会发生这种情况。如果在电话簿中没有找到“Bjarne”,则从读访问中创建一对(“Bjarne”,0)。可以通过在第40行前面打印Bjarne的数据,强制数据竞争。

可以看到的是,Bjarne的值是0。

互斥量的问题 - 图7

修复这个问题的最直接的方法是使用printNumber函数中的读取操作:

  1. // readerWriterLocksResolved.cpp
  2. ...
  3. void printNumber(const std::string& na){
  4. std::shared_lock<std::shared_timed_mutex> readerLock(teleBookMutex);
  5. auto searchEntry = teleBook.find(na);
  6. if(searchEntry != teleBook.end()){
  7. std::cout << searchEntry->first << ": " << searchEntry->second << std::endl;
  8. }
  9. else{
  10. std::cout << na << " not found!" << std::endl;
  11. }
  12. }
  13. ...

如果电话簿里没有相应键值,就把键值写下来,并且向控制台输出“找不到!”。

互斥量的问题 - 图8

第二个程序执行的输出中,可以看到Bjarne的信息没有找到。第一个程序执行中,首先执行了addToTeleBook,所以Bjarne被找到了。

线程安全的初始化

如果变量从未修改过,那么就不需要锁或原子变量来进行同步,只需确保以线程安全的方式初始化就可以了。

C++中有三种以线程安全初始化变量的方法:

  • 常量表达式
  • std::call_oncestd::once_flag结合的方式
  • 作用域的静态变量

主线程中的安全初始化

以线程安全的方式初始化变量的最简单方法,是在创建任何子线程之前在主线程中初始化变量。

常数表达式

常量表达式,是编译器可以在编译时计算的表达式,隐式线程安全的。将关键字constexpr放在变量前面,会使该变量成为常量表达式。常量表达式必须初始化。

  1. constexpr double pi = 3.14;

此外,用户定义的类型也可以是常量表达式。不过,必须满足一些条件才能在编译时初始化:

  • 不能有虚方法或虚基类
  • 构造函数必须为空,且本身为常量表达式
  • 必须初始化每个基类和每个非静态成员
  • 成员函数在编译时应该是可调用的,必须是常量表达式

MyDouble的实例满足所有这些需求,因此可以在编译时实例化。所以,这个实例化是线程安全的。

  1. // constexpr.cpp
  2. #include <iostream>
  3. class MyDouble {
  4. private:
  5. double myVal1;
  6. double myVal2;
  7. public:
  8. constexpr MyDouble(double v1, double v2):myVal1(v1),myVal2(v2){}
  9. constexpr double getSum() const { return myVal1 + myVal2; }
  10. };
  11. int main() {
  12. constexpr double myStatVal = 2.0;
  13. constexpr MyDouble myStatic(10.5, myStatVal);
  14. constexpr double sumStat = myStatic.getSum();
  15. }

std::call_once和std::once_flag

通过使用std::call_once函数,可以注册一个可调用单元。std::once_flag确保已注册的函数只调用一次。可以通过相同的std::once_flag注册其他函数,只能调用注册函数组中的一个函数。

std::call_once遵循以下规则:

  • 只执行其中一个函数的一次,未定义选择哪个函数执行。所选函数与std::call_once在同一个线程中执行。
  • 上述所选函数的执行成功完成之前,不返回任何调用。
  • 如果函数异常退出,则将其传播到调用处。然后,执行另一个函数。

这个短例演示了std::call_oncestd::once_flag的应用(都在头文件<mutex>中声明)。

  1. // callOnce.cpp
  2. #include <iostream>
  3. #include <thread>
  4. #include <mutex>
  5. std::once_flag onceFlag;
  6. void do_once() {
  7. std::call_once(onceFlag, [] {std::cout << "Only once." << std::endl; });
  8. }
  9. void do_once2() {
  10. std::call_once(onceFlag, [] {std::cout << "Only once2." << std::endl; });
  11. }
  12. int main() {
  13. std::cout << std::endl;
  14. std::thread t1(do_once);
  15. std::thread t2(do_once);
  16. std::thread t3(do_once2);
  17. std::thread t4(do_once2);
  18. t1.join();
  19. t2.join();
  20. t3.join();
  21. t4.join();
  22. std::cout << std::endl;
  23. }

程序从四个线程开始(第21 - 24行)。其中两个调用do_once,另两个调用do_once2。预期的结果是“Only once”或“Only once2”只显示一次。

互斥量的问题 - 图9

单例模式保证只创建类的一个实例,这在多线程环境中是一个具有挑战性的任务。由于std::call_oncestd::once_flag的存在,实现这样的功能就非常容易了。

现在,单例以线程安全的方式初始化。

  1. // singletonCallOnce.cpp
  2. #include <iostream>
  3. #include <mutex>
  4. using namespace std;
  5. class MySingleton {
  6. private:
  7. static once_flag initInstanceFlag;
  8. static MySingleton* instance;
  9. MySingleton() = default;
  10. ~MySingleton() = default;
  11. public:
  12. MySingleton(const MySingleton&) = delete;
  13. MySingleton& operator=(const MySingleton&) = delete;
  14. static MySingleton* getInstance() {
  15. call_once(initInstanceFlag, MySingleton::initSingleton);
  16. return instance;
  17. }
  18. static void initSingleton() {
  19. instance = new MySingleton();
  20. }
  21. };
  22. MySingleton* MySingleton::instance = nullptr;
  23. once_flag MySingleton::initInstanceFlag;
  24. int main() {
  25. cout << endl;
  26. cout << "MySingleton::getInstance(): " << MySingleton::getInstance() << endl;
  27. cout << "MySingleton::getInstance(): " << MySingleton::getInstance() << endl;
  28. cout << endl;
  29. }

静态变量initInstanceFlag在第11行声明,在第31行初始化。静态方法getInstance(第20 - 23行)使用initInstanceFlag标志,来确保静态方法initSingleton(第25 - 27行)只执行一次。

default和delete修饰符

可以使用关键字default向编译器申请函数实现,编译器可以创建并实现它们。

delete修饰一个成员函数的话,则该函数不可用,因此不能被调用。如果尝试使用它们,将得到一个编译时错误。这里有default和delete的详细信息。

MySingleton::getIstance()函数显示了单例的地址。

互斥量的问题 - 图10

有作用域的静态变量

具有作用域的静态变量只创建一次,并且是惰性的,惰性意味着它们只在使用时创建。这一特点是基于Meyers单例的基础,以Scott Meyers命名,这是迄今为止C++中单例模式最优雅的实现。C++11中,带有作用域的静态变量有一个额外的特点,可以以线程安全的方式初始化。

下面是线程安全的Meyers单例模式。

  1. // meyersSingleton.cpp
  2. class MySingleton {
  3. public:
  4. static MySingleton& getInstance() {
  5. static MySingleton instance;
  6. return instance;
  7. }
  8. private:
  9. MySingleton();
  10. ~MySingleton();
  11. MySingleton(const MySingleton&) = delete;
  12. MySingleton& operator=(const MySingleton&) = delete;
  13. };
  14. MySingleton::MySingleton()= default;
  15. MySingleton::~MySingleton()= default;
  16. int main(){
  17. MySingleton::getInstance();
  18. }

编译器对静态变量的支持

如果在并发环境中使用Meyers单例,请确保编译器对于C++11的支持。开发者经常依赖于C++11的静态变量语义,但是有时他们的编译器不支持这项特性,结果可能会创建多个单例实例。

讨论了这么多,而在thread_local中就没有共享变量的问题了。

接下来,我们来了解一下thread_local