线程创建与管理--thread
C++的多线程可以充分利用计算机资源,提高代码运行效率。在这里总结了一些多线程应用过程中的基本概念和用法。
一个多线程C++程序是什么样子的?它看上去和其他所有C++程序一样,通常是变量、类以及函数的组合。唯一真正的区别在于某些函数可以并发运行,所以你需要确保共享数据的并发访问是安全的。当然,为了并发地运行函数,必须使用特定的函数以及对象来管理各个线程。
C++11新标准多线程支持库
C++标准并没有提供对多进程并发的原生支持,所以C++的多进程并发要靠其他API(比如用CreateThread
进行创建)
C++11可以通过多线程实现并发,这是一种比较底层、传统的实现方式。C++11引入了5个头文件来支持多线程编程,是<atomic>
、<thread>
、<mutex>
、<condition_variable>
、<future>
- <
thread
> : 提供线程创建及管理的函数或类接口; - <
mutex
> : C++11 互斥量Mutex。在多线程环境中,有多个线程竞争同一个公共资源,就很容易引发线程安全的问题 - <
condition_variable
> : 允许一定量的线程等待(可以定时)被另一线程唤醒,然后再继续执行; - <
future
> : 提供了一些工具来获取异步任务(即在单独的线程中启动的函数)的返回值,并捕捉其所抛出的异常 - <
atomic
>:为细粒度的原子操作(不能被处理器拆分处理的操作)提供组件,允许无锁并发编程。
std::thread类成员函数
get_id
:获取线程ID,返回一个类型为std::thread::id
的对象。joinable
:检查线程是否可被join
。检查thread
对象是否标识一个活动(active
)的可行性线程。缺省构造的thread
对象、已经完成join
的thread
对象、已经detach
的thread
对象都不是joinable
。join
:通过join()
函数关联并阻塞线程,等待该线程执行完毕后继续;detach
:通过detach()
函数解除关联使线程可以与主线程并发执行,但若主线程执行完毕退出后,detach()
接触关联的线程即便没有执行完毕,也将自动退出。native_handle
:该函数返回与std::thread
具体实现相关的线程句柄。swap
:交换两个线程对象所代表的底层句柄。operator
:移动线程对象
有时候我们需要在线程执行代码里面对当前调用者线程进行操作,针对这种情况,C++11里面专门定义了一个命名空间this_thread
,此命名空间也声明在<thread>
头文件中,其中包括:
get_id()
函数用来获取当前调用者线程的ID;yield()
函数可以用来将调用者线程跳出运行状态,重新交给操作系统进行调度,即当前线程放弃执行,操作系统调度另一线程继续执行;sleep_until()
函数是将线程休眠至某个指定的时刻(time point),该线程才被重新唤醒;sleep_for()
函数是将线程休眠某个指定的时间片(time span),该线程才被重新唤醒,不过由于线程调度等原因,实际休眠实际可能比sleep_duration所表示的时间片更长。
std::thread的关键总结
- C++ 11中创建线程非常简单,使用
std::thread
类就可以,thread
类定义于thread
头文件,构造thread
对象时传入一个可调用对象作为参数(如果可调用对象有参数,把参数同时传入),这样构造完成后,新的线程马上被创建,同时执行该可调用对象 - 用
std::thread
默认的构造函数构造的对象不关联任何线程;判断一个thread
对象是否关联某个线程,使用joinable()
接口,如果返回true,表明该对象关联着某个线程(即使该线程已经执行结束); "joinable"
的对象析构前,必须调用join()
接口等待线程结束,或者调用detach()
接口解除与线程的关联,否则会抛异常;- 正在执行的线程从关联的对象
detach
后会自主执行直至结束,对应的对象变成不关联任何线程的对象,joinable()
将返回false; std::thread
没有拷贝构造函数和拷贝赋值操作符,因此不支持复制操作(但是可以move
),也就是说,没有两个std::thread
对象会表示同一执行线程;- 容易知道,如下几种情况下,
std::thread
对象是不关联任何线程的(对这种对象调用join
或detach
接口会抛异常):- 默认构造的
thread
对象; - 被移动后的
thread
对象; detach
或join
后的thread
对象;
- 默认构造的
C++中多线程创建
- 简单使用
这里创建传入的函数f,实际上其构造函数需要的是可调用(callable)类型,只要是有函数调用类型的实例都是可以的。所有除了传递函数外,还可以使用:#include <iostream> #include <thread> using namespace std; void f() { cout << "thread 1 is running" << endl; this_thread::sleep_for(chrono::seconds(1)); } int main() { thread t1(f); // 创建线程,一旦创建完毕,马上开始运行 t1.join(); return 0; }
- lambda表达式
for (int i = 0; i < 4; i++) { thread t([i]{ cout << i << endl; }); t.detach(); }
- 重载了()运算符的类的实例
当线程启动后,一定要在和线程相关联的thread销毁前,确定以何种方式等待线程执行结束。C++11有两种方式来等待线程结束:#include <iostream> #include <thread> using namespace std; class Task { public : void operator()(int i) //()重载 { cout << i << endl; } }; int main() { for (int i = 0; i < 4; i++) { Task task; thread t(task, i); t.detach(); } }
(1) detach方式,启动的线程自主在后台运行,当前的代码继续往下执行,不等待新线程结束。前面代码所使用的就是这种方式。因此执行的结果就多种多样了。
这就涉及到多线程编程最核心的问题了资源竞争。CPU有4核,可以同时执行4个线程是没有问题的。但是控制台(资源)却只有一个,同时只能有一个线程拥有这个唯一的控制台,将数字输出。
(2) join方式,等待启动的线程完成,才会继续往下执行。假如前面的代码使用这种方式,其输出就会0,1,2,3,因为每次都是前一个线程输出完成了才会进行下一个循环,启动下一个新线程。
-
带函数参数的线程
当需要向线程函数传递参数时,直接在创建线程时,同时也把参数作为入参传递给线程函数。
注意当调用函数的参数为引用参数时,线程调用需要加上ref关键字表示引用。并且线程函数会改变引用的变量值。#include <iostream> #include <thread> using namespace std; void f1(int n) { n++; cout<<"n = "<< n <<endl; } void f2(int &n)//引用参数 { n++; cout<<"n = "<<n<<endl; } int main() { int n = 0; thread t1(f1, n); t1.join(); cout<<"n = "<<n<<endl; thread t2(f2, ref(n)); t2.join(); cout<<"n = "<<n<<endl; } /* n = 1 n = 0 n = 1 n = 0 */
-
转移线程的所有权
thread
是可移动的(movable
)的,但不可复制(copyable
)。可以通过move
来改变线程的所有权,灵活的决定线程在什么时候join
或者detach
。#include <iostream> #include <thread> using namespace std; void f2(int &n) { n++; cout << "n = " << n << endl; } int main() { int n = 0; thread t3(f2, ref(n)); thread t4(move(t3)); t4.join(); //此时t4正在运行f2(),t3不再是一个线程了。 return 0; } /* n = 1 */
将线程从t3转移给t4,这时候t3就不再拥有线程的所有权,调用
t3.join
或t3.detach
会出现异常,要使用t4来管理线程。这也就意味着thread
可以作为函数的返回类型,或者作为参数传递给函数,能够更为方便的管理线程。 -
线程暂停
如果让线程从外部暂停会引发很多并发问题,这也是为什么std::thread
没有直接提供pause
函数的原因。
如果线程在运行过程中,确实需要停顿,就可以用this_thread::sleep_for
。this_thread::sleep_for(chrono::seconds(3)); //此处线程停顿3秒。
-
获取当前线程号
线程的标识类型为std::thread::id
,有两种方式获得到线程的id。- 通过
thread
的实例调用get_id()
直接获取 - 在当前线程上调用
this_thread::get_id()
获取
thread::id main_threadId = this_thread::get_id();
- 通过
小结:
本结主要介绍了C++11引入的标准多线程库的一些基本操作。有以下内容:
- 线程的创建
- 线程的执行方式,
join
或者detach
- 向线程函数传递参数,需要注意的是线程默认是以拷贝的方式传递参数的,当期望传入一个引用时,要使用
std::ref
进行转换 - 线程是
movable
的,可以在函数内部或者外部进行传递 - 线程在运行过程中,如果需要停顿,可以用
this_thread::sleep_for
实现。 - 每个线程都一个标识,可以调用
get_id
获取。
线程管理的示例代码
#include <iostream>
#include <thread>
#include <chrono>
using namespace std;
void thread_function(int n) {
thread::id this_id = this_thread::get_id(); // 获取线程ID
for (int i = 0; i < 5; i++) {
cout << "子线程 " << this_id << "运行 : " << i + 1 << endl;
this_thread::sleep_for(chrono::seconds(n)); // 进程睡眠n秒
}
}
class Thread_functor {
public:
// functor行为类似函数,C++中的仿函数是通过在类中重载()运算符实现,使你可以像使用函数一样来创建类的对象
void operator()(int n) {
thread::id this_id = this_thread::get_id();
for (int i = 0; i < 5; i++) {
cout << "子仿函数线程" << this_id << "运行 : " << i + 1 << endl;
this_thread::sleep_for(chrono::seconds(n)); // 进程睡眠n秒
}
}
};
int main() {
thread mythread1(thread_function, 1); // 传递初始函数作为线程的参数
if (mythread1.joinable()) { // 判断是否可以成功使用join()或者detach(),返回true则可以,返回false则不可以
mythread1.join(); // 使用join()函数阻塞主线程直至子线程执行完毕
}
Thread_functor thread_functor; // 函数对象实例化一个对象
thread mythread2(thread_functor, 3); // 传递初始函数作为线程的参数
if (mythread2.joinable()) {
mythread2.detach(); // 使用detach()函数让子线程和主线程并行运行,主线程也不再等待子线程
}
auto thread_lambda = [](int n) { // lambda表达式格式:[捕获列表](参数列表)可变异常 -> 返回类型{函数体}
thread::id this_id = this_thread::get_id();
for (int i = 0; i < 5; i++) {
cout << "子lambda线程" << this_id << " 运行 : " << i + 1 << endl;
this_thread::sleep_for(chrono::seconds(n)); // 进程睡眠n秒
}
};
thread mythread3(thread_lambda, 4); // 传递初始函数作为线程的参数
if (mythread3.joinable()) {
mythread3.join(); // 使用join()函数阻塞主线程直至子线程执行完毕
}
thread::id this_id = this_thread::get_id();
for (int i = 0; i < 5; i++) {
cout << "主线程" << this_id << " 运行 : " << i + 1 << endl;
this_thread::sleep_for(chrono::seconds(1));
}
getchar();
return 0;
}
/*
[New Thread 0x7ffff77ff6c0 (LWP 6824)]
子线程 140737345746624运行 : 1
子线程 140737345746624运行 : 2
子线程 140737345746624运行 : 3
子线程 140737345746624运行 : 4
子线程 140737345746624运行 : 5
[New Thread 0x7ffff77ff6c0 (LWP 6826)]
[Thread 0x7ffff77ff6c0 (LWP 6824) exited]
子仿函数线程140737345746624运行 : 1
[New Thread 0x7ffff6ffe6c0 (LWP 6827)]
子lambda线程140737337353920 运行 : 1
子仿函数线程140737345746624运行 : 2
子lambda线程140737337353920 运行 : 2
子仿函数线程140737345746624运行 : 3
子lambda线程140737337353920 运行 : 3
子仿函数线程140737345746624运行 : 4
子仿函数线程140737345746624运行 : 5
子lambda线程140737337353920 运行 : 4
[Thread 0x7ffff77ff6c0 (LWP 6826) exited]
子lambda线程140737337353920 运行 : 5
主线程140737352688768 运行 : 1
[Thread 0x7ffff6ffe6c0 (LWP 6827) exited]
主线程140737352688768 运行 : 2
主线程140737352688768 运行 : 3
主线程140737352688768 运行 : 4
主线程140737352688768 运行 : 5
[Inferior 1 (process 6821) exited normally]
*/
/*
上面的代码分别用三种函数对象创建了三个线程,其中
第一个线程mythread1阻塞等待其执行完后继续往下执行,
第二个线程mythread2不阻塞等待在后台与后面的第三个线程mythread3并发执行,
第三个线程继续阻塞等待其完成后再继续往下执行主线程任务。
为了便于观察并发过程,对三个线程均用了睡眠延时this_thread::sleep_for(duration)函数,且延时时间作为参数传递给该函数。
*/