创建线程

核心概念
  1. std::thread:C++11 引入的线程类,用于创建和管理线程,构造时传入可调用对象(函数、仿函数、Lambda、类成员函数)及参数,调用 join() 等待线程结束,调用 detach() 让线程后台运行。
  2. 可调用对象:能被 () 调用的实体,包括函数指针、仿函数(重载 operator() 的类对象)、Lambda 表达式、类成员函数指针。
  3. 最令人头疼的解析:C++ 语法歧义问题,当用 std::thread t1(伪函数) 声明时,编译器会解析为函数声明(返回 std::thread,参数是返回伪函数的无参函数指针),而非线程对象。
  4. Move 语义:针对独占资源(如 std::unique_ptr),通过 std::move 转移资源所有权,因为独占资源不可拷贝,无法直接传递给线程。
  5. 类成员函数线程:类成员函数有隐式的 this 指针,因此创建线程时必须传递对象指针(如 &my_x),否则无法调用成员函数
void thread_work1(std::string s){
   cout<<"thread_work1: "<<s<<endl;
 }

 class work1{
   public:
     void operator()(){
       cout<<"work1"<<endl;
     }
 };

 int main() {
   std::string s ="hello world";
   
   //直接调用函数:函数作为可调用对象
   std::thread t(thread_work1,s);
   t.join();


   // 用仿函数:触发“最令人头疼的解析”
	std::thread t1(work1());
	t1.join();
   // 编译器解析为函数声明:thread(*)(work1(*)())
   // 解决办法:加括号/用列表初始化,强制编译器解析为对象
	std::thread t1((work1()));
	std::thread t1{work1(),s};


   //Lambda:轻量级可调用对象,适合简短线程逻辑
   std::thread t2([](std::string l){std::cout << l << std::endl;},s);
   t2.join();
 }

// 绑定类成员函数:需传递对象指针(this指针)
 class X {
 public:
     void do_work() {
         std::cout << "X::do_work" << std::endl;
     }
 };

 void bind_class_oops() {
     X my_x;
     std::thread t1(&X::do_work, &my_x);
     t1.join();
 }
 // 普通函数传参时,&可省略(函数名隐式转为函数指针)


 // 使用move操作:转移unique_ptr所有权(不可拷贝)
 void deal_unique(std::unique_ptr<int> p) {
     std::cout << "unique ptr data is " << *p << endl;
     (*p)++;

std::cout << "after unique ptr data is " << *p << endl;
 }

 void move_oops() {
     auto p = std::make_unique<int>(1);
     std::thread t(deal_unique, std::move(p));
     t.join();
     // p已失去所有权,不可再使用
 }

线程传参

核心概念
  1. join() vs detach()
    • join():主线程阻塞,等待子线程执行完毕后再继续,子线程和主线程生命周期绑定
    • detach():子线程与主线程分离,后台运行,由系统接管资源,主线程退出后子线程可能仍在运行(分离线程)。
  2. 局部变量生命周期风险:分离线程若访问主线程的局部变量,主线程退出后局部变量被销毁,子线程会访问野指针,导致未定义行为。
  3. RAII(资源获取即初始化):一种 C++ 编程范式,利用对象的生命周期管理资源(如线程的 join())。thread_guard 类在析构时自动调用 join(),避免手动管理的遗漏和异常风险。
  4. 隐式类型转换陷阱:线程构造函数会拷贝参数,但隐式转换(如 char[]std::string)会在线程执行时才进行,若此时原变量已销毁,转换会失败。
  5. std::ref 引用包装器std::thread 构造函数默认按值传递参数,若要传递引用,需用 std::ref 包装,否则会传递参数的拷贝,无法修改原变量。
struct work {
   int& _i;
   work(int& i) : _i(i) {}
   void operator()() {
     for (int i = 0; i < 3; ++i) {
       std::cout << _i << std::endl;
       std::this_thread::sleep_for(std::chrono::seconds(1));
     }
   }
 };

 void oops() {
   int locate_state = 0;
   work w(locate_state);
   std::thread t(w);
   // 隐患:detach后子线程访问局部变量locate_state,主线程退出后变量销毁
   t.detach();
 }

 int main() {
   oops();
   std::this_thread::sleep_for(std::chrono::seconds(10));
 }


 //解决局部变量问题的措施
 //1.智能指针:传递动态分配的资源,延长生命周期
 //2.值传参:拷贝局部变量,避免野指针
 //3.使用join():绑定主线程和子线程生命周期


 //异常处理:手动join的臃肿写法
 void catch_exception() {
   int local_state = 0;
   work mywork(local_state);
   std::thread t(mywork);
   try {
     std::this_thread::sleep_for(std::chrono::seconds(1));
   }catch(exception& e) {
     t.join(); // 异常时也要join,否则程序崩溃
     throw;
   }
   t.join();
 }

 //RAII封装:自动管理线程join
 class thread_guard {
 private:
   std::thread& t;
 public:
   explicit thread_guard(std::thread& t) : t(t) {}
   ~thread_guard() {
     if (t.joinable()) { // joinable()判断线程是否可汇合(未调用过join/detach)
       t.join();// 析构时自动join,无需手动调用
     }
   }

   thread_guard(const thread_guard&) = delete; // 禁止拷贝,避免双重join
   thread_guard& operator=(const thread_guard&) = delete;
 };

 //使用RAII:无需手动处理join和异常
 void auto_guard() {
   int local_state = 0;
   work my_work(local_state);
   std::thread t(my_work);
   thread_guard g(t); // 出作用域时g析构,自动join
 }


 //慎用隐式转换:char[]转std::string的时机问题
 void danger_oops(int param) {
     char buffer[1024];
     sprintf(buffer, "%d", param);
     // 风险:buffer是局部数组,线程执行时可能已销毁,转换失败
     std::thread t1(print_str,3, buffer);
     // 改进:提前显式转换为std::string,拷贝到线程参数中
     std::thread t2(print_str,4, (std::string)buffer);
 }


 //引用传参:必须用std::ref包装
 void change_param(int& param) {
     param++;
 }

 void ref_oops(int some_param) {
     std::cout << "before ref_oops" << some_param<<std::endl;
     // 编译失败:some_param被拷贝为右值,无法绑定到左值引用
     std::thread t2(change_param, some_param);
     t2.join();
     std::cout << "after ref_oops" << some_param<<std::endl;
 }
 // 修正:用std::ref包装,传递真实引用
 void ref_oops(int some_param) {
     std::cout << "before ref_oops" << some_param << std::endl;
     std::thread t2(change_param, std::ref(some_param));
     t2.join();
     std::cout << "after ref_oops" << some_param << std::endl;
 }

thread 原理

核心概念
  1. 完美转发(Perfect Forwarding):通过 std::forward 实现,保持参数的原始类型(左值 / 右值),避免不必要的拷贝,是实现通用模板的关键。
  2. std::tuple 参数封装std::thread 会将可调用对象和参数打包成 tuple 存储,线程启动时再解包调用,统一管理不同数量和类型的参数。
  3. 底层线程 API:Windows 平台下 std::thread 底层调用 _beginthreadex 创建线程,Linux 平台下调用 pthread_create,是 C++ 线程库对系统 API 的封装。
  4. std::invoke:C++17 引入的函数模板,用于统一调用各种可调用对象(函数、仿函数、Lambda、成员函数),自动处理成员函数的 this 指针。
  5. 右值传递:线程参数以右值方式存入 tuple,确保资源可以被移动(而非拷贝),提升效率,同时支持独占资源的传递。
template <class _Fn, class... _Args, std::enable_if_t<!is_same_v<_Remove_cvref_t<_Fn>, thread>, int> = 0>
 _NODISCARD_CTOR explicit thread(_Fn&& _Fx, _Args&&... _Ax) {
     _Start(_STD forward<_Fn>(_Fx), _STD forward<_Args>(_Ax)...);// 完美转发:保持参数左/右值属性
 }

 //_start函数:启动底层线程,调用系统API
 template <class _Fn, class... _Args>
 void _Start(_Fn&& _Fx, _Args&&... _Ax) {
     using _Tuple = tuple<decay_t<_Fn>, decay_t<_Args>...>; // 封装可调用对象和参数为tuple
     auto _Decay_copied = _STD make_unique<_Tuple>(_STD forward<_Fn>(_Fx), _STD forward<_Args>(_Ax)...);
     // 获取调用器:用于解包tuple并执行可调用对象
     constexpr auto _Invoker_proc = _Get_invoke<_Tuple>(make_index_sequence<1 + sizeof...(_Args)>{});

 #pragma warning(push)
 #pragma warning(disable : 5039)
     // 调用Windows底层线程API _beginthreadex
     _Thr._Hnd =
     reinterpret_cast<void*>(_CSTD _beginthreadex(nullptr, 0, _Invoker_proc, _Decay_copied.get(), 0, &_Thr._Id));
 #pragma warning(pop)

 if (_Thr._Hnd) { // 线程创建成功,转移tuple所有权给线程
         (void) _Decay_copied.release();
     } else { // 线程创建失败,抛出异常
         _Thr._Id = 0;
         _Throw_Cpp_error(_RESOURCE_UNAVAILABLE_TRY_AGAIN);
     }
 }

 // 核心调用逻辑:解包tuple并执行可调用对象
 template <class _Tuple, size_t... _Indices>
 static unsigned int __stdcall _Invoke(void *_RawVals) noexcept /* terminates */ {
     const unique_ptr<_Tuple> _FnVals(static_cast<_Tuple *>(_RawVals));
     _Tuple &_Tup = *_FnVals;
     _STD invoke(_STD move(_STD get<_Indices>(_Tup))...); // std::invoke统一调用可调用对象
     _Cnd_do_broadcast_at_thread_exit();
     return 0;
 }

 // std::invoke的重载:支持各种可调用对象
 CONSTEXPR17 auto invoke(_Callable&& _Obj, _Ty1&& _Arg1, _Types2&&... _Args2) noexcept(
 noexcept(_Invoker1<_Callable, _Ty1>::_Call(
 static_cast<_Callable&&>(_Obj), static_cast<_Ty1&&>(_Arg1), static_cast<_Types2&&>(_Args2)...)))
     
 // 结论:线程参数以右值方式存入tuple,支持移动语义

线程管理

1. 线程归属权

核心概念
  1. 线程对象的不可拷贝性std::thread 禁用了拷贝构造和拷贝赋值,因为线程是操作系统的资源,拷贝会导致资源管理混乱。
  2. 移动语义(Move Semantics)std::thread 支持移动构造和移动赋值,通过 std::move 转移线程的所有权,原线程对象会变为 “空状态”(不可汇合)。
  3. 空线程对象:默认构造的 std::thread 对象(如 std::thread t3;)不管理任何线程,可通过移动赋值接收其他线程的所有权。
  4. 赋值崩溃风险:若一个线程对象正在管理运行中的线程,直接用另一个线程对象赋值给它,会触发 std::terminate 终止程序,因为无法同时管理两个线程。
void some_function(){
    while(true){
      std::this_thread::sleep_for(std::chrono::seconds(1));
    }
}

void some_other_function(){
  while(true){
    std::this_thread::sleep_for(std::chrono::seconds(1));
  }
}

int main(){
  std::thread t1(some_function);
  std::thread t2 = std::move(t1); // 转移所有权:t1变空,t2管理some_function线程
  // t1可重新绑定新线程
  t1 = std::thread(some_other_function);

    
  std::thread t3; // 空线程对象,不管理任何线程
  t3 = std::move(t2); // t3接管t2的线程所有权
  // 错误:t1正在管理线程,赋值会触发terminate
  t1 = std::move(t3);
  std::this_thread::sleep_for(std::chrono::seconds(100));
}


// 函数返回局部thread对象:编译器自动调用移动构造
std::thread f() {
  return std::thread(some_function);
}
// C++返回局部对象时,优先用移动构造(若有),否则用拷贝构造(禁用则编译失败)

2. joining_thread

核心概念
  1. 自动汇合(Auto-join)joining_threadstd::jthread(C++20 标准)的前身,核心特性是析构时自动调用 join(),彻底解决 “忘记 join 导致程序崩溃” 的问题。
  2. RAII 线程管理:通过封装 std::thread,利用对象析构函数自动释放资源,是 C++ 资源管理的最佳实践。
  3. 所有权转移兼容joining_thread 支持移动语义,可通过 std::move 转移线程所有权,同时保证析构时的自动 join。
// 模拟C++20 std::jthread的joining_thread类:自动join的RAII封装
class joining_thread {
    std::thread _t; // 封装原生线程对象
public:
    joining_thread() noexcept = default; // 默认构造:空线程

template<typename Callable, typename ... Args>
    explicit joining_thread(Callable&& func, Args&& ...args):
    _t(std::forward<Callable>(func), std::forward<Args>(args)...) {} // 完美转发构造线程
    

explicit joining_thread(std::thread t) noexcept: _t(std::move(t)){} // 接收原生线程对象
    
joining_thread(joining_thread&& other) noexcept: _t(std::move(other._t)){} // 移动构造

joining_thread& operator=(joining_thread&& other) noexcept
    {
        // 赋值前先join当前线程,避免资源泄漏
        if (joinable()) {
            join();
        }
        _t = std::move(other._t); // 转移所有权
        return *this;
    }

    ~joining_thread() noexcept {
        if (joinable()) {
            join(); // 析构自动join,核心特性
        }
    }
    
    void swap(joining_thread& other) noexcept {
        _t.swap(other._t);
    }
    
    std::thread::id get_id() const noexcept {
        return _t.get_id();
    }
    
    bool joinable() const noexcept {
        return _t.joinable();
    }
    
    void join() {
        _t.join();
    }
    
    void detach() {
        _t.detach();
    }
    
    std::thread& as_thread() noexcept {
        return _t;
    }
    
    const std::thread& as_thread() const noexcept {
        return _t;
    }
};

// 使用joining_thread:无需手动join,出作用域自动汇合
void use_joining_thread() {
    joining_thread j1([](int maxindex) {
        std::cout << maxindex << std::endl;
    },3);

    // 用原生thread构造
    joining_thread j2(std::thread([](int maxindex) {},3));
    
    // 移动赋值:j1接管j2的线程,j2变空
    j1 = std::move(j2);
}

3. 容器存储

核心概念
  1. emplace_back 直接构造std::vector 存储 std::thread 时,不能用 push_back(需要拷贝),必须用 emplace_back 直接在容器中构造线程对象,避免拷贝操作。
  2. 硬件并发数std::thread::hardware_concurrency() 返回 CPU 核心数(逻辑核心),是确定最优并发线程数的关键依据,避免线程过多导致的调度开销。
  3. 分治算法(Divide and Conquer):并行累加的核心思想,将大任务拆分为多个小任务,多线程并行执行,最后合并结果,是并行计算的基础范式。
  4. 任务拆分粒度:拆分任务时需考虑 “最小任务量”(如代码中的 min_per_thread = 25),避免任务过小导致的线程创建开销超过并行收益。
// 容器存储线程:用emplace_back直接构造,避免拷贝
void use_vector() {
    std::vector<std::thread> threads;
    for (int i = 0; i < 10; i++) {
        // emplace_back:直接在容器内构造线程对象
        threads.emplace_back([&]() {std::cout << i << std::endl;});
    }
    for(auto& thread : threads) {
        thread.join(); // 等待所有线程完成
    }
}


// 硬件并发数:获取CPU逻辑核心数,确定最优线程数
// std::thread::hardware_concurrency()


// 并行累加:分治思想的实践,多线程并行计算
template<typename Iterator, typename T>
T parallel_accumulate(Iterator first, Iterator last, T init)
{
    unsigned long const length = std::distance(first, last);
    if (!length)
        return init;
    unsigned long const min_per_thread = 25; // 最小任务量:避免过度拆分
    unsigned long const max_threads =
        (length + min_per_thread - 1) / min_per_thread; // 最大线程数
    unsigned long const hardware_threads =
    std::thread::hardware_concurrency(); // CPU核心数
    unsigned long const num_threads =
        std::min(hardware_threads != 0 ? hardware_threads : 2, max_threads); // 最优线程数
    unsigned long const block_size = length / num_threads; // 每个线程处理的数据块大小
    std::vector<T> results(num_threads); // 存储每个线程的计算结果
    std::vector<std::thread> threads(num_threads - 1); // 主线程也处理一个块,减少线程数
    Iterator block_start = first;
    for (unsigned long i = 0; i < (num_threads - 1); ++i)
    {
        Iterator block_end = block_start;
        std::advance(block_end, block_size); // 移动到块末尾
        // 每个线程处理一个数据块,结果存入results
        threads[i] = std::thread(
            accumulate_block<Iterator, T>(),
            block_start, block_end, std::ref(results[i]));
        block_start = block_end; 
    }
    // 主线程处理最后一个块
    accumulate_block<Iterator, T>()(
        block_start, last, results[num_threads - 1]);

    for (auto& entry : threads)
        entry.join(); // 等待所有子线程完成
    return std::accumulate(results.begin(), results.end(), init); // 合并结果
}

void use_parallel_acc() {
    std::vector <int> vec(10000);
    for (int i = 0; i < 10000; i++) {
        vec.push_back(i);
    }
    int sum = 0;
    sum = parallel_accumulate<std::vector<int>::iterator, int>(vec.begin(),
    vec.end(), sum);

    std::cout << "sum is " << sum << std::endl;
}

4. 识别线程

核心概念
  1. 线程 ID:每个线程有唯一的 ID,类型为 std::thread::id,用于区分不同线程,是线程的唯一标识。
  2. 线程 ID 获取方式
    • 线程外部:通过 std::thread 对象的 get_id() 成员函数获取。
    • 线程内部:通过 std::this_thread::get_id() 获取当前线程的 ID(无需线程对象)。
  3. 线程 ID 的用途:日志记录、线程唯一性判断、调试定位问题等。
int main() {
    std::thread t([](){std::cout<<"Hello World!"<<std::endl;});
    t.get_id(); // 外部获取线程ID

    std::thread t2([]() {
        // 内部获取当前线程ID
        std::cout<<"in thread id: "
        <<std::this_thread::get_id()<<std::endl
        << "start";
    });
}

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐