C++ thread 源码阅读笔记

发布时间 2023-04-06 21:43:25作者: 木制品

thread类解析

构造函数

thread()

无参构造,会创建一个空的线程对象。

thread(FunctionCallback, ...Args)

创建并开启一个线程,线程任务就是参数里的回调函数。

thread(thread&& other)

移动构造,具体请参照C++的移动语义。

PS:

thread类没有拷贝构造。

thread(const thread&) = delete;
thread& operator=(const thread&) = delete;

成员函数

join函数解释

阻塞调用线程的,在一个线程环境调用,那么这个线程环境将会等待join()返回后才继续往下执行
如代码1.1,输出结果:
Main: 18972
子线程开始:10188
i=0 i=1 i=2 i=3 i=4 i=5 i=6 i=7 i=8 i=9
Main: 18972 // 只一句话无论执行该程序多少次,永远会在t1线程任务完成后才会输出

代码清单1.1
#include <thread>
#include <iostream>
using namespace std;

void test1()
{
	cout << "子线程开始:" << this_thread::get_id() << endl;
	for (int i = 0; i < 10; i++)
	{
		cout << " i=" << i;
	}
}

int main()
{
	cout<< "\r\nMain: " << this_thread::get_id() << endl;

	thread t1{test1};

	// 把t1 加入main线程,main线程会等待t1执行完成后才会向下执行
	t1.join();

	cout << "\r\nMain: " << this_thread::get_id() << endl;

	return 0;
}

修改代码清单1.1 的部分内容,进一步体会join()。

void test1()
{
	cout << "子线程开始:" << this_thread::get_id() << endl;
	for (int i = 0; i < 10; i++)
	{
		this_thread::sleep_for(chrono::seconds(1));
		cout << " i=" << i;
	}
}

int main()
{
	cout<< "\r\nMain: " << this_thread::get_id() << endl;

	thread t1{test1}; // 创建线程,并开始执行 

    // 该部分代码会和 t1 线程并发执行
	for (int i = 0; i < 10; i++)
	{
		this_thread::sleep_for(chrono::seconds(1));
		cout << " j=" << i;
	}

	// 把t1 加入main线程,main线程会等待t1执行完成
	t1.join();

    // 该部分代码会等待t1线程执行完后才执行
	for (int i = 0; i < 10; i++)
	{
		this_thread::sleep_for(chrono::seconds(1));
		cout << " k=" << i;
	}

	cout << "\r\nMain: " << this_thread::get_id() << endl;

	return 0;
}

PS: 如果打算等待对应线程,则需要细心挑选调用join()的位置。


detach函数解释

情景搭建:有这么一个情况,用户关闭了应用程序,但是用户想要下载并不停止。
分析:总所周知,关闭一个程序后操作系统会回收分配的资源,包括给程序分配的变量。如果用户关闭了程序,但是你的下载线程并没有结束,这个时候就会报错。如图:
image

该报错是由代码清单1.2引起的。

代码清单1.2
#include <thread>
#include <iostream>
using namespace std;

void test1()
{
    // 至少会执行 100s
	cout << "子线程开始:" << this_thread::get_id() << endl;
	for (int i = 0; i < 100; i++)
	{
		this_thread::sleep_for(chrono::seconds(1));
		cout << " i=" << i << endl;
	}
}

int main()
{
	// 一个代码块,程序出这个代码块后,t1会被释放
	{
		thread t1{ test1 };
	}

	for (int i = 0; i < 10; i++)
	{
		this_thread::sleep_for(chrono::seconds(2));
		cout << "do something " << i << endl;
	}
	return 0;
}

代码清单1.2里,子线程对象很快就被释放了,但是子线程还在执行,所以就会报错。
为了解决这个问题,我们可以使用join让主线程等待子线程,但有一点不好,主线程会阻塞在join这里,我们应该让主线程去干自己的事。
第二种方法就是,不让子线程依赖该子线程对象 -- detach
使用该方法后,线程就不在依赖线程对象,哪怕线程对象被销毁,该线程依旧会执行。如代码清单 1.2.1 在代码块结束后 t1线程对象被自动回收,但该线程并没有结束,会一直运行,直到主线程结束。

代码清单 1.2.1
#include <thread>
#include <iostream>
using namespace std;

void test1()
{
    // 至少会执行 100s
	cout << "子线程开始:" << this_thread::get_id() << endl;
	for (int i = 0; i < 100; i++)
	{
		this_thread::sleep_for(chrono::seconds(1));
		cout << " i=" << i << endl;
	}
}

int main()
{
	// 一个代码块,程序出这个代码块后,t1会被释放
	{
		thread t1{ test1 };
		t1.detach();
	}

	for (int i = 0; i < 10; i++)
	{
		this_thread::sleep_for(chrono::seconds(2));
		cout << "do something " << i << endl;
	}
	return 0;
}

thread类源码解读

以下代码清单是一个简化并写有注释的thread类。

class thread 
{ // class for observing and managing threads
public:
    class id; // 线程ID

    using native_handle_type = void*; // 回调函数句柄

	// 无参构造,空的线程对象
    thread() noexcept : _Thr{} {} 

private:
	// 创建并执行线程
    template <class _Fn, class... _Args>
    void _Start(_Fn&& _Fx, _Args&&... _Ax) {
        using _Tuple                 = tuple<decay_t<_Fn>, decay_t<_Args>...>;
        auto _Decay_copied           = _STD make_unique<_Tuple>(_STD forward<_Fn>(_Fx), _STD forward<_Args>(_Ax)...);
        constexpr auto _Invoker_proc = _Get_invoke<_Tuple>(make_index_sequence<1 + sizeof...(_Args)>{});

        _Thr._Hnd =
            reinterpret_cast<void*>(_CSTD _beginthreadex(nullptr, 0, _Invoker_proc, _Decay_copied.get(), 0, &_Thr._Id));

        if (_Thr._Hnd) { // ownership transferred to the thread
            (void) _Decay_copied.release();
        } else { // failed to start thread
            _Thr._Id = 0;
            _Throw_Cpp_error(_RESOURCE_UNAVAILABLE_TRY_AGAIN);
        }
    }

public:
	// 使用函数指针作为参数的构造函数
    template <class _Fn, class... _Args, enable_if_t<!is_same_v<_Remove_cvref_t<_Fn>, thread>, int> = 0>
    _NODISCARD_CTOR_THREAD explicit thread(_Fn&& _Fx, _Args&&... _Ax) {
        _Start(_STD forward<_Fn>(_Fx), _STD forward<_Args>(_Ax)...); // 调用start 创建并执行线程
    }

    ~thread() noexcept {
        if (joinable()) {
            _STD terminate();
        }
    }

	// 移动构造
    thread(thread&& _Other) noexcept : _Thr(_STD exchange(_Other._Thr, {})) {}
	// 移动赋值
    thread& operator=(thread&& _Other) noexcept {
        if (joinable()) {
            _STD terminate();
        }

        _Thr = _STD exchange(_Other._Thr, {});
        return *this;
    }

	// 不允许拷贝构造 和 拷贝赋值
    thread(const thread&)            = delete;
    thread& operator=(const thread&) = delete;

	// 交换两个线程对象
    void swap(thread& _Other) noexcept {
        _STD swap(_Thr, _Other._Thr);
    }

	// 判断线程是否有效, 是否可以执行
    _NODISCARD bool joinable() const noexcept {
        return _Thr._Id != 0;
    }

	// 阻塞调用线程的,在一个线程环境调用,那么这个线程环境将会等待join()返回后才继续往下执行
    void join() {
        if (!joinable()) {
            _Throw_Cpp_error(_INVALID_ARGUMENT);
        }

        if (_Thr._Id == _Thrd_id()) {
            _Throw_Cpp_error(_RESOURCE_DEADLOCK_WOULD_OCCUR);
        }

        if (_Thrd_join(_Thr, nullptr) != _Thrd_success) {
            _Throw_Cpp_error(_NO_SUCH_PROCESS);
        }

        _Thr = {};
    }

	// 分离对象和线程的依赖
    void detach() {
        if (!joinable()) {
            _Throw_Cpp_error(_INVALID_ARGUMENT);
        }

        _Check_C_return(_Thrd_detach(_Thr));
        _Thr = {};
    }

	// 获取线程的ID
    _NODISCARD id get_id() const noexcept;
    // 获取线程任务句柄
    _NODISCARD native_handle_type native_handle() noexcept /* strengthened */ { // return Win32 HANDLE as void *
        return _Thr._Hnd;
    }
private:
    _Thrd_t _Thr; // 线程对象维护的成员变量
};
思考:
  1. 为什么创建线程对象的时候,传入函数指针,线程就自动执行

答:该构造函数的原形是:

// 使用函数指针作为参数
explicit thread(_Fn&& _Fx, _Args&&... _Ax) {
_Start(_STD forward<_Fn>(_Fx), _STD forward<_Args>(_Ax)...);
}
会自动调用start函数,start函数是私有成员函数,start函数会创建线程并执行,_Start函数里的主要代码:

_Thr._Hnd = reinterpret_cast<void*>(_CSTD _beginthreadex(nullptr, 0, _Invoker_proc, _Decay_copied.get(), 0, &_Thr._Id));