在现代软件开发中,并发编程成为提升性能的关键。无论是处理大量数据、提升响应速度,还是高效利用多核 CPU,多线程编程都至关重要。在 C++ 中 通过使用 std::thread 类,我们能够轻松地创建并启动一个或多个线程,完成较为复杂的任务。
1. 基本使用
我们使用 std::thread 来创建并启动一个线程。
#if 1
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
#include<thread>
void do_work(int num1, int num2){}
struct MyTask
{
void task1(int num1, int num2) {}
static void task2(int num1, int num2) {}
};
struct Task
{
void operator()(int num1, int num2) {}
};
void test01()
{
// 普通函数
std::thread t1(do_work, 10, 20);
// lambda 匿名函数
std::thread t2([]() {});
// 成员函数
MyTask task;
std::thread t3(&MyTask::task1, task, 10, 20);
std::thread t4(&MyTask::task1, &task, 10, 20);
std::thread t5(&MyTask::task2, 10, 20);
// 函数对象
std::thread t6{ Task(), 10, 20};
}
void test02()
{
std::thread t([]() {});
// 阻塞当前线程,等到子线程执行完毕,继续向下执行
t.join();
// 子线程从当前线程分离,独立运行,调用线程不需要等待
// t.detach();
// 获得线程编号
std::cout << t.get_id() << std::endl;
// 系统最大并发线程数
std::cout << t.hardware_concurrency() << std::endl;
}
int main()
{
test02();
return EXIT_SUCCESS;
}
#endif
2. 参数传递
我们经常看到在构建 std::thread 对象时,使用 std::ref 来传递参数。有些同学可能不清楚,此处为什么这么写?以及这么写有什么好处?接下来,从 std::thread 源码的角度来分析下这个问题。具体会讲解到的问题如下:
- 了解 std::thread 内部如何处理传递进来的参数
- 了解 std::thread 不同的传递参数的方式的区别
- 了解 std::thread 可调用对象的参数类型如何写
#include<thread>
struct Data
{
Data() = default;
Data(const Data&) = default;
Data(Data&&) = default;
};
void do_work(const Data& data){}
void test()
{
Data data;
std::thread t(do_work, std::ref(data));
t.join();
}
我们首先从 std::thread 的构造函数开始,直到线程运行起来,看一看参数在这个过程中都发生了什么事情。
class thread {
// 调用线程函数,并将 std::tuple 中的参数 move 到线程函数
template <class _Tuple, size_t... _Indices>
static unsigned int __stdcall _Invoke(void* _RawVals) noexcept
{
const unique_ptr<_Tuple> _FnVals(static_cast<_Tuple*>(_RawVals));
_Tuple& _Tup = *_FnVals.get();
_STD invoke(_STD move(_STD get<_Indices>(_Tup))...);
_Cnd_do_broadcast_at_thread_exit();
return 0;
}
template <class _Tuple, size_t... _Indices>
static constexpr auto _Get_invoke(index_sequence<_Indices...>) noexcept
{
return &_Invoke<_Tuple, _Indices...>;
}
// 参数以值方式保存参数到 std::tuple 中
// 如果参数为左值,则拷贝到元组中
// 如果参数为右值,则移动到元组中
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)
{
(void) _Decay_copied.release();
}
else
{
_Thr._Id = 0;
_Throw_Cpp_error(_RESOURCE_UNAVAILABLE_TRY_AGAIN);
}
}
// thread 变长参数构造函数
template <class _Fn, class... _Args>
explicit thread(_Fn&& _Fx, _Args&&... _Ax)
{
_Start(_STD forward<_Fn>(_Fx), _STD forward<_Args>(_Ax)...);
}
}
通过对 std::thread 源码分析可知:
std::thread使用std::tuple存储参数,如果传递是左值则进行拷贝,如果传递右值则进行移动- 线程启动时,会将
std::tuple中存储的参数std::move到线程对象关联的可调用对象中
#if 1
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
#include<thread>
struct Data
{
Data() = default;
Data(const Data&) { std::cout << "拷贝构造" << std::endl; }
// Data(const Data&) = delete;
// Data(Data&&) { std::cout << "移动构造" << std::endl; }
Data(Data&&) = delete;
int value{ 0 };
};
void do_work1(Data data)
{
data.value = 200;
}
void do_work2(Data&& data)
{
data.value = 200;
}
// 1. 左值方式传递
void test01()
{
Data data;
#if 0
// 拷贝构造函数 -> 移动构造函数
auto do_work = do_work1;
#else
// 拷贝构造函数
auto do_work = do_work2;
#endif
std::thread t(do_work, data);
t.join();
}
// 2. 右值方式传递
void test02()
{
Data data;
#if 0
// 移动构造函数 -> 移动构造函数
auto do_work = do_work1;
#else
// 如果没有移动构造,则调用拷贝构造
auto do_work = do_work2;
#endif
std::thread t(do_work, std::move(data));
t.join();
}
int main()
{
test02();
return EXIT_SUCCESS;
}
#endif
我们发现,上面两种方式在整个参数传递过程中,会存在以下问题:
- 无论以左值、还是右值传递,都不具备在 do_work 中修改 data 对象的能力
- 无论以左值、还是右值传递,都要求参数对象能够被拷贝或移动
- 如果拷贝则降低传递效率,如果移动则有可能导致外部 data 失效
为了进一步提升参数对象传递的效率,我们可以使用 std::ref 引用包装器来进行参数对象传递。这样在参数对象不进行任何拷贝和移动的情况下,实现高效的传递。那么,这个过程是什么样的呢?
- 首先,使用 std::ref 创建一个持有 data 对象指针的 reference_wrapper 匿名对象
- 然后,将 reference_wrapper 拷贝到 tuple 中存储(对于 data 而言,只是拷贝一个对象指针)
- 最后,从 tuple 中取出 reference_wrapper 对象,将其转换为 Data& 引用,传递到可调用对象中
// 注意:参数类型写左值引用,避免拷贝
void do_work(Data& data)
{
data.value = 200;
}
void test()
{
Data data;
// 1. 首先,使用 std::ref 创建一个持有 data 对象指针的 reference_wrapper 匿名对象
// 2. 然后,将 reference_wrapper 存储到 tuple 时进行拷贝
std::tuple<std::reference_wrapper<Data>> my_tuple = { std::ref(data) };
// 3. 最后,从 tuple 中取出 reference_wrapper 对象,将其转换为 Data& 引用,传递到可调用对象中
// 注意:如果 do_work 参数类型为在值类型,将会发生拷贝
do_work(std::move(std::get<0>(my_tuple)));
std::cout << data.value << std::endl;
}
最后,我们来总结一下:
- 当传递对象、或者右值对象时,std::thread 内部会对对象进行拷贝和移动操作,线程可调用对象的参数设置为右值引用能够减少拷贝构造、移动构造调用次数。
- std::ref 可以在不进行任何对象拷贝和移动的情况下传递参数,线程可调用对象的参数设置为左值引用,性能最好。并且,线程可调用对象具备修改外部对象的能力。



冀公网安备13050302001966号