【C++11多线程入门教程】系列之线程传参模式

线程函数传递临时对象

  首先,我们先来看一下线程函数传递的临时对象,引用传递与函数指针传递;我们知道使用detach()函数后,主线程与子线程脱离关联,各自独立运行,主线程运行结束,可能会导致子线程数据非法访问的问题。我们看下面的错误示例:

#include <iostream>
#include <thread>
#include <Windows.h>

void PrintMsg(const int& var, char* flow)
{
	std::cout << "var address is : " << var << std::endl;
	std::cout << "flow address is : " << flow << std::endl; // ①
}

int main(void)
{
	std::cout << "the main thread starts." << std::endl;
	int mvar = 1;
	char tmp_flow[] = "I learn C++ Thread.";
	
	std::thread print(PrintMsg, mvar, tmp_flow); // ②
	//print.join();
	print.detach();

	std::cout << "the main thread end." << std::endl;

	return 0;
}

  上述代码中线程PrintMsg()函数分别传递的是引用mvar和tmp_flow的数据参数,我们知道使用线程阻塞join()函数不会出现任何问题。但是,如果使用detach()函数,脱离关联独自运行。那么,这里可能会涉及到非法访问。我们首先看一下函数的地址:

在这里插入图片描述
  从上面调试地址值可以看到,整形变量的引用作为函数参数传递,但是实际上进行地址拷贝操作。虽然不会出现非法引用,但是并不建议这么操作,还是建议传值。参数指针传递,tmp_flowflow的地址完全一致,说明线程函数执行时候读取tmp_flow的地址所有数据值。那么,这里就会存在问题:如果主线程执行完毕,子线程还在运行。主线程的tmp_flow已经被释放,这个时候子线程还需要访问数据,就会造成崩溃。因此,使用detach()函数,一定要关注线程函数入参的生命周期。或者,尽量不要使用detach(),使用线程join()来阻塞,保证线程函数参数的生命周期。

线程函数传类对象

#include <iostream>
#include <thread>

class A
{
public:
	A(int m_i) :m_i_(m_i)
	{
		std::cout << "A::A()构造函数执行." << std::endl;
	}

	A(const A& a) :m_i_(a.m_i_)
	{
		std::cout << "A::A(const A& a)拷贝构造函数执行." << std::endl;
	}

	~A()
	{
		std::cout << "A::~A()析构函数执行." << std::endl;
	}

public:
	int m_i_;
};

void print(const A& a) // ③
//void print(const A a) // ④
{
	std::cout << "the A address is " << &a << std::endl;
}

int main(void)
{
	int m_i = 3;

	A a(m_i);

	std::cout << "the main thread A address is " << &a << std::endl;

	std::thread print(print, a);         // ①
	//std::thread print(print, std::ref(a));   // ②

	print.join();
	//print.detach();

	return 0;
}

  简单依据上述代码进行分析,想必都注意到代码中的①②③④的符号,分别对应不同的使用情况导致不同的输出结果。

  • 使用③①组合,输出的结果为:
A::A()构造函数执行.
the main thread A address is 010FFA84
A::A(const A& a)拷贝构造函数执行.
the A address is 0155E550
A::~A()析构函数执行.
A::~A()析构函数执行.
  • 使用④①组合,输出的结果为:
A::A()构造函数执行.
the main thread A address is 00CFFA40
A::A(const A& a)拷贝构造函数执行.
A::A(const A& a)拷贝构造函数执行.
the A address is 00E3F960
A::~A()析构函数执行.
A::~A()析构函数执行.
A::~A()析构函数执行.
  • 使用③②情况,输出的结果为:
A::A()构造函数执行.
the main thread A address is 006FF870
the A address is 006FF870
A::~A()析构函数执行.
  • 使用④②情况下,输出的结果为:
A::A()构造函数执行.
the main thread A address is 006FFCA8
A::A(const A& a)拷贝构造函数执行.
the A address is 007FF508
A::~A()析构函数执行.
A::~A()析构函数执行.

  分析上述输出:我们通过上述几个不同的输出对比可以看到,④①组合调用了两次拷贝构造函数,③②一次拷贝构造函数也没有调用。因此③②组合效率最快,④①组合效率最差。其中③①与④②分别调用了一次拷贝构造函数,但是它们两个调用拷贝构造函数是不同的阶段。

  • ③①产生的拷贝构造函数是启动线程,线程触发的一次类对象拷贝;
  • ④②产生的拷贝构造函数是print()函数入参以传值模式,触发的拷贝类对象;
  • ④①产生的拷贝构造函数分别是启动线程与函数传值导致两次拷贝类对象;
  • ③②将线程触发改为类对象引用,线程函数入参也为引用,因此省去两次拷贝构造;

每多一次拷贝构造,分别对应多一次析构。

  上述代码在使用线程阻塞join()的情况下,并不会出现任何问题。但是,一旦使用detach()函数,使用引用②将会导致不安全情况出现,如果主线程执行结束,类A对象已经析构,这个时候子线程还需要引用类A对象的数据,这将造成索引失败,导致崩溃。

线程函数传智能指针

  关于智能指针作为线程函数参数传递时候,必须使用移动构造函数的方式进行传参,std::move()将智能指针采取右值引用的权限授权给线程函数ptr。如果使用std::ref()来引用其智能指针地址,不涉及拷贝问题。但是,需要时刻关注生命周期。

#include <iostream>
#include <thread>

// shared_ptr unique_ptr 
void use_smart_pointer(std::unique_ptr<int>& ptr) // std::ref() 这里必须使用&
{
	std::cout << "use_smart_pointer()." << std::endl;
}


int main(void)
{
	std::cout << "main thread start." << std::endl;
	std::unique_ptr<int> m_ptr(new int(10));

	std::thread testUniquePtr(use_smart_pointer, std::move(m_ptr));
	//std::thread testUniquePtr(use_smart_pointer, std::ref(m_ptr)); 

	testUniquePtr.join();

	std::cout << "main thread end." << std::endl;

	return 0;
}
线程函数传递成员函数

  接下来我们看一下,线程函数调用类成员函数的方式。我们通过获取线程的id来判断线程处于主线程还是子线程。调用方式:std::thread temp(&A::work, a, parameters)。其中A为类,a为类对象,work为类A的成员函数,后面的parameterswork()函数的入参。

#include <iostream>
#include <thread>

class A
{
public:
	A(int m_i) :m_i_(m_i)
	{
		std::cout << "A::A()构造函数执行." << this << "| get thread_id " << std::this_thread::get_id() << std::endl;
	}

	A(const A& a) :m_i_(a.m_i_)
	{
		std::cout << "A::A(const A& a)拷贝构造函数执行." << this << "| get thread_id " << std::this_thread::get_id() << std::endl;
	}

	~A()
	{
		std::cout << "A::~A()析构函数执行." << this << "| get thread_id " << std::this_thread::get_id() << std::endl;
	}

	void work(int n)
	{
		std::cout << "work函数执行 " << this << "| get thread_id " << std::this_thread::get_id() << std::endl;
	}

public:
	int m_i_;
};


int main(void)
{
	int m_i = 3;

	A a(m_i);

	std::cout << "主线程 ID " << "| get thread_id " << std::this_thread::get_id() << std::endl;

	std::thread print(&A::work, a, m_i);   // ②

	print.join();
	//print.detach();

	return 0;
}

  输出结果如下:我们可以看到构造函数、拷贝构造函数都在主线程id下面进行操作,说明调用线程函数时候,执行在主线程上生成临时对象,然后子线程进行执行work()函数。

A::A()构造函数执行.004FFE40| get thread_id 17136
主线程 ID | get thread_id 17136
A::A(const A& a)拷贝构造函数执行.009CE6F4| get thread_id 17136
work函数执行 009CE6F4| get thread_id 17776
A::~A()析构函数执行.009CE6F4| get thread_id 17776
A::~A()析构函数执行.004FFE40| get thread_id 17136
线程函数传递参数std::ref()

  试想一下,我们启动一个新的线程函数,可能会需要进行对该线程执行结束后的参数调用。那么,我们就需要使用std::ref()来进行操作。前面的例子中我们已经看到使用std::ref()只是引用了地址,并未进行拷贝操作。

#include <iostream>
#include <thread>
#include <Windows.h>

void PrintMsg(int& var, char* flow)
{
	var = 5;
	std::cout << "var value is : " << var << std::endl;
}

int main(void)
{
	std::cout << "the main thread starts." << std::endl;
	int mvar = 1;
	char tmp_flow[] = "I learn C++ Thread.";

	std::cout << "mvar value before is " << mvar << std::endl;
	
	std::thread print(PrintMsg, std::ref(mvar), tmp_flow);

	print.join();
	//print.detach();
	std::cout << "mvar value after is " << mvar << std::endl;

	std::cout << "the main thread end." << std::endl;

	return 0;
}

上述的一个小程序,主要就是想介绍一下,如果主线程需要使用子线程PrintMsg()执行后的mvar结果,为下面继续操作,那么我们就要使用std::ref()来进行操作。mvar的值经过子线程执行后,值发生改变。

the main thread starts.
mvar value before is 1
var value is : 5
mvar value after is 5
the main thread end.
小结

线程传参经验小结:

  1. 线程函数参数为单个类型值情况下(非结构体、类等),建议使用传值模式,不要使用指针或引用;
  2. 线程函数参数为类对象情况下,建议使用引用方式,避免多次调用拷贝构造函数。不过需要注意类对象的生命周期;
  3. 如果你想要通过线程函数改变参数值,来为后续使用的话,建议使用std::ref()来进行真正的引用;
  4. 使用detach()函数将线程关联脱离时候,必须小心数据的生命周期。
  5. 一般线程调用主要使用阻塞join()函数,以此来避免使用detach()函数导致的生命周期问题。但是,如果使用detach()则必须时刻关注子线程调用的数据是否会随着主线程结束而释放导致子线程非法访问。
参考文献

https://zh.cppreference.com/w/cpp/thread/thread

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
单例模式是一种设计模式,它保证类在整个程序中只能创建一个实例。在多线程环境下,如果不加以处理,可能会出现多个线程同时调用getInstance()方法创建实例的问题,从而违反了单例模式的原则。 为了在多线程环境下保证单例模式的正确性,可以采用以下几种解决方案: 1. 懒汉式-线程不安全:在 getInstance() 方法中进行实例化时,没有进行多线程并发控制,可能会导致创建多个实例的问题。 2. 懒汉式-线程安全:在 getInstance() 方法加上 synchronized 关键字,使用同步锁来控制多线程并发访问,确保只有一个线程能够创建实例。但是,由于加锁会造成多线程竞争锁资源的性能损耗,因此并不推荐使用该方式。 3. 饿汉式:在类加载时就进行实例化,保证了线程安全,不存在并发问题。但是,由于直接创建对象实例,可能会占用空间,影响程序的性能。 4. 双重检查锁定:使用 volatile 关键字来保证多线程环境下的可见性,通过两次判断实例是否为 null 来控制并发访问。第一次判断是为了避免不必要的同步锁开销,第二次判断是为了在实例为 null 的情况下进行同步锁。这种方式可以避免懒汉式加锁方式的性能问题。 5. 静态内部类:利用类加载机制和类初始化锁的特性,在静态内部类中创建实例,保证了线程安全性和延迟加载。通过静态内部类的方式创建单例,只有在调用 getInstance() 方法时才会加载内部类,从而实现了懒加载。 综上所述,针对多线程环境下的单例模式,可以根据具体需求选择适当的实现方式。在保证线程安全的前提下,尽量避免加锁操作,以提高程序的性能。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值