C++98
标准中并没有线程库的存在。C++11
中才提供了多线程的标准库,提供了thread
、mutex
、condition_variable
、atomic
等相关对象及功能功能。
1 概述
1.1 线程、进程
进程
是程序的一次执行过程,是一个动态概念,是程序在执行过程中分配
和管理资源
的最小单位
,每一个进程都有一个自己的地址空间。线程
是CPU调度和执行
的最小单位,它可与同属一个进程的其他的线程共享进程所拥有的全部资源。
进程和线程的联系
- I. 线程是进程的一部分
- II. 一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程
1.2 并发、并行
- 如下图所示这般。
并发
是两个队列交替使用一台咖啡机,而并发是指在一个时间段内,多个任务交替进行。虽然看起来像在同时执行,但其实是交替的。并行
是两个队列同时使用两台咖啡机, 并行指的是多个任务在同一时刻同时在执行。
1.3 c++ Thread
- 头文件
#include<thread>
。 C++11
中管理线程的函数和类在该头文件中声明,其中包括std::thread
、std::this_thread
。- 其中:
std::thread
为 class,std::this_thread
为包含功能的namespace。
2. thread
1.1 thread-创建线程
重点:
线程创建成功后,对于可结合的线程,一定要在线程被销毁前对线程运用join()
或detach()
方法。- 可结合的线程:
thread.joinable()==true;
- 这将在1.3小节进行介绍。
构造函数原型如下:
thread() noexcept; // 1. default,不执行任何任务的 线程对象
template <class Fn, class... Args>
explicit thread(Fn&& fn, Args&&... args); // 2. initialization, 构造的线程对象调用Fn,
thread(const thread&) = delete; // 3. copy, 该版本已被删除,线程对象无法拷贝
thread(thread&& x) noexcept; // 4. move
- I. 默认版本构造一个不执行任何任务的 线程对象。
- II.
initialization
版本, 构造的线程对象将执行入口函数Fn
,Args&&
为参数 - III. copy版本已被删除,线程对象无法拷贝
- IV.
move
版本构造一个新的线程对象,该线程对象获取参数对象X
的执行线程。
void thread_func_1(){
// 1. 无参数函数
for (int i=0; i<5; ++i){
std::cout << i << " ";// 当前线程sleep一秒,后续会介绍
std::this_thread::sleep_for(std::chrono::seconds(1));
}
};
void thread_func_2(int& n){
// 2. 带参函数
for (int i=0; i<n; ++i){
std::cout << i+10 << " ";
std::this_thread::sleep_for(std::chrono::seconds(1));
}
};
int main () {
std::thread thread_first(thread_func_1); // 3. 无参函数作为线程入口函数
int n = 5;
std::thread thread_second(thread_func_2, std::ref(n)); // 4. 有参函数作为线程入口函数
thread_first.join();
thread_second.join();
std::cout << "\nmain.func over!" << std::endl;
return 0;
}
0 10 1 11 2 12 3 13 4 14
main.func over!
- 首先,从上述打印结果可以看出,两个线程
打印交错
,而主线程的打印却没有执行。这是因为子线程使用了join
,主函数线程阻塞,直至使用join
的子线程运行结束,主线程才会继续执行。 - 主线程阻塞的过程中,子线程之间同时执行,产生了这种交错打印的现象。
- 本文将线程执行的函数称作
入口函数
,下文一致。
重点: 细心的小伙伴已经发现参数传递使用了
ref()
函数,下面总结一下thread
的中入口函数
传参
- I. 当入口函数参数
不为引用、指针 时
,会拷贝一份参数给创建的线程。此时会调用参数对象的拷贝构造函数。- II. 当入口函数参数
为指针时
,会浅拷贝一份给创建的线程,也就是说,只会拷贝对象的指针,不会拷贝指针指向的对象本身。- III. 当入口函数参数
为引用时
,实参必须用ref()函数
处理后传递给形参,否则编译将不通过。
原因:引用只是变量的别名,函数式编程如:std::bind
默认传参为值传递(拷贝),而不是引用,例子如下:
#include <functional>
#include <iostream>
void func(int& n1, int& n2, const int& n3) {
std::cout << "In function: " << n1 << ' ' << n2 << ' ' << n3 << '\n';
++n1; // increments the copy of n1 stored in the function object
++n2; // increments the main()'s n2
// ++n3; // const 类型, 无法修改
}
int main() {
int n1 = 1, n2 = 2, n3 = 3; // 1. 初始化 1,2,3
// 2. 使用std::bind, 传入参数
std::function<void()> bound_f = std::bind(func, n1, std::ref(n2), std::cref(n3));
n1 = 10;
n2 = 11;
n3 = 12; // 3. 修改为 11,12,13
std::cout << "Before function: " << n1 << ' ' << n2 << ' ' << n3 << '\n';
bound_f();
std::cout << "After function: " << n1 << ' ' << n2 << ' ' << n3 << '\n';
return 0;
}
Before function: 10 11 12
In function: 1 11 12
After function: 10 12 12
- 上述代码语句2中,函数
func()
中 的形参n1
的是值传递,这说明std::bind
默认使用的是参数的拷贝而不是引用, - 因此必须显示利用
std::ref
来进行引用绑定,n2
和n3
为引用类型。
1.2 thread-成员函数
std::thread
对象常用函数如下:
detach 将该线程与 调用线程分离
get_id 返回线程ID
join 子线程开始执行,当前线程阻塞
joinable 子线程是否可链接
native_handle
operator= 重载=,只包含move版本
sawp 交换两个线程
get_id
、operator=
、swap
声明原型
id get_id() const noexcept; // 1. get_id
thread& operator=(thread&& rhs) noexcept; // 2. operator=, 只包含move版本
thread& operator=(const thread&) = delete;
void swap(thread& x) noexcept; // 3. swap
测试代码:
#include <thread>
#include <iostream>
std::thread::id main_thread_id = std::this_thread::get_id();
void is_main_thread() {
if ( main_thread_id == std::this_thread::get_id() ) // 0. namespace-func
std::cout << "这是 main-thread." << std::endl;
else
std::cout << "这不是 main-thread." << std::endl;
}
int main() {
std::cout << "主函数thread_id:" << main_thread_id << std::endl;
std::thread thread_first(is_main_thread);
std::thread thread_second(is_main_thread);
std::cout << "thread_first_id: " << thread_first.get_id() // 1. get id
<< " thread_second: " << thread_second.get_id() << std::endl;
thread_second.swap(thread_first); // 3. swap
std::cout << "thread_first_id: " << thread_first.get_id()
<< " thread_second: " << thread_second.get_id() << std::endl;
thread_first.join();
thread_second.join();
return 0;
}
主函数thread_id:139954172499776
thread_first_id: 139954155321088 thread_second: 139954146928384
thread_first_id: 139954146928384 thread_second: 139954155321088
这不是 main-thread.
这不是 main-thread.
1.3 详解-join、detach
- 线程创建成功后,对于可结合的线程:
thread.joinable()==true;
,一定要在线程被销毁前对线程运用join()
或detach()
方法。
// 三个函数的声明原型
void join(); // 1. join
bool joinable() const noexcept; // 2. joinable
void detach(); // 3. detach
<1>join()
: 子线程使用该函数后,主线程阻塞,直至子线程运行结束,主线程从该句后一句开始继续运行。值得注意的是:
- I. 若
thread_temp.join();
下一句为line_n+1
:代码为其他线程detach
或join
,则将执行这一句。 - II. 否则,主线程阻塞。
<2>joinable()
: 返回线程对象是否可联接,以下三种情况函数返回 fasle;
- I. 该线程是默认构造的。
- II. 通过
move
将该线程分配给其他线程了。 - III. 该线程已调用过
detach
或join
。
测试代码1:
void hello(){
std::cout << "Hello thread!" << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(1));
std::cout << "Bye thread!" << std::endl;
}
int main()
{
std::thread thread_1(hello);
std::thread thread_2(hello);
thread_1.join(); // line_1
thread_2.join(); // line_2
std::cout << "main over. " << std::endl;
return 0 ;
}
Hello thread!
Hello thread!
Bye thread!
Bye thread!
main over.
- 代码执行完line_1,主函数未阻塞,执行完line_2,主函数阻塞。
- 从打印结果可以看出,两个子线程并发。
测试代码2:
void hello(){
std::cout << "Hello thread!" << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(1));
std::cout << "Bye thread!" << std::endl;
}
int main()
{
std::thread thread_1(hello);
thread_1.join(); // line_1
std::thread thread_2(hello); // line_2
thread_2.join();
std::cout << "main over. " << std::endl;
return 0 ;
}
Hello thread!
Bye thread!
Hello thread!
Bye thread!
main over.
- 代码执行到
line_1
,主函数阻塞,直至thread_1
运行结束,才开始运行line_2
。
<3>detach()
: 将对象表示的线程与调用线程分离,允许它们彼此独立执行。
void hello(){
while (true)
std::cout << "Hello thread!" << std::endl; // line_1
}
int main()
{
std::thread thread_1(hello);
thread_1.detach();
std::cout << "main over. " << std::endl; // line_2
return 0 ;
}
main over.
Hello thread!
Hello thread!
Hello thread!
Hello thread!
H
- 结果很有意思。先执行了
line_2
,接着line_1
在主函数弥留之际赶紧打印了几句。 - 最后一句只打印了一个
H
,说明主函数结束,代码块中thread_1
对象被销毁。 - 使用了
deach
后,分离的线程可以与 原父亲线程共同访问原子量。
std::atomic<bool> g_ready(false);
void waitReady() {
while (!g_ready) {
this_thread::yield();
}
cout << "ok" << endl;
}
int main() {
thread t(waitReady);
t.detach();
string inputStr;
while (cin >> inputStr) {
if (inputStr == "hello"){
break;
}
}
g_ready = true;
std::this_thread::sleep_for(std::chrono::seconds(1));
return 0;
}
hello
ok
- 主函数修改原子量,孤儿线程也能访问该原子量。
3 this_thread
this_thread
是 头文件#include <thread>
中的namespace
,其中含以下功能函数:
get_id 返回 当前线程id
sleep_for 阻塞 当前线程一段时间
sleep_until 阻塞 当前线程,直至系统时间到达某时间点
yield 当前线程需要等待其他线程时调用,阻塞在该句-让出时间片一段时间
yield()
:线程需要等待某个操作完成,如果你直接用一个循环不断判断这个操作是否完成就会使得这个线程占满CPU时间,这会造成资源浪费。这时候你可以判断一次操作是否完成,如果没有完成就调用 yield 交出时间片,过一会儿再来判断是否完成,这样这个线程占用CPU时间会大大减少。
测试代码:yield()
#include <thread>
#include <iostream>
#include <atomic>
atomic<bool> ready(false); // 1. 原子量 ready
void thfunc(int id) {
while (!ready) // 等待原子量 ready
std::this_thread::yield(); // 让出自己的CPU时间片
for (volatile int i = 0; i < 1000000; ++i){
}
std::cout << id << ",";
}
int main()
{
std::thread threads[10]; // 定义10个线程对象
std::cout << "看哪一个线程计算1到1000000的累加速度最快:\n";
for (int i = 0; i < 10; ++i)
threads[i] = std::thread(thfunc, i);
ready = true; // 2. 操作原子量
for (auto& th : threads)
th.join(); // 10个线程依次 jion
cout << '\n';
return 0;
}
看哪一个线程计算1到1000000的累加速度最快:
0,9,3,5,7,2,1,8,64,
测试代码:sleep_*()
using namespace std;
int main() {
this_thread::sleep_for(chrono::milliseconds(1000)); //阻塞当前线程 1000毫秒
this_thread::sleep_for(chrono::seconds(2)+ chrono::seconds(1)); //阻塞当前线程 3秒
std::cout << "sleep_for() 等待结束." << std::endl;
chrono::system_clock::time_point until = chrono::system_clock::now();
until += chrono::seconds(5);
this_thread::sleep_until(until); //阻塞到5秒之后
std::cout << "sleep_until() 等待结束." << std::endl;
return 0;
}
sleep_for() 等待结束.
sleep_until() 等待结束.