C++11 线程

C++11 std::thread 浅析

C++从11开始在语言层面引进了线程以及同步相关的语义支持,今天针对thread的源码把自己的理解大概记录一下。

先看看关于thread类的介绍:

std::thread,
C++ Thread support library std::thread
Defined in header
class thread;
(since C++11)

The class thread represents a single thread of execution. Threads allow multiple functions to execute concurrently.
Threads begin execution immediately upon construction of the associated thread object (pending any OS scheduling delays), starting at the top-level function provided as a constructor argument. The return value of the top-level function is ignored and if it terminates by throwing an exception, std::terminate is called. The top-level function may communicate its return value or an exception to the caller via std::promise or by modifying shared variables (which may require synchronization, see std::mutex and std::atomic)
std::thread objects may also be in the state that does not represent any thread (after default construction, move from, detach, or join), and a thread of execution may be not associated with any thread objects (after detach).
No two std::thread objects may represent the same thread of execution; std::thread is not CopyConstructible or CopyAssignable, although it is MoveConstructible and MoveAssignable.


翻译如下:

1、类 thread 表示单个执行线程。线程允许多个函数并发执行。

2、一旦关联的线程对象体被构造,线程立刻开始执行(具体依赖相应OS的调度延迟),线程的执行入口是 构造thread对象时传递的函数,函数对象,lambda表达式等参数等。

3、线程会忽略执行函数的返回值,若执行函数在执行过程中抛出异常,系统调用 std::terminate终止该线程 。

4、执行函数可以通过 std::promise 在多个线程间传递返回值或一个异常,也可以通过同步机制如std::mutex,std::atomic等去修改一个共享变量。

5、std::thread 可能处在一个不关联任何执行体的状态(在默认构造、移动、 detach 或 join 之后),一个detach后的thread不能再关联任何执行体。

6、没有两个 std::thread 对象会表示同一执行流; 因为std::thread 不可拷贝构造 (CopyConstructible) 和赋值 (CopyAssignable) ,但是拥有移动语义,即移动构造 (MoveConstructible) 和移动赋值 (MoveAssignable) 。


由于不知从何说起,就先从上面这个6个问题开始吧:

  • 问题1:就不用说了,引入线程的目的就为此。

  • 问题2:先看代码:


		//普通函数
		void printFun(std::string s)
		{
			std::cout << "in pure function " << s << std::endl;
		}
		
		
		//函数对象
		class PrintObj
		{
		public:
			void operator()(std::string s) const
			{
				std::cout << "in function object: " << s << std::endl;
			}
		};
		
		//Lamdba 表达式
		auto lambFun = [](std::string s) {
			std::cout << "in lambda function: " << s << std::endl;
		};
		
		int main()
		{
			std::string s = "Hello World!";

			/*std::vector<std::thread> threads;
			threads.emplace_back(printFun, s);
			threads.emplace_back(PrintObj(), s);
			threads.emplace_back(lambFun, s);
			for (auto& t : threads)
				t.join();
			std::for_each(threads.begin(), threads.end(), std::mem_fn(&std::thread::join));*/

			std::thread th_1(printFun, s);
			std::thread th_2(PrintObj(), s);
			std::thread th_3(lambFun, s);
			th_1.join();
			th_2.join();
			th_3.join();
			return 0;
		}

输出结果如下:

结合上面的代码,首先看看std::thread的源码:

		
		//std::thread中封装的数据实体,thread的成员变量_Thr,即为_Thrd_imp_t类型,
		//_Thrd_imp_t包含两项数据成员,一个系统底层的handle,一个线程id。
		typedef unsigned int _Thrd_id_t;
		typedef struct
			{	/* thread identifier for Win32 */
			void *_Hnd;	/* Win32 HANDLE */
			_Thrd_id_t _Id;
			} _Thrd_imp_t;
		
		//操作_Thrd_imp_t的宏定义,主要为thread服务。
		#define _Thr_val(thr) thr._Id
		#define _Thr_set_null(thr) (thr._Hnd = nullptr, thr._Id = 0)
		#define _Thr_is_null(thr) (thr._Id == 0)

		class thread
			{	// class for observing and managing threads
		public:
			class id; //内部类,对thread-id _Thrd_id_t数据类型的封装。
		
			typedef void *native_handle_type;
		
			thread() noexcept
				{	// construct with no thread
				_Thr_set_null(_Thr); //默认构造函数handle = nullptr,id = 0,不关联任何执行体,也就是个摆设,基本用不到。
				}
		
			//(1)
			//thread最重要的构造函数,可以看出,利用了perfectly forward特性,将执行体的_Fx和参数_Ax转发给thread对象(注意还传递了&_Thr),然后Lunch这个thread。
			//这里有一个地方需要特别注意,传给thread的参数实际上是复制了一份在tuple中,供后续的线程使用,后面会具体解释这块。
			template<class _Fn,
				class... _Args,
				class = enable_if_t<!is_same_v<remove_cv_t<remove_reference_t<_Fn>>, thread>>>
				explicit thread(_Fn&& _Fx, _Args&&... _Ax)
				{	// construct with _Fx(_Ax...)
				_Launch(&_Thr,
					_STD make_unique<tuple<decay_t<_Fn>, decay_t<_Args>...> >(
						_STD forward<_Fn>(_Fx), _STD forward<_Args>(_Ax)...));
				}
		
			//(2)
			//析构函数,此处需要注意的是,析构时,会先判断是否可joinable,如果已经join()
			//或detach()的过,此处显然什么都不做。从这里也可以看出,如果子线程不可joinable,当主线程退出时,子线程也会被terminate。
			~thread() noexcept
				{	// clean up
				if (joinable())
					_STD terminate();
				}
		
			//移动拷贝,不多解释。
			thread(thread&& _Other) noexcept
				: _Thr(_Other._Thr)
				{	// move from _Other
				_Thr_set_null(_Other._Thr);
				}
		
			//移动复制
			thread& operator=(thread&& _Other) noexcept
				{	// move from _Other
				return (_Move_thread(_Other));
				}
		
			//注意这里:delete掉拷贝构造和复制,意味着thread的资源只能转移并不能复制和拷贝。
			thread(const thread&) = delete;
			thread& operator=(const thread&) = delete;
		
			void swap(thread& _Other) noexcept
				{	// swap with _Other
				_STD swap(_Thr, _Other._Thr);
				}
		
			//(3)
			//实际在判断(thr._Id != 0),也就是说,只有thead-id不为零,thead对象才是
			可joinable的。从上面可知,default构造函数后,id == 0,线程不可joinable。还有就是join()和detach()后,都会_Thr_set_null(_Thr),将thead-id设置为零,
			此时thread变成不可joinable。所以joinable的状态是一次性的,不可重复在用。
			_NODISCARD bool joinable() const noexcept
				{	// return true if this thread can be joined
				return (!_Thr_is_null(_Thr));
				}
		
			void join();
		
			//先判断是否可joinable,否则throw exception
			void detach()
				{	// detach thread
				if (!joinable())
					_Throw_Cpp_error(_INVALID_ARGUMENT);
				_Thrd_detachX(_Thr);
				_Thr_set_null(_Thr);
				}
		
			_NODISCARD id get_id() const noexcept;
		
			_NODISCARD static unsigned int hardware_concurrency() noexcept
				{	// return number of hardware thread contexts
				return (_Thrd_hardware_concurrency());
				}
		
			_NODISCARD native_handle_type native_handle()
				{	// return Win32 HANDLE as void *
				return (_Thr._Hnd);
				}
		
		private:
			thread& _Move_thread(thread& _Other)
				{	// move from _Other
				if (joinable())
					_STD terminate();
				_Thr = _Other._Thr;
				_Thr_set_null(_Other._Thr);
				return (*this);
				}
		
			_Thrd_t _Thr; //见开始部分的定义,thread唯一的一个成员变量,包含handle 和 id。
			};


		//不多解释了,就是对_Thrd_id_t的封装,把一个基本类型数据unsigned int变成一个封装类型。
		class thread::id
			{	// thread id
		public:
			id() noexcept
				: _Id(0)
				{	// id for no thread
				}
		
			//重载std::cout << id
			template<class _Ch,
				class _Tr>
				basic_ostream<_Ch, _Tr>& _To_text(
					basic_ostream<_Ch, _Tr>& _Str)
				{	// insert representation into stream
				return (_Str << _Id);
				}
		
		private:
			id(_Thrd_id_t _Other_id)
				: _Id(_Other_id)
				{	// construct from unique id
				}
		
			_Thrd_id_t _Id;
		
			friend thread::id thread::get_id() const noexcept;
			friend thread::id this_thread::get_id() noexcept;
			friend bool operator==(thread::id _Left, thread::id _Right) noexcept;
			friend bool operator<(thread::id _Left, thread::id _Right) noexcept;
			friend hash<thread::id>;
			};

注释(1)、(2)、(3)解释如下:

1、参数拷贝的问题,先看例子:


		int num = 0; //global varible
		void changeNum(const int& n)
		{
			int& m = const_cast<int&>(n);
			++m;
			std::cout << "in changeNum: m == " << m << std::endl;
			std::cout << "in changeNum: num == " << num << std::endl;
		}
		
		int main()
		{
			std::thread th(changeNum, num);
			th.join();
			std::cout << "in main: num = " << num << std::endl;
			return 0;
		}

可以看出,按引用传给thread的变量num,并没有在子线程中因 ++m 而改变,原因就在于参数在内部被复制了,不管你是按照引用还是其他方式传递,具体原因看代码:


	template<class _Fn,
			class... _Args,
			class = enable_if_t<!is_same_v<remove_cv_t<remove_reference_t<_Fn>>, thread>>>
			explicit thread(_Fn&& _Fx, _Args&&... _Ax)
			{	// construct with _Fx(_Ax...)
			_Launch(&_Thr,
				_STD make_unique<tuple<decay_t<_Fn>, decay_t<_Args>...> >(
					_STD forward<_Fn>(_Fx), _STD forward<_Args>(_Ax)...));
			}

	template<class _Ty>
		struct decay
		{	// determines decayed version of _Ty
		using _Ty1 = remove_reference_t<_Ty>;
	
		using type = conditional_t<is_array_v<_Ty1>,
			add_pointer_t<remove_extent_t<_Ty1>>,
			conditional_t<is_function_v<_Ty1>,
				add_pointer_t<_Ty1>,
				remove_cv_t<_Ty1>>>;
		};
	
	
	template<class _Ty>
		using decay_t = typename decay<_Ty>::type;
	
	template<class _Ty,
		class... _Types,
		enable_if_t<!is_array_v<_Ty>, int> = 0>
		_NODISCARD inline unique_ptr<_Ty> make_unique(_Types&&... _Args)
		{	// make a unique_ptr
		return (unique_ptr<_Ty>(new _Ty(_STD forward<_Types>(_Args)...)));
		}


	template<class _This,
		class... _Rest>
		class tuple<_This, _Rest...>
			: private tuple<_Rest...>
		{	// recursive tuple definition
	public:
		typedef _This _This_type;
		typedef tuple<_Rest...> _Mybase;
	
		template<class _Tag,
			class _This2,
			class... _Rest2,
			enable_if_t<is_same_v<_Tag, _Exact_args_t>, int> = 0>
			constexpr tuple(_Tag, _This2&& _This_arg, _Rest2&&... _Rest_arg)
			: _Mybase(_Exact_args_t{}, _STD forward<_Rest2>(_Rest_arg)...),
				_Myfirst(_STD forward<_This2>(_This_arg))
			{	// construct from one arg per element
			}
	.........................

从上面可以看出,传递给std::thread的参数:_Fx,_Ax 先通过std::make_unique 包装成一个指向tuple的unique_ptr,再传给_Launch。关键就在于构建tuple时的模板实例化参数,decay_t<_Fn>, decay_t<_Args>… ,正是由于它,才导致参数的拷贝。下面先说说decay_t的作用:

decay_t<_Ty>的作用如下:
首先移除_Ty的引用修饰,产生新类型 _Ty1;
a、如果_Ty1是数组,先移除扩展,然后加指针修饰;
	eg: 若_Ty是 int[5],则 decay_t<_Ty>::type == int*
b、若_Ty1不是数组类型,如果是函数,则为函数指针,否则移除const 和 volatile修饰.

这样构建的tuple<decay_t<_Fn>, decay_t<_Args>…>对象,在内部分别调用了每个参数对应成员的拷贝构造函数,如 tuple 的构造函数的初始化成员列表中的**_Myfirst(_STD forward<_This2>(_This_arg))**,正因如此,才产生了数据的拷贝。

概括起来,就是传给std::thread的参数会被内部拷贝一份存储在std::tuple中

既然说到这里了,那再举一个不拷贝的例子,看看std::call_once的实现,此函数的原型作用如下:

std::call_once C++ Thread support library Defined in header <mutex>

template< class Callable, class… Args >
void call_once( std::once_flag& flag, Callable&& f, Args&&… args );
(since C++11)

Executes the Callable object f exactly once, even if called concurrently, from >several threads.
call_once invokes ​std​::​forward(f) with the arguments std​::​>forward(args)… (as if by std::invoke). Unlike the std::thread >constructor or std::async, the arguments are not moved or copied because they >don’t need to be transferred to another thread of execution.

std::call_once的作用就是只允许传递给它的函数f执行一次(一般用于初始化),即便是多线程环境下。

先看看该函数的大概使用:


	std::once_flag flag1;
	int num = 0;
	void simple_do_once()
	{
		std::call_once(flag1, [](int& x) { 
			++x;
			std::cout << "Simple example: called once\n"; },num);
	}
	
	int main()
	{
		//四个线程同时执行,但是call\_once中的函数只被执行了一次。
		//并且global变量num确实被改变了
		std::thread st1(simple_do_once);
		std::thread st2(simple_do_once);
		std::thread st3(simple_do_once);
		std::thread st4(simple_do_once);
		st1.join();
		st2.join();
		st3.join();
		st4.join();
		std::cout << "num = " << num << std::endl;
		return 0;
}


输出如下:

前面说过std::thread中传递的参数是内部拷贝的,但是此处显然不是,原因何在?看代码:


	template<class _Fn,
		class... _Args> inline
		void (call_once)(once_flag& _Flag, _Fn&& _Fx, _Args&&... _Ax)
		{	// call _Fx(_Ax...) once
		//关键就在这里,实例化tuple的参数不在是decay_t<_Fn>了,而是_Fn&&
		//根据perfectly forward的知识,外部传递左值,tuple中就是左值引用,
		//传递右值,就是右值引用。
		typedef tuple<_Fn&&, _Args&&..., exception_ptr&> _Tuple;
		typedef make_index_sequence<1 + sizeof...(_Args)> _Seq;
	
		exception_ptr _Exc;
		_Tuple _Tup(_STD forward<_Fn>(_Fx), _STD forward<_Args>(_Ax)..., _Exc);

好了,关于std::thread拷贝参数的问题就算彻底讲完了。下面说说,如果传递给std::thread的函数确实需要引用参数该怎么办?看代码:


	void changeNum(int m, int& n, std::string& s)
	{
		++m;
		++n;
		s.append("__append");
	}
	
	int main()
	{
		int num_1 = 0;
		int num_2 = 10;
		std::string str{ "Hello" };
		//对需传递引用的地方用std::ref包装
		std::thread th(changeNum, num_1, std::ref(num_2), std::ref(str));
		th.join();
		std::cout << "in main: num_1 = " << num_1 << std::endl;
		std::cout << "in main: num_2 = " << num_2 << std::endl;
		std::cout << "in main: str = " << str << std::endl;
		return 0;
	}

输出如下:

从上面可以看出,经std::ref包装后,程序产生的结果,确实达到了我们的预期,原因何在?,std::ref的机理有是什么?


		// FUNCTION TEMPLATES ref AND cref
	template<class _Ty>
		_NODISCARD inline reference_wrapper<_Ty> ref(_Ty& _Val) noexcept
		{	// create reference_wrapper<_Ty> object
		return (reference_wrapper<_Ty>(_Val));
		}
	
	template<class _Ty>
		void ref(const _Ty&&) = delete;
	
	template<class _Ty>
		_NODISCARD inline reference_wrapper<_Ty> ref(reference_wrapper<_Ty> _Val) noexcept
		{	// create reference_wrapper<_Ty> object
		return (_STD ref(_Val.get()));
		}

此函数有三个版本,从上到下依次为: 左值引用,delete掉右值引用,由reference_wrapper产生一个新的对象reference_wrapper,其实质还是指向同一个外部左值。我们例子中用的就是第一个 左值引用版本。 想想为什么要删除 右值引用呢? 原因很简单,std::ref的作用就是为了通过这种方式以达到改变变量值得目的,为程序后续所用,而右值即使改变了也不可能被程序其他地方所使用,所以根本就没有必要就改变它,直接delete掉。
从上面代码看出,std::ref把_Val包装成一个reference_wrapper类型的变量,那我们看看reference_wrapper是如何实现的:


	template<class _Ty>
		class reference_wrapper
			: public _Weak_types<_Ty>::type
		{	// stand-in for an assignable reference
	public:
		static_assert(is_object_v<_Ty> || is_function_v<_Ty>,
			"reference_wrapper<T> requires T to be an object type or a function type.");
	
		using type = _Ty;
	
		template<class _Uty,
			enable_if_t<conjunction_v<
				negation<is_same<remove_cv_t<remove_reference_t<_Uty>>, reference_wrapper>>,
				_Refwrap_has_ctor_from<_Ty, _Uty>>, int> = 0>
			reference_wrapper(_Uty&& _Val)
				_NOEXCEPT_COND(_NOEXCEPT_OPER(_Refwrap_ctor_fun<_Ty>(_STD declval<_Uty>())))
				{	// construct
				_Ty& _Ref = _STD forward<_Uty>(_Val);
				_Ptr = _STD addressof(_Ref);
				}
	
		//注意这个类型转换函数,在真正调用函数的地方,其实存在下面的隐式类型转换,
		//使得被调用函数的引用参数仍能引用到外部变量,这可能就是这个类的核心所在。
		operator _Ty&() const noexcept
			{	// return reference
			return (*_Ptr);
			}
	
		_NODISCARD _Ty& get() const noexcept
			{	// return reference
			return (*_Ptr);
			}
	
		template<class... _Types>
			auto operator()(_Types&&... _Args) const
			-> decltype(_STD invoke(get(), _STD forward<_Types>(_Args)...))
			{	// invoke object/function
			return (_STD invoke(get(), _STD forward<_Types>(_Args)...));
			}
	
	private:
		_Ty * _Ptr; //此模板实际存储的是外部变量的地址。
		};

到此,std::ref的作用了说清楚了,其实还有一个std::cref,道理都一样,只不过实例化reference_wrapper时用const T 参数而已。

2、std::thread 析构函数的问题,先看例子:


	void thr_fun()
	{
		while (true)
		{
			using namespace std::chrono_literals;
			std::this_thread::sleep_for(1s);
			std::cout << "thead-id: " << std::this_thread::get_id()<< std::endl;
		}
	}
	int main()
	{
		std::thread thr{thr_fun};
		std::this_thread::sleep_for(std::chrono::seconds(3));
		//此处专门不join子线程
		return 0;
	}

输出结果:

此处产生Debug Error!对话框的原因就是因为子线程还处于joinable状态,主线程已经退出了。
导致std::thread的析构函数被调用,进而调用terminate()终止程序。所以得注意。

3、关于线程的joinable问题, 先看例子:


	//io输出同步锁
	std::mutex io_mutex;
	void thr_fun()
	{
		using namespace std::chrono_literals;
		std::this_thread::sleep_for(1s);
		std::lock_guard<std::mutex> lk(io_mutex);
		std::cout << "thead-id: " << std::this_thread::get_id() << std::endl;
	}
	int main()
	{
		std::thread thr_1{ thr_fun };
		std::thread thr_2{ thr_fun };
		std::thread thr_3;
		thr_1.join();
		//thr_2.detach();
		thr_2.join();
		std::cout << "thr_1 thead: " << thr_1.joinable() << std::endl;
		std::cout << "thr_2 thead: " << thr_2.joinable() << std::endl;
		std::cout << "thr_3 thead: " << thr_3.joinable() << std::endl;
		return 0;
	}

输出结果:

结合这个例子,以及前面的源码,可以看出,线程是否可joinable等价于线程关联的id是否为0,
若调用std::thread的default construct构造线程 或者 调用join() 和 detach(),都会将线程的id设置为0,同时在调用join()和 detach()之前还会判断是否可joinable,否则 _Throw_Cpp_error(_INVALID_ARGUMENT)。

问题2就到此结束。

  • 问题3:
    a、线程会忽略执行函数的返回值
    answer: 类似java中的thread,函数的返回值没法从std::thread中得到,实际上std::thread也未提供相关的接口,参考前面贴出的源码。实际上C++11中提供了类似java中的Callable,FutureTask等高级技法去communicate with many thread,后面再讲。

    b、若执行函数在执行过程中抛出异常,系统调用 std::terminate终止该线程
    answer: 不多解释,看代码


	void thr_fun(bool flag)
	{
		if (flag == true)
			throw std::current_exception; //系统直接调用std::terminate终止
		std::cout << "thread-id: " << std::this_thread::get_id() << std::endl;
	}
	int main()
	{
		std::thread thr_1(thr_fun,true);
		thr_1.join();
		return 0;
	}

此篇文章就到这里。下一篇讲讲C++11中线程同步的知识,主要涉及:

锁的类型:
std::mutex,std::timed_mutex,std::shared_mutex,std::shared、_timed_mutex
条件变量:
std::condition_variable, std::cv_status
锁的管理(RAII):
std::lock_guard,std::scoped_lock,std::unique_lock,std::shared_lock
锁的控制策略:
std::defer_lock_t,std::try_to_lock_t,std::adopt_lock_t
一般的锁算法:
std::lock, std::try_lock
期望和承诺:
std::promise,std::packaged_task,std::future,std::async,std::future_status

第一次写文章,望各位海涵!!

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值