前言:前段时间初看了下Boost库,看到线程学了下。c++11是有线程的,这个之前我也是知道的,但就感觉自己没怎么系统的学过,翻了下c++primer 居然没有讲线程的,真是坑啊。。。怪不得总感觉自己学c++语言方面没系统的学过这东西。。还是看的少。
多线程是多任务处理的一种特殊形式,多任务处理允许让电脑同时运行两个或两个以上的程序。一般情况下,两种类型的多任务处理:基于进程和基于线程。
- 基于进程的多任务处理是程序的并发执行。
- 基于线程的多任务处理是同一程序的片段的并发执行。
多线程程序包含可以同时运行的两个或多个部分。这样的程序中的每个部分称为一个线程,每个线程定义了一个单独的执行路径。
并发和并行:
说到多线程编程,那么就不得不提并行和并发,多线程是实现并发(并行)的一种手段。并行是指两个或多个独立的操作同时进行。注意这里是同时进行,区别于并发,在一个时间段内执行多个操作。在单核时代,多个线程是并发的,在一个时间段内轮流执行;在多核时代,多个线程可以实现真正的并行,在多核上真正独立的并行执行。例如现在常见的4核4线程可以并行4个线程;4核8线程则使用了超线程技术,把一个物理核模拟为2个逻辑核心,可以并行8个线程。所以,通常,要实现并发有两种方法:多进程和多线程。
多进程并发
使用多进程并发是将一个应用程序划分为多个独立的进程(每个进程只有一个线程),这些独立的进程间可以互相通信,共同完成任务。由于操作系统对进程提供了大量的保护机制,以避免一个进程修改了另一个进程的数据,使用多进程比多线程更容易写出安全的代码。但这也造就了多进程并发的两个缺点:
- 在进程件的通信,无论是使用信号、套接字,还是文件、管道等方式,其使用要么比较复杂,要么就是速度较慢或者两者兼而有之。
- 运行多个线程的开销很大,操作系统要分配很多的资源来对这些进程进行管理。
由于多个进程并发完成同一个任务时,不可避免的是:操作同一个数据和进程间的相互通信,上述的两个缺点也就决定了多进程的并发不是一个好的选择。
多线程并发
多线程并发指的是在同一个进程中执行多个线程。有操作系统相关知识的应该知道,线程是轻量级的进程,每个线程可以独立的运行不同的指令序列,但是线程不独立的拥有资源,依赖于创建它的进程而存在。也就是说,同一进程中的多个线程共享相同的地址空间,可以访问进程中的大部分数据,指针和引用可以在线程间进行传递。这样,同一进程内的多个线程能够很方便的进行数据共享以及通信,也就比进程更适用于并发操作。由于缺少操作系统提供的保护机制,在多线程共享数据及通信时,就需要程序员做更多的工作以保证对共享数据段的操作是以预想的操作顺序进行的,并且要极力的避免死锁(deadlock)。
c++11提供了一个新的头文件<thread>提供了对线程函数的支持的声明
#include <iostream>
#include <thread>
void fun()
{
std::cout << "A new thread!" << std::endl;
}
int main()
{
std::thread t(fun);
t.join();
std::cout << "Main thread!" << std::endl;
}
这样的demo就是一个简单的多线程的应用了。其输出如下:
A new thread!
Main thread!
存在小问题的代码:
#include <iostream>
#include <thread>
using namespace std;
void output(int i)
{
cout << i << endl;
}
int main()
{
for (uint8_t i = 0; i < 4; i++)
{
thread t(output, i);
t.detach();
}
getchar();
return 0;
}
在一个for循环内,创建4个线程分别输出数字0、1、2、3,并且在每个数字的末尾输出换行符。语句thread t(output, i)
创建一个线程t,该线程运行output
,第二个参数i是传递给output
的参数。t在创建完成后自动启动,t.detach
表示该线程在后台允许,无需等待该线程完成,继续执行后面的语句。这段代码的功能是很简单的,如果是顺序执行的话,其结果很容易预测得到
0 \n 1 \n 2 \n 3 \n
但是在并行多线程下,其执行的结果就多种多样了,下图是代码一次运行的结果:
01
2
3
可以看出,首先输出了01,并没有输出换行符;紧接着却连续输出了2个换行符。不是说好的并行么,同时执行,怎么还有先后的顺序?这就涉及到多线程编程最核心的问题了资源竞争。CPU有4核,可以同时执行4个线程这是没有问题了,但是控制台却只有一个,同时只能有一个线程拥有这个唯一的控制台,将数字输出。将上面代码创建的四个线程进行编号:t0,t1,t2,t3,分别输出的数字:0,1,2,3。参照上图的执行结果,控制台的拥有权的转移如下:
- t0拥有控制台,输出了数字0,但是其没有来的及输出换行符,控制的拥有权却转移到了t1;(0)
- t1完成自己的输出,t1线程完成 (1\n)
- 控制台拥有权转移给t0,输出换行符 (\n)
- t2拥有控制台,完成输出 (2\n)
- t3拥有控制台,完成输出 (3\n)
由于控制台是系统资源,这里控制台拥有权的管理是操作系统完成的。但是,假如是多个线程共享进程空间的数据,这就需要自己写代码控制,每个线程何时能够拥有共享数据进行操作。共享数据的管理以及线程间的通信,是多线程编程的两大核心。
这里创建std::thread
传入的函数,实际上其构造函数需要的是可调用(callable)类型,只要是有函数调用类型的实例都是可以的。所有除了传递函数外,还可以使用:lambda表达式
join函数与detch
上面的两个例子分别使用了join函数与detch。
解释:
在使用std::thread
的时候,对创建的线程有两种操作:等待/分离,也就是join/detach
操作。
join()
操作是在std::thread t(func)
后“某个”合适的地方调用,其作用是回收对应创建的线程的资源,避免造成资源的泄露。
detach()
操作是在std::thread t(func)
后马上调用,用于把被创建的线程与做创建动作的线程分离,分离的线程变为后台线程,其后,创建的线程的“死活”就与其做创建动作的线程无关,它的资源会被init进程回收。
注意:
1.在一个线程中,开了另一个线程去干另一件事,使用join函数后,原始线程会等待新线程执行结束之后,再去销毁线程对象。
2.这样有什么好处?---->因为它要等到新线程执行完,再销毁,线程对象,这样如果新线程使用了共享变量,等到新线程执行完再销毁这个线程对象,不会产生异常。如果不使用join,使用detch,那么新线程就会与原线程分离,如果原线程先执行完毕,销毁线程对象及局部变量,并且新线程有共享变量或引用之类,这样新线程可能使用的变量,就变成未定义,产生异常或不可预测的错误。
3.所以,当你确定程序没有使用共享变量或引用之类的话,可以使用detch函数,分离线程。
4.但是使用join可能会造成性能损失,因为原始线程要等待新线程的完成,所以有些情况(前提是你知道这种情况,如上)使用detch会更好。
分析下面有问题代码:
#include <iostream>
#include <thread>
#include <windows.h>
void func(){
for (int i = 0; i < 100; ++i) {
std::cout<<"thread::func"<<std::endl;
}
}
int main() {
std::thread my_thread(func);
// Sleep(1);
my_thread.detach();
// my_thread.join();
return 0;
}
结果是:还未执行一次打印线程也就结束了,由于使用的是detch函数,新线程与主线程分离,在detch函数执行完成,主线程main函数结束,my_thread对象销毁。如果使用join,结果是打印100次的thread::func。
由于join
是等待被创建线程的结束,并回收它的资源。因此,join的调用位置就比较关键。比如,以下的调用位置都是错误的。
void test()
{
}
bool do_other_things()
{
}
int main()
{
std::thread t(test);
int ret = do_other_things();
if(ret == ERROR) {
return -1;
}
t.join();
return 0;
}
很明显,如果do_other_things()
函数调用返ERROR, 那么就会直接退出main函数
,此时join就不会被调用,所以线程t的资源没有被回收,造成了资源泄露。
当线程启动后,一定要在和线程相关联的thread
销毁前,确定以何种方式等待线程执行结束。
异常情况下等待线程完成
void test()
{
}
bool do_other_things()
{
}
int main()
{
std::thread t(test);
try {
do_other_things();
}
catch(...) {
t.join();
throw;
}
t.join();
return 0;
}
还有更好的方法!
一种比较好的方法是资源获取即初始化(RAII,Resource Acquisition Is Initialization),该方法提供一个类,在析构函数中调用join
。
class mythread {
private:
std::thread &m_t;
public:
explicit mythread(std::thread &t):m_t(t){}
~mythread() {
if(t.joinable()) {
t.join()
}
}
mythread(mythread const&) = delete;
mythread& operate=(mythread const&) = delete;
}
void test()
{
}
bool do_other_things()
{
}
int main()
{
std::thread t(test);
mythread q(t);
if(do_other_things()) {
return -1;
}
return 0;
}
无论是何种情况,当函数退出时,局部变量q调用其析构函数销毁,从而能够保证join
一定会被调用
向线程传递参数
需要注意的是,默认的会将传递的参数以拷贝的方式复制到线程空间,即使参数的类型是引用。例如:
void func(int a,const string& str);
thread t(func,3,"hello");
func
的第二个参数是string &
,而传入的是一个字符串字面量。该字面量以const char*
类型传入线程空间后,在线程的空间内转换为string
。
例子:
#include <iostream>
#include <thread>
void foo(const int &x, char *mychar,int a)
{
std::cout << &x << " " << &mychar << std::endl;
a = 0;
std::cout << "正在运行的线程为:" << std::this_thread::get_id() << "线程的参数为: " << x << " " << mychar << std::endl;
return;
}
int main()
{
std::cout << "主线程的线程id为: " << std::this_thread::get_id() << std::endl;
int x = 1;
char mybuff[] = "This is a test";
int number = 10;
std::cout << &x << " " << &mybuff << std::endl;
std::thread second(foo, x, mybuff,number);
second.join();
std::cout << number << std::endl;
std::cout << "主线程运行结束" << std::endl;
while (true)
{
}
return 0;
}
转移线程的所有权
thread
是可移动的(movable)的,但不可复制(copyable)。可以通过move
来改变线程的所有权,灵活的决定线程在什么时候join或者detach。
thread t1(f1);
thread t3(move(t1));
将线程从t1转移给t3,这时候t1就不再拥有线程的所有权,调用t1.join
或t1.detach
会出现异常,要使用t3来管理线程。这也就意味着thread
可以作为函数的返回类型,或者作为参数传递给函数,能够更为方便的管理线程。
线程的标识类型为std::thread::id
,有两种方式获得到线程的id。
- 通过
thread
的实例调用get_id()
直接获取 - 在当前线程上调用
this_thread::get_id()
获取
参考博文:
https://www.cnblogs.com/wangguchangqing/p/6134635.html
https://blog.csdn.net/li1615882553/article/details/85342101