C++多线程编程(下)

简介: C++多线程编程

C++多线程编程(上):https://developer.aliyun.com/article/1508301

lock类

std::lock_gurad 是 C++11 中定义的模板类。

lock_guard 对象通常用于管理某个锁(Lock)对象,因此与 Mutex RAII 相关,方便线程对互斥量上锁,即在某个 lock_guard 对象的声明周期内,它所管理的锁对象会一直保持上锁状态;而 lock_guard 的生命周期结束之后,它所管理的锁对象会被解锁(注:类似 shared_ptr 等智能指针管理动态分配的内存资源)。


在 lock_guard 对象构造时,传入的 Mutex 对象会被当前线程锁住。在lock_guard 对象被析构时,它所管理的 Mutex 对象会自动解锁,即不需要程序员手动调用 lock 和 unlock 对 Mutex 进行上锁和解锁操作,这种情况下在程序抛出异常后先前已被上锁的 Mutex 对象可以正确进行解锁操作,极大地简化了程序员编写与 Mutex 相关的异常处理代码。


值得注意的是,lock_guard 对象并不负责管理 Mutex 对象的生命周期,lock_guard 对象只是简化了 Mutex 对象的上锁和解锁操作:即在某个 lock_guard 对象的声明周期内,它所管理的锁对象会一直保持上锁状态;而 lock_guard 的生命周期结束之后,它所管理的锁对象会被解锁。(一般是以{}来作为一个生命周期)


例如以下代码:

// 参考https://blog.csdn.net/coolwriter/article/details/79884298
// example  1-4 
#include <iostream>       // std::cout  
#include <thread>         // std::thread  
#include <mutex>          // std::mutex, std::lock_guard  
#include <stdexcept>      // std::logic_error  
std::mutex mtx;
void print_even(int x) {
  if (x % 2 == 0) std::cout << x << " is even\n";
  else throw (std::logic_error("not even"));
}
void print_thread_id(int id) {
  try {
    // using a local lock_guard to lock mtx guarantees unlocking on destruction / exception:  
    std::lock_guard<std::mutex> lck(mtx);
    print_even(id);
  }
  catch (std::logic_error&) {
    std::cout << "[exception caught]\n";
  }
}

int main()
{
  std::thread threads[10];
  // spawn 10 threads:  
  for (int i = 0; i<10; ++i)
    threads[i] = std::thread(print_thread_id, i + 1);

  for (auto& th : threads) th.join();
  system("pause");
  return 0;
}

最后的输出为:

[exception caught]
2 is even
[exception caught]
4 is even
[exception caught]
6 is even
[exception caught]
8 is even
[exception caught]
10 is even
请按任意键继续. . .

综上所述:lock_guard 最大的特点就是安全易于使用,在异常抛出的时候通过 lock_guard 对象管理的 Mutex 可以得到正确地解锁。但它的缺点也很明显:那就是过于简单,不具有灵活性。


针对这一缺点,C++11有如下新特性:unique_lock


unique_lock 对象以独占所有权的方式管理 mutex 对象的上锁和解锁操作,所谓独占所有权,即该mutex 对象的所有权只能由该unique_lock占有。


在构造(或移动(move)赋值)时,unique_lock 对象需要传递一个 Mutex 对象作为它的参数,新创建的 unique_lock 对象负责传入的 Mutex 对象的上锁和解锁操作。


std::unique_lock 对象也能保证在其自身析构时它所管理的 Mutex 对象能够被正确地解锁(即使没有显式地调用 unlock 函数)。因此,和 lock_guard 一样,这也是一种简单而又安全的上锁和解锁方式,尤其是在程序抛出异常后先前已被上锁的 Mutex 对象可以正确进行解锁操作,极大地简化了程序员编写与 Mutex 相关的异常处理代码。


值得注意的是,unique_lock 对象同样也不负责管理 Mutex 对象的生命周期,unique_lock 对象只是简化了 Mutex 对象的上锁和解锁操作。unique_lock 的生命周期结束之后,它所管理的锁对象会被解锁,这一点和 lock_guard 类似,但 unique_lock 给程序员提供了更多的自由。

四、条件变量condition_variable

条件变量(Condtion Variable)是在多线程程序中用来实现**“等待->唤醒”逻辑常用的方法。**


举个简单的例子,应用程序A中包含两个线程t1和t2。t1需要在bool变量test_cond为true时才能继续执行,而test_cond的值是由t2来改变的,这种情况下,如何来写程序呢?可供选择的方案有两种:


第一种是t1定时的去轮询变量test_cond,如果test_cond为false,则继续休眠;如果test_cond为true,则开始执行。

第二种就是上面提到的条件变量,t1在test_cond为false时调用cond_wait进行等待,t2在改变test_cond的值后,调用cond_signal,唤醒在等待中的t1,告诉t1 test_cond的值变了,这样t1便可继续往下执行。

很明显,上面两种方案中,第二种方案是比较优的。在第一种方案中,在每次轮询时,如果t1休眠的时间比较短,会导致cpu资源浪费很厉害;如果t1休眠的时间比较长,又会导致应用逻辑处理不够及时,致使应用程序性能下降。第二种方案就是为了解决轮询的弊端而生的。(参考自https://blog.csdn.net/erickhuang1989/article/details/8754357


请看如下例子:

//example 1-7
#include <thread>                // std::thread
#include <mutex>                // std::mutex, std::unique_lock
#include <condition_variable>    // std::condition_variable
#include<iostream>
std::mutex mtx; // 全局互斥锁.
std::condition_variable cv; // 全局条件变量.
bool ready = false; // 全局标志位.

void do_print_id(int id)
{
  std::unique_lock <std::mutex> lck(mtx);
  while (!ready) // 如果标志位不为 true, 则等待...
    cv.wait(lck); // 当前线程被阻塞, 当全局标志位变为 true 之后,
            // 线程被唤醒, 继续往下执行打印线程编号id.
  std::cout << "thread " << id << '\n';
}

void go()
{
  std ::unique_lock <std::mutex> lck(mtx);
  ready = true; // 设置全局标志位为 true.
  cv.notify_all(); // 唤醒所有线程.
}

int main()
{
  std::thread threads[10];
  // spawn 10 threads:
  for (int i = 0; i < 10; ++i)
    threads[i] = std::thread(do_print_id, i);

  std::cout << "10 threads ready to race...\n";
  go(); // go!

  for (auto & th : threads)
    th.join();
  system("pause");
  return 0;
}

结果为:

10 threads ready to race...
thread 9
thread 6
thread 8
thread 5
thread 7
thread 2
thread 1
thread 4
thread 3
thread 0
请按任意键继续. . .

代码中判断是否ready的时候,使用的是while(),而不是 if(q.empty()),这是因为wait()从阻塞到返回,不一定就是由于notify_one()函数造成的,还有可能由于系统的不确定原因唤醒(可能和条件变量的实现机制有关),这个的时机和频率都是不确定的,被称作伪唤醒,如果在错误的时候被唤醒了,执行后面的语句就会错误,所以需要再次判断队列是否为空,如果还是为空,就继续wait()阻塞。


std::condition_variable提供了两种 wait() 函数。一是当前线程调用 wait() 后将被阻塞(此时当前线程应该获得了锁(mutex),不妨设获得锁 lck),直到另外某个线程调用 notify_* 唤醒了当前线程。


在线程被阻塞时,该函数会自动调用 lck.unlock() 释放锁,使得其他被阻塞在锁竞争上的线程得以继续执行。另外,一旦当前线程获得通知(notified,通常是另外某个线程调用 notify_ 唤醒了当前线程)*,wait()函数也是自动调用 lck.lock(),使得lck的状态和 wait 函数被调用时相同。


在第二种情况下(即设置了 Predicate),**只有当 pred 条件为false 时调用 wait() 才会阻塞当前线程,并且在收到其他线程的通知后只有当 pred 为 true 时才会被解除阻塞。**请注意以下代码跟之前的区别:

#include <thread>                // std::thread, std::this_thread::yield
#include <mutex>                // std::mutex, std::unique_lock
#include <condition_variable>    // std::condition_variable
#include <iostream>
std::mutex mtx;
std::condition_variable cv;
int cargo = 0;
bool shipment_available()
{
  return cargo != 0;
}
// 消费者线程.
void consume(int n)
{
  for (int i = 0; i < n; ++i) {
    std::unique_lock <std::mutex> lck(mtx);
    cv.wait(lck, shipment_available);
    std::cout << cargo << '\n';
    cargo = 0;
  }
}
int main()
{
  std::thread consumer_thread(consume, 10); // 消费者线程.
                        // 主线程为生产者线程, 生产 10 个物品.
  for (int i = 0; i < 10; ++i) {
    while (shipment_available())
      std::this_thread::yield();//当前线程“放弃”执行(在一个时间拍片的时间内),让操作系统调度另一线程继续执行,也就是主线程放弃执行,让子线程先进行
    std::unique_lock <std::mutex> lck(mtx);
    cargo = i + 1;
    cv.notify_one();
  }

  consumer_thread.join();
  system("pause");
  return 0;
}

结果为:

1
2
3
4
5
6
7
8
9
10
请按任意键继续. . .

与std::condition_variable::wait() 类似,**不过 wait_for可以指定一个时间段,在当前线程收到通知或者指定的时间 rel_time 超时之前,该线程都会处于阻塞状态。**而一旦超时或者收到了其他线程的通知,wait_for返回,剩下的处理步骤和 wait()类似。


另外,wait_for 的重载版本的最后一个参数pred表示 wait_for的预测条件,只有当 pred条件为false时调用 wait()才会阻塞当前线程,并且在收到其他线程的通知后只有当 pred为 true时才会被解除阻塞。


详见:https://www.2cto.com/kf/201506/411327.html


值得注意的是:


在C++的condition_variable中,当 std::condition_variable对象的某个wait函数被调用的时候,它使用 std::unique_lock(通过 std::mutex) 来锁住当前线程。当前线程会一直被阻塞,直到另外一个线程在相同的 std::condition_variable 对象上调用了 notification 函数来唤醒当前线程。


std::condition_variable 对象通常使用 std::unique_lock 来等待,如果需要使用另外的 lockable 类型,可以使用std::condition_variable_any类。


这里需要注意,在管理互斥锁的时候,使用的是std::unique_lock而不是std::lock_guard,而且事实上也不能使用std::lock_guard,这需要先解释下wait()函数所做的事情。


可以看到,在wait()函数之前,使用互斥锁保护了,如果wait的时候什么都没做?那生产者也会一直卡住,不能够将数据放入队列中了。所以,wait()函数会先调用互斥锁的unlock()函数,然后再将自己睡眠,在被唤醒后,又会继续持有锁,保护后面变量。而lock_guard没有lock和unlock接口,而unique_lock提供了。这就是必须使用unique_lock的原因。


有关这个部分的进一步解释请参考:


https://blog.csdn.net/shichao1470/article/details/89856443


https://www.cnblogs.com/lidabo/p/11423825.html

五、异步调用future

在上文的代码中基本都用到的thread对象,它是C++11中提供同步创建多线程的工具。但是我们想要从线程中返回异步任务结果,一般需要依靠全局变量;从安全角度看,有些不妥;为此C++11提供了std::future类模板,future对象提供访问异步操作结果的机制,很轻松解决从异步任务中返回结果。

创建方法如下:

auto futureFunction = std::async(helloFunction, “function”);
//参考博客:https://blog.csdn.net/y396397735/article/details/82381874

跟线程的创建一样:一个可调用的对象以及可选的入口参数;


运行futureFunction.get()即可获取该调用对象的返回值;


这个时候需要了解两种策略:Eager or lazy evaluation(急速或惰性求值) 是计算表达式结果的两种策略。


在急速求值的情况下,将立即计算评估表达式。


在惰性求值的情况下,只会在需要的情况下评估表达式。


通常惰性求值被称为call-by-need,按需调用。


值得注意的是,通常情况下, std::async 立即开始执行它的工作包。


C++运行时会决定, 计算是发生在同一个线程还是一个新的线程。


使用std::launch::async参数的话,std::async 将在一个新线程中运行它。


相反,使用参数 std::launch::deferred, std::async将在同一个线程中运行它的工作包,这属于惰性求值。


这意味着,急速求值是立即执行的,惰性求值的策略std::launch::deferred是随着future调用get()后才开始执行。


参考如下代码:

//example 1-8
#include <future>
#include <iostream>
#include<string>
using namespace std;
bool is_prime(int x)
{
  for (int i = 1; i<x; i++)
  {
    if (x % i == 0)
      return false;
  }
  return true;
}
int main()
{
  std::future<bool> fut = std::async(is_prime, 700020007);
  std::cout << "please wait";
  std::chrono::milliseconds span(1);
  while (fut.wait_for(span) != std::future_status::ready)
    std::cout << ".";
  std::cout << std::endl;

  bool ret = fut.get();
  std::cout << "final result: " << std::to_string(ret) << std::endl;
  system("pause");
  return 0;
}

std::async会首先创建线程执行is_prime(700020007), 任务创建之后,std::async立即返回一个std::future对象。


主线程既可使用std::future::get获取结果,如果调用过程中,任务尚未完成,则主线程阻塞至任务完成。


主线程也可使用std::future::wait_for等待结果返回,wait_for可设置超时时间,如果在超时时间之内任务完成,则返回std::future_status::ready状态;如果在超时时间之内任务尚未完成,则返回std::future_status::timeout状态。(参考https://www.cnblogs.com/taiyang-li/p/5914167.html


Std::promise


std::promise是C++11并发编程中常用的一个类,常配合std::future使用。其作用是在一个线程t1中保存一个类型typename T的值,可供相绑定的std::future对象在另一线程t2中获取。


Std::launch是枚举类型,用于启动异步任务时,传递给函数async的参数,它的定义如下:

enum class launch {
async = 0x1,创建线程的时候就开始调用(创建新线程)
deferred = 0x2延迟调用,等到wait或者get的时候才进行调用(直接在原有的线程进行);
};

补充:async与thread的区别:关于系统资源紧张与否、返回值获取的难易程度;thread必定会创建线程,而这可能会造成系统崩溃;

六、atomic


std::atomic为C++11封装的原子数据类型。


从功能上看,简单地说,原子数据类型不会发生数据竞争,能直接用在多线程中而不必我们用户对其进行添加互斥资源锁的类型。从实现上,大家可以理解为这些原子类型内部自己加了锁。


atomic的变量支持–,++,+=等对本体的操作,但是类似于=+1的操作就会出错(相当于调用了两次);


std::atomic_flag


std::atomic_flag是一个原子的布尔类型,可支持两种原子操作:


test_and_set, 如果atomic_flag对象被设置,则返回true; 如果atomic_flag对象未被设置,则设置之,返回false

clear, 清楚atomic_flag对象

std::atomic_flag可用于多线程之间的同步操作,类似于linux中的信号量。使用atomic_flag可实现mutex,请参考如下代码.

//来自https://www.cnblogs.com/taiyang-li/p/5914331.html
#include <iostream>
#include <atomic>
#include <vector>
#include <thread>
#include <sstream>

std::atomic_flag lock = ATOMIC_FLAG_INIT;
std::stringstream stream;
void append_numer(int x)
{
  while (lock.test_and_set());
  stream << "thread#" << x << "\n";
  lock.clear();
}

int main()
{
  std::vector<std::thread> ths;
  for (int i=0; i<10; i++)
    ths.push_back(std::thread(append_numer, i));
  for (int i=0; i<10; i++)
    ths[i].join();
  std::cout << stream.str();
  return 0;
}

std::atomic对int, char, bool等数据结构进行原子性封装,在多线程环境中,对std::atomic对象的访问不会造成竞争-冒险。利用std::atomic可实现数据结构的无锁设计。如下列代码:

//来自https://www.cnblogs.com/taiyang-li/p/5914331.html
#include <iostream>
#include <atomic>
#include <vector>
#include <thread>
#include <sstream>

std::atomic<bool> ready(false);
std::atomic_flag winner = ATOMIC_FLAG_INIT;

void count1m(int i)
{
  while (!ready);
  for (int i=0; i<1000000; i++);
  if (!winner.test_and_set())
    std::cout << "winner: " << i << std::endl;
}

int main()
{
  std::vector<std::thread> ths;
  for (int i=0; i<10; i++)
    ths.push_back(std::thread(count1m, i));
  ready = true;
  for (int i=0; i<10; i++)
    ths[i].join();
  return 0;
}

参考资料

https://www.runoob.com/w3cnote/cpp-std-thread.html

https://github.com/forhappy/Cplusplus-Concurrency-In-Practice/blob/master/zh/chapter3-Thread/Introduction-to-Thread.md

相关文章
|
1月前
|
Java 开发者
Java多线程编程中的常见误区与最佳实践####
本文深入剖析了Java多线程编程中开发者常遇到的几个典型误区,如对`start()`与`run()`方法的混淆使用、忽视线程安全问题、错误处理未同步的共享变量等,并针对这些问题提出了具体的解决方案和最佳实践。通过实例代码对比,直观展示了正确与错误的实现方式,旨在帮助读者构建更加健壮、高效的多线程应用程序。 ####
|
1月前
|
安全 Java UED
深入浅出Java多线程编程
【10月更文挑战第40天】在Java的世界中,多线程是提升应用性能和响应能力的关键。本文将通过浅显易懂的方式介绍Java中的多线程编程,从基础概念到高级特性,再到实际应用案例,带你一步步深入了解如何在Java中高效地使用多线程。文章不仅涵盖了理论知识,还提供了实用的代码示例,帮助你在实际开发中更好地应用多线程技术。
45 5
|
23天前
|
设计模式 Java 开发者
Java多线程编程的陷阱与解决方案####
本文深入探讨了Java多线程编程中常见的问题及其解决策略。通过分析竞态条件、死锁、活锁等典型场景,并结合代码示例和实用技巧,帮助开发者有效避免这些陷阱,提升并发程序的稳定性和性能。 ####
|
21天前
|
缓存 Java 调度
多线程编程核心:上下文切换深度解析
在现代计算机系统中,多线程编程已成为提高程序性能和响应速度的关键技术。然而,多线程编程中一个不可避免的概念就是上下文切换(Context Switching)。本文将深入探讨上下文切换的概念、原因、影响以及优化策略,帮助你在工作和学习中深入理解这一技术干货。
37 10
|
23天前
|
缓存 Java 开发者
Java多线程编程的陷阱与最佳实践####
本文深入探讨了Java多线程编程中常见的陷阱,如竞态条件、死锁和内存一致性错误,并提供了实用的避免策略。通过分析典型错误案例,本文旨在帮助开发者更好地理解和掌握多线程环境下的编程技巧,从而提升并发程序的稳定性和性能。 ####
|
16天前
|
安全 算法 Java
Java多线程编程中的陷阱与最佳实践####
本文探讨了Java多线程编程中常见的陷阱,并介绍了如何通过最佳实践来避免这些问题。我们将从基础概念入手,逐步深入到具体的代码示例,帮助开发者更好地理解和应用多线程技术。无论是初学者还是有经验的开发者,都能从中获得有价值的见解和建议。 ####
|
16天前
|
Java 调度
Java中的多线程编程与并发控制
本文深入探讨了Java编程语言中多线程编程的基础知识和并发控制机制。文章首先介绍了多线程的基本概念,包括线程的定义、生命周期以及在Java中创建和管理线程的方法。接着,详细讲解了Java提供的同步机制,如synchronized关键字、wait()和notify()方法等,以及如何通过这些机制实现线程间的协调与通信。最后,本文还讨论了一些常见的并发问题,例如死锁、竞态条件等,并提供了相应的解决策略。
40 3
|
21天前
|
算法 调度 开发者
多线程编程核心:上下文切换深度解析
在多线程编程中,上下文切换是一个至关重要的概念,它直接影响到程序的性能和响应速度。本文将深入探讨上下文切换的含义、原因、影响以及如何优化,帮助你在工作和学习中更好地理解和应用多线程技术。
31 4
|
26天前
|
数据采集 存储 数据处理
Python中的多线程编程及其在数据处理中的应用
本文深入探讨了Python中多线程编程的概念、原理和实现方法,并详细介绍了其在数据处理领域的应用。通过对比单线程与多线程的性能差异,展示了多线程编程在提升程序运行效率方面的显著优势。文章还提供了实际案例,帮助读者更好地理解和掌握多线程编程技术。
|
22天前
|
安全 Java 开发者
Java中的多线程编程:从基础到实践
本文深入探讨了Java多线程编程的核心概念和实践技巧,旨在帮助读者理解多线程的工作原理,掌握线程的创建、管理和同步机制。通过具体示例和最佳实践,本文展示了如何在Java应用中有效地利用多线程技术,提高程序性能和响应速度。
54 1