c++多线程笔记1

记录参考:https://www.bilibili.com/video/BV1Yb411L7ak

1、 创建线程:
thread myobj(sp);   //sp是一个函数的名,即指向该函数的指针

这句话中创建了一个线程,并且设置线程的起点,即执行的任务。并且开始执行。

2、join的作用:

阻塞主线程,让主线程等待子线程执行完毕,然后主线程再往下执行。

#include <iostream>
#include <thread>
using namespace std;
void sp() {
    cout << "hello"<<endl;
}

int main() {
    thread myobj(sp);    
    myobj.join();     //运行时,先到这里来对主线程进行阻塞,然后再去执行线程的具体内容
    
    cout << "love"<<endl;
}

在如上的多线程中,主线程需要阻塞等待子线程执行结束,然后自己再接着执行并退出,join由子线程提交给主线程,一个线程一个join

如果把join注释掉,单单是创建一个线程并执行,系统会报错,原因是没有阻塞主线程,就可能会子线程还没执行完,主线程就执行完了准备退出。

3、detach作用:

detach的功能和join对立的,join使得主线程必须等待子线程执行完再继续,相当于把子线程和主线程捆在一起。而detach则是把二者分离开来,主线程无需等待子线程执行完成,两者各自执行各自的。

一旦调用了detach就不能再调用join了,detach在调用以后,子线程和主线程分离,主线程不再等待子线程的运行,子线程被转入后台运行。

相当于子线程被C++运行时库接管了,当子线程执行完,由运行时库清理资源。

分别在主线程和子线程中打印多条记录就能看到效果,由于主线程的几句代码输出太快,子线程有时候还没输出进程就结束了,所以我穿插了一些sleep

#include <iostream>
#include <thread>
#include <windows.h>
using namespace std;

void sp() {
    cout << "hello1"<<endl;
    Sleep(1);
    cout << "hello2" << endl;
    Sleep(1);
    cout << "hello3" << endl;
    Sleep(1);
    cout << "hello4" << endl;
    Sleep(1);
    cout << "hello5" << endl;
}

int main() {
    thread myobj(sp);    
    myobj.detach();

    cout << "love"<<endl;
    Sleep(1);
    cout << "love2" << endl;
    Sleep(1);
    cout << "love3" << endl;
    cout << "love4" << endl;
    cout << "love5" << endl;
    return 0;
}

运行结果:
在这里插入图片描述
可以看到其实每次的运行结果都不同,两个线程完全在各自执行自己的代码,互不干扰。甚至子线程输出了一个hello2还没执行换行操作,主线程就立马输出了一个love2和换行。而且hello4hello5还没输出,主线程就结束,进程就退出了,子线程也被终止。

由此可以看到,线程在调用了detach以后,陷入了不可控的状态,我们不能确保这个线程能够被完全执行。创建线程后,可以joindetach二选一,但是为了使线程处于一个可控的范围,最好还是使用join。后面会了解到,detach会引起非常多的问题。

4、joinable作用:

判断是否可以成功使用joindetach。可以使用这两种时返回true,不可以时返回false

什么情况下会返回false呢,上面说过,detach以后就不能设置join了,此时会返回false,设置join后也同样不能再detach了,不然程序会抛出异常。

所以在设置线程join时可以:

if(thread1.joinable()){
    thread1.join();
}

这样就避免了重复设置导致的程序异常结束。

5、使用类来创建线程:
(1)使用仿函数构造:
#include <iostream>
#include <thread>
using namespace std;

class th {
public:
    void operator()() //第一个括号表示重载对象,第二个括号表示重载的参数void   //之前记过,类中重载括号实现仿函数
    {
        cout << "hello"<<endl;
    };
};

int main() {
    th th;
    //由于重载,就有仿函数 th(),作用是输出hello
    thread myobj(th);     
    myobj.join();
    cout << "love"<<endl;
}
(2)存在的问题:
#include <iostream>
#include <thread>
using namespace std;

class th {
public:
    int& m_i;   //定义一个引用
    th(int& i):m_i(i){};  //构造函数传入一个引用量 i。初始化列表将引用i赋给m_i,m_i也变成了传入进来的引用对象的引用
    void operator()() 
    {
        cout << "m_i的值为" << m_i <<endl;
    };
};

int main() {
    int m = 3;

    th th(m);
    thread myobj(th);      //此处th是被复制进去的,重载一个上面的构造函数,参数类型为th,会发现构造函数又被执行了一次,因为此处th不是被引用或指针指过去的,而是拷贝过去,创建了一个临时的th对象,所以又触发了一次构造函数
    myobj.detach();
    cout << "love1" << endl;
}

可以看到这里类的构造函数传入一个引用,将m传入进去,m_i就成为了m的一个引用。由于这里设置的detach,所以主线程和子线程会各自执行各自的。存在的问题就是,要是我主线程快一步的执行完了,m被释放掉了,而m_i又是m的一个引用,子线程还在执着的打印m_i,就会产生不可预料的后果。

要么不引用,要么用join

6、线程传参:
(1)传递临时对象:
#include <iostream>
#include <thread>
using namespace std;
void fa(const int &i,char *j) {
	cout << "i为:" << i << endl
		<< "j为:" << j << endl;
}
int main()
{
	int a = 12;
	char buf[] = "hello";
	thread t1(fa,a,buf);  //第一个是线程的执行函数,往后的参数是函数的参数
	//也可以 thread t1{ fa,a,buf }; 书上使用的是这个花括号
	t1.detach();
	std::cout << "hello world!"<<endl;
	return 0;
}

此处虽然为fa函数传入a 然后作为引用,但是对a和函数内的部引用的地址进行监控,会发现地址不一样,实际内部还是进行了一个复制。

既然是复制,上面5 - (2)中提到的问题怎么会发生呢。回到之前的代码,对参数进行一个监控,就会发现那段代码中传入的m和类中m_i的地址是一样的。

事实上是因为之前的参数m并不是通过线程传参传进去的,而是通过th th(m);th类的构造函数传进去的,所以是实打实的引用,线程中为了避免这一问题,改为了复制一个对象进去,此时函数引用的实际上是复制体,所以这里线程的执行函数中使用引用也是安全的。

(2)存在的问题1:

但是上述代码中,当监控bufj的地址时,会发现bufj的地址相同,代码中使用的指针并不是复制的,所以由于设置了detach,当主线程结束并释放掉指针时,必定会出现问题:
在这里插入图片描述
所以在设置了detach的线程中最好不要使用引用,绝对不能使用指针。

(3)存在的问题2:

既然无法使用指针,那就把指针替换成引用:

#include <iostream>
#include <thread>
using namespace std;
void fa(const int& i, const string &j) {  
//隐式的将变量转换为string,且引用。即由系统来转换其类型。
	cout << "i为:" << i << endl
		<< "j为:" << j.c_str() << endl;
}
int main()
{
	int a = 12;
	char buf[] = "hello";
	thread t1(fa, a, buf); 
	t1.detach();
	std::cout << "hello world!" << endl;
	return 0;
}

由于上面说过,引用以后,实际上线程中是将引用重新复制了一个对象进行引用的,所以即使主线程运行结束释放了相关变量的内存,也不会影响子线程中变量的使用。

事实上,这里还存在一个问题,注意到上面的隐式转换,系统进行隐式转换是需要以buf为基础进行转换的,但是要是在做转换的时候,buf被释放掉,传入子线程的参数也会是无可预料的。

为了解决这一问题,可以将类型转换的操作在主线程中进行,在主线程中将类型转为string,或在传入线程时构造一个临时的string,即可避免隐式的转换,如:

thread t1(fa, a, string(buf));

在子线程的传参中,应尽量避免隐式转换。

(4)总结:
  • 如果传递int这种简单的数据类型,建议直接使用值传递。
  • 如果传递类对象,应避免隐式类型转换。全部在创建线程中就构建临时对象,然后线程的函数参数中使用线程来接收,否则系统还会构造一次函数对象,造成资源的浪费。
  • 建议只是用join,不是万不得已不要用detach,就不存在局部变量失效,导致线程对内存的非法引用。

上面讲的使用引用替换实体,写一下代码:

实体对象

#include <iostream>
#include <thread>
using namespace std;
class A {
public:
	A() { cout << "aa"<<endl; }
	A(const A& a) { cout << "AA" << endl; }
};
void fa(A a) {
}
int main()
{
	int a = 1;
	A a1;
	thread t1(fa, a1);
	t1.join();
	return 0;
}

在这里插入图片描述

引用

#include <iostream>
#include <thread>
using namespace std;
class A {
public:
	A() { cout << "aa"<<endl; }
	A(const A& a) { cout << "AA" << endl; }
};
void fa(const A &a) {
}
int main()
{
	int a = 1;
	A a1;
	thread t1(fa, a1);
	t1.join();
	return 0;
}

在这里插入图片描述
可以看到,上面记过了,引用是拷贝一个对象,然后对其引用,所以过程中构造了一次类A,而直接传递实体对象时,则是进行了两次构造,等于是复制了两次,造成了资源的浪费,所以尽量使用引用。且不管是实体还是引用,都要避免隐式转换。

(5)get_id()获取线程的id:
#include <iostream>
#include <thread>
using namespace std;
void fa() {
	cout << "当前线程id:" << this_thread::get_id() << endl;
}
int main()
{
	cout <<"主线程id:"<< this_thread::get_id() << endl;  
	int a = 12;
	char buf[] = "hello";
	
	thread t1(fa);
	cout << "t1的线程id:" << t1.get_id() << endl;
	
	t1.join();

	thread t2(fa);
	cout << "t2的线程id:" << t2.get_id() << endl;
	t2.join(); 
	
	return 0;
}

在这里插入图片描述

(6)修改线程中的引用:

将线程函数中,引用参数的const删去,发现程序报错invoke,这就意味着想要在线程中修改传入的参数是不可行的。

即使使用volatilemutable修饰变量,也难以在线程中对其进行修改。

这一问题可以通过设置std:ref()来解决,设置以后,线程函数中的引用的const就可以去掉了:

#include <iostream>
#include <thread>
using namespace std;
void fa(int& i) {
	i = 2;
}
int main()
{
	int a = 1;
	thread t1(fa, std::ref(a)); //std::ref 用于修饰引用传递
	t1.join();

	cout << "a:" << a;
	return 0;
}

在这里插入图片描述
要注意的是,前面写过了,如果传递一个引用给线程函数以后,线程函数中的引用实际上是拷贝了一个新的对象来引用,而添加了ref以后,就变成了真正的引用了,此时如果再用detach就又陷入到了主线程执行完,释放了内存,造成子线程非法访问,所以如果设置了ref,一定搭配join来使用。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值