1 前言
C++11标准在标准库中为多线程提供了组件,这意味着使用C++编写与平台无关的多线程程序成为可能,而C++程序的可移植性也得到了有力的保证。
在之前我们主要使用的多线程库要么是属于某个单独平台的,例如:POSIX线程库(Linux),Windows线程库(Windows),还有第三方数据库:Boost线程库。但是且不说性能上的不同,它们都有各自缺点,要么受平台限制无法系统间移植程序,要么需要下载第三方程序包来支持。相比较而言,我们当然更希望使用可移植的官方标准多线程库,这些在C++11中提供了组件,下面我们就一同来学习学习C++11中多线程库吧。
2 并发与并行的区别
并发指的是两个或者多个独立的活动在同一时间段内发生, 例如在跑步的时候你可能同时在听音乐;在看电脑显示器的同时你的手指在敲击键盘。这时我们称我们大脑并发地处理这些事件,只不过我们大脑的处理是有次重点的:有时候你会更关注你呼吸的频率,而有时候你更多地被美妙的音乐旋律所吸引。这时我们可以说大脑是一种并发设计的结构。这种次重点在计算机程序设计中,体现为某一个时刻只能处理一个操作。
与并发相近的另一个概念是并行。它们两者存在很大的差别。并行就是同时执行,计算机在同一时刻,在某个时间点上处理两个或以上的操作。判断一个程序是否并行执行,只需要看某个时刻上是否多两个或以上的工作单位在运行。一个程序如果是单线程的,那么它无法并行地运行。利用多线程与多进程可以使得计算机并行地处理程序(当然 ,前提是该计算机有多个处理核心)。
并发:同一时间段内可以交替处理多个操作
并行:同一时间段内同时处理多个操作
第一张图中两个任务队列同时等待一个处理器处理,两个队列可能约定交替着进行被处理,也可能是大家同时竞争被处理(通信)。后一种方式可能引起冲突:因为一个处理器无法同时进行两步操作。但在逻辑上看来,这个处理器是同时处理这两个队列。
而第二张图中两个任务队列是并行处理,每个队列都有自己的独立处理器(或者核),两个队列中间没有竞争关系,队列中的某个排队者只需等待队列前面的消息处理完成,然后再轮到自己被处理。在物理上,如果是两个核的CPU宏观上看是同时处理这两个任务队列。
并发的程序设计,提供了一种方式让我们能够设计出一种方案将问题(非必须地)并行地解决。如果我们将程序的结构设计为可以并发执行的,那么在支持并行的机器上,我们可以将程序并行地执行。因此,并发重点指的是程序的设计结构,而并行指的是程序运行的状态。并发编程,是一种将一个程序分解成小片段独立执行的程序设计方法。
3 并发的两种模式
这里两种模式指的就是我们都知道的多线程并发与多进程并发的两种方法。
3.1 多进程并发
多个进程独立地运行,它们之间通过进程间常规的通信渠道传递讯息(信号,套接字,文件,管道等),这种进程间通信不是设置复杂就是速度慢,这是因为为了避免一个进程去修改另一个进程,操作系统在进程间提供了一定的保护措施,当然,这也使得编写安全的并发代码更容易。运行多个进程也需要固定的开销:进程的启动时间,进程管理的资源消耗。
3.2 多线程并发
在当个进程中运行多个线程也可以并发。线程就像轻量级的进程,每个线程相互独立运行,但它们共享地址空间,所有线程访问到的大部分数据如指针、对象引用或其他数据可以在线程之间进行传递,它们都可以访问全局变量。进程之间通常共享内存,但这种共享通常难以建立且难以管理,缺少线程间数据的保护。因此,在多线程编程中,我们必须确保每个线程锁访问到的数据是一致的。
4 C++中的并发与多线程
原来的C++标准并没有提供对多进程的原生并发的支持,所以C++多进程并发要靠其他的API,这需要依赖相关平台。
C++11标准提供了一个新的线程库,内容包括了管理线程、保护共享数据、线程间的同步操作、低级原子操作等各种类。标准极大的提高了程序的可移植性,以前的多线程依赖于具体的平台,而现在有了统一的接口进行实现。
C++11的新标准中引入了几个头文件来支持多线程编程:
- <thread> : 包含std::thread类以及std::this_thread命名空间。管理线程的函数和类在该头文件中有声明;
- <atomic> :包含std::atomic和std::atomic_flag类,以及一套C风格的原子类型和与C兼容的原子操作的函数;
- <mutex> :包含了与互斥量相关的类以及其他类型的函数;
- <future>: 包含两个Provider类(std::promise和std::package_task)和两个Future类(std::future和std::shared_future)以及相关的类型和函数;
- <condition_variable> : 包含与条件变量相关的类,包括std::condition_variable和std::condition_variable_any
注:以上内容参考C++11并发编程(一)——初始C++11多线程库_无鞋童鞋的博客-优快云博客_c++ 多线程库
4.1 std::thread
thread
类代表着单线程执行,Threads 允许同时执行多个函数。要执行的函数会作为构造函数的参数传递给thread 对象,
所以在thread 对象构造的时候,线程就开始执行了。函数的返回值被忽略,如果函数在执行过程中发生了异常,那么会调用
std::terminate 方法。通过 std::promise 方法,函数也可以和调用者进行通信。
通过一段多线程代码,我们来揭开thread
的神秘面纱:
-
#include <iostream>
-
#include <thread>
-
using namespace std;
-
void task_one() {
-
for (int i = 0; i < 10; i++) {
-
cout << this_thread::get_id() << '\t' << i << endl;
-
this_thread::sleep_for(chrono::milliseconds(5)); // 休眠5ms
-
}
-
}
-
void task_two(int n) {
-
for (int i = 0; i < n; i++) {
-
cout << this_thread::get_id() << '\t' << i << endl;
-
this_thread::sleep_for(chrono::milliseconds(10)); //休眠10ms
-
}
-
}
-
int main() {
-
int n = 20;
-
thread t1(task_one);
-
thread t2(task_two, n);
-
t1.join();
-
t2.join();
-
return 0;
-
}
在上述代码中,一共存在三个线程,t1,t2和程序的主线程(也就是执行main函数那个线程),线程t1,t2要执行的任务分别是task_one与task_two(也就是两个函数),在各自的循环里输出线程id以及循环变量i。
另外代码t1.join()与t2.join()在main函数中,也就是说在主线程中,表示将主线程与t1,t2相结合,这样一来,主线程会阻塞,直到线程t1,t2执行完毕,主线程才会执行后面的代码。
判断一个线程对象是否是活跃的正在执行的线程。如果
1 std::thread::joinable
get_id() != std::thread::id()
那么,返回true。所以默认构造的线程是不可结合的。一个线程执行完了,但是并没有join(),那么仍然被认为是一个活跃的正在执行的线程,因此是可以结合的。
-
#include <iostream>
-
#include <thread>
-
#include <chrono>
-
void foo()
-
{
-
std::this_thread::sleep_for(std::chrono::seconds(1));
-
}
-
int main()
-
{
-
std::thread t;
-
std::cout << "before starting, joinable: " << std::boolalpha << t.joinable()
-
<< '\n';
-
t = std::thread(foo);
-
std::cout << "after starting, joinable: " << t.joinable()
-
<< '\n';
-
t.join();
-
std::cout << "after joining, joinable: " << t.joinable()
-
<< '\n';
-
}
-
//output
-
before starting, joinable: false
-
after starting, joinable: true
-
after joining, joinable: false
2 std::thread::join()
阻塞当前线程,直到线程(通过this标记)结束。
后置条件:joinable:false
-
#include <iostream>
-
#include <thread>
-
#include <chrono>
-
void foo()
-
{
-
// simulate expensive operation
-
std::this_thread::sleep_for(std::chrono::seconds(1));
-
}
-
void bar()
-
{
-
// simulate expensive operation
-
std::this_thread::sleep_for(std::chrono::seconds(1));
-
}
-
int main()
-
{
-
std::cout << "starting first helper...\n";
-
std::thread helper1(foo);
-
std::cout << "starting second helper...\n";
-
std::thread helper2(bar);
-
std::cout << "waiting for helpers to finish..." << std::endl;
-
helper1.join();
-
helper2.join();
-
std::cout << "done!\n";
-
}
-
output
-
starting first helper...
-
starting second helper...
-
waiting for helpers to finish...
-
done!
3 std::thread::detach()
从线程对象中分离出执行线程,允许线程独立的执行。一旦线程退出,所有分配的资源都会被释放。在调用了detach之后,*this就不会拥有任何线程了。
-
#include <iostream>
-
#include <chrono>
-
#include <thread>
-
void independentThread()
-
{
-
std::cout << "Starting concurrent thread.\n";
-
std::this_thread::sleep_for(std::chrono::seconds(2));
-
std::cout << "Exiting concurrent thread.\n";
-
}
-
void threadCaller()
-
{
-
std::cout << "Starting thread caller.\n";
-
std::thread t(independentThread);
-
t.detach();
-
std::this_thread::sleep_for(std::chrono::seconds(1));
-
std::cout << "Exiting thread caller.\n";
-
}
-
int main()
-
{
-
threadCaller(); //函数结束后要释放变量
-
std::this_thread::sleep_for(std::chrono::seconds(5));
-
}
-
output
-
Starting thread caller.
-
Starting concurrent thread.
-
Exiting thread caller.
-
Exiting concurrent thread.
其实重点就是理解线程的join,detach,joinable三者之间的关系:
我们从thread的析构函数~thread()入手分析
-
~thread() _NOEXCEPT { // 析构函数
-
if (joinable()) // 线程是可结合的,析构异常(也就是说只能析构不可结合的线程)
-
_XSTD terminate(); // terminate会调用abort()来终止程序
-
}
其实析构函数在这里只进行了判断,并没有析构什么,因为thread成员变量不存在用new或者是malloc进行内存分配的指针或者数组,所以在析构函数里不做资源释放的工作,那为什么只能析构不可结合的线程呢?
(可结合或者是不可结合都是指的是线程的状态,正在执行的线程和没有执行的线程)
我们还是以博客最开始的源代码进行分析,主线程main以及t1,t2两个线程:
-
int main() {
-
int n = 20;
-
thread t1(task_one);
-
thread t2(task_two, n);
-
t1.join();
-
t2.join();
-
cout << "main thread" << endl;
-
return 0;
-
}
我们总结一下线程为不可结合(即joinable()为false)的几种情况:
- 空线程
- move后的线程,即move(t),则t是不可结合的
- join后的线程
- detach后的线程
在实例化了t1、t2对象之后,它们的状态默认都是可结合的,如果现在直接调用它们的析构函数来析构它们,那么在析构的时候线程处于什么状态呢?是执行完了吗?还是正在执行呢?注意,如果一个在没有结合(join)的情况下,就算它先于主线程执行完毕,其id依然是不为0的。所以我们是不能确定其状态的,所以我们只能析构明确了id为0的线程。因为id为0的线程要么已经执行完毕,要么是空线程,要么是分离后的线程。
另外,一个线程分离(detech)后,该线程对象边便不能控制该线程,而是交由系统接管。
4.2 std::stread 小例子
在C++11中引入了一个用于多线程操作的thread类,简单的多线程实例:
-
#include <iostream>
-
#include <thread>
-
#include <Windows.h>
-
using namespace std;
-
void thread01()
-
{
-
for (int i = 0; i < 5; i++)
-
{
-
cout << "Thread 01 is working !" << endl;
-
Sleep(100);
-
}
-
}
-
void thread02()
-
{
-
for (int i = 0; i < 5; i++)
-
{
-
cout << "Thread 02 is working !" << endl;
-
Sleep(200);
-
}
-
}
-
int main()
-
{
-
thread task01(thread01);
-
thread task02(thread02);
-
task01.join();
-
task02.join();
-
for (int i = 0; i < 5; i++)
-
{
-
cout << "Main thread is working !" << endl;
-
Sleep(200);
-
}
-
system("pause");
-
}
简单分析一下,正如前面所说,join()的作用是阻塞主线程,只有在thread01与cthread02两个线程执行完成之后,才会继续执行主线程里面的东西,输出结果如下:
当然,我们也可以使用detach将子线程从主流程中分离,独立运行,不会阻塞主线程:
-
#include <iostream>
-
#include <thread>
-
#include <Windows.h>
-
using namespace std;
-
void thread01()
-
{
-
for (int i = 0; i < 5; i++)
-
{
-
cout << "Thread 01 is working !" << endl;
-
Sleep(100);
-
}
-
}
-
void thread02()
-
{
-
for (int i = 0; i < 5; i++)
-
{
-
cout << "Thread 02 is working !" << endl;
-
Sleep(200);
-
}
-
}
-
int main()
-
{
-
thread task01(thread01);
-
thread task02(thread02);
-
task01.detach();
-
task02.detach();
-
for (int i = 0; i < 5; i++)
-
{
-
cout << "Main thread is working !" << endl;
-
Sleep(200);
-
}
-
system("pause");
这样的话,两个子线程就与主线程并行执行了,输出结果如下:
也就是说,即使主线程main函数结束了,那两个子线程还是照样运行的。
参考资料:
C++使用thread类多线程编程 C++使用thread类多线程编程_牧野的博客-优快云博客_c++使用线程
《探索C++多线程》:thread源码(二) 《探索C++多线程》:thread源码(二)_hujingshuang-优快云博客_c thread源码
C++11 并发指南二(std::thread 详解) - Haippy - 博客园
(72条消息) 基于C++11并发库的线程池与消息队列多线程框架——std::thread类_godqiao的博客-优快云博客_c++ 线程池库