C++并发编程指南03 参数传递


2.2 传递参数:给线程"员工"分派工作材料

想象一下,当CEO(主线程)招聘新员工(启动线程)时,需要给员工提供工作所需的材料(参数)。在C++线程中,参数传递有自己独特的规则:

基本规则:参数会被复制
#include <iostream>
#include <thread>
#include <string>

// 员工任务:处理ID和名称
void worker_task(int id, const std::string& name) {
    std::cout << "Worker " << id << " (" << name << ") is processing data.\n";
    // ... 实际工作 ...
}

int main() {
    int worker_id = 42;
    std::string worker_name = "Alice";
    
    // 创建线程并传递参数
    std::thread worker(worker_task, worker_id, worker_name);
    
    worker.join();
    return 0;
}

关键点:​

  • 即使worker_task的第二个参数是引用(const std::string&),线程构造函数仍然会创建参数的副本

  • 新线程得到的是参数的拷贝,不是原始变量

  • 这通常是安全的,因为每个线程有自己的数据副本

⚠️ 危险情况:传递指向局部变量的指针
void risky_worker_task(int id, const std::string& name) {
    // 同上
}

void risky_boss_function() {
    int local_id = 99;
    char buffer[1024]; // 局部变量
    sprintf(buffer, "Worker %d", local_id);
    
    // 危险!传递指向局部变量的指针
    std::thread worker(risky_worker_task, local_id, buffer);
    worker.detach();
} // buffer在这里被销毁!但线程可能还在使用它

问题分析:​

  • buffer是局部数组,函数结束时会销毁

  • 线程构造函数接收的是char*指针

  • 在线程内部,这个指针会被转换为std::string

  • 如果主线程先结束,转换时指针已失效 → 未定义行为(通常是崩溃)

安全解决方案:提前转换

void safe_boss_function() {
    int local_id = 99;
    char buffer[1024];
    sprintf(buffer, "Worker %d", local_id);
    
    // 安全做法:在主线程完成转换
    std::thread worker(risky_worker_task, local_id, std::string(buffer));
    worker.detach();
}
传递引用:允许员工修改原始文件

有时我们需要员工直接修改CEO办公室的文件(原始数据),而不是使用副本:

// 员工任务:修改共享数据
void update_shared_data(int id, std::vector<int>& data) {
    std::cout << "Worker " << id << " is updating shared data.\n";
    data.push_back(id); // 直接修改原始数据
}

int main() {
    std::vector<int> shared_data = {1, 2, 3};
    int worker_id = 4;
    
    // 错误尝试:期望修改shared_data,但实际上传递的是副本
    // std::thread worker(update_shared_data, worker_id, shared_data);
    
    // 正确做法:使用std::ref明确传递引用
    std::thread worker(update_shared_data, worker_id, std::ref(shared_data));
    worker.join();
    
    // 检查数据是否被修改
    std::cout << "Updated data: ";
    for (int num : shared_data) {
        std::cout << num << " "; // 应该输出 1 2 3 4
    }
    std::cout << "\n";
    
    return 0;
}

关键点:​

  • 默认情况下参数会被复制,即使函数期望引用

  • 使用std::ref()包装参数来传递真正的引用

  • 注意:​​ 多线程修改共享数据需要同步机制(如互斥锁),否则会有数据竞争问题

传递成员函数:指定特定员工执行任务
class Employee {
public:
    void perform_task(int task_id) {
        std::cout << "Employee " << id << " performing task " << task_id << "\n";
        // ... 执行任务 ...
    }
    int id;
};

int main() {
    Employee bob;
    bob.id = 101;
    int task_num = 5;
    
    // 创建线程执行bob的成员函数
    std::thread worker(&Employee::perform_task, &bob, task_num);
    worker.join();
    
    return 0;
}

语法说明:​

  1. 成员函数指针:&Employee::perform_task

  2. 对象指针:&bob(谁执行这个任务)

  3. 成员函数参数:task_num

移动语义:转移独一无二的材料所有权

有些材料是独一无二的(如公司公章),只能由一个员工持有:

#include <memory>

// 处理大数据任务
void process_big_data(std::unique_ptr<std::string> data) {
    std::cout << "Processing data: " << *data << "\n";
    // ... 处理数据 ...
    // data会在函数结束时自动销毁
}

int main() {
    // 创建独占指针(唯一所有权)
    auto company_secret = std::make_unique<std::string>("Top Secret Plans");
    
    // 错误!unique_ptr不能复制
    // std::thread worker(process_big_data, company_secret);
    
    // 正确:使用std::move转移所有权
    std::thread worker(process_big_data, std::move(company_secret));
    worker.join();
    
    // 现在company_secret为空(所有权已转移)
    if (!company_secret) {
        std::cout << "Main: Secret data has been moved to worker.\n";
    }
    
    return 0;
}

关键点:​

  • std::unique_ptr代表独占所有权,不能复制

  • 使用std::move()转移所有权到新线程

  • 转移后,原始指针变为空

  • 新线程负责管理数据的生命周期

线程对象本身的移动

线程的所有权本身也可以转移:

void task1() { /* ... */ }
void task2() { /* ... */ }

int main() {
    // 启动线程执行task1
    std::thread worker1(task1);
    
    // 将worker1的所有权转移给worker2
    std::thread worker2 = std::move(worker1);
    
    // worker1现在为空(不关联任何线程)
    if (!worker1.joinable()) {
        std::cout << "worker1 is no longer associated with a thread\n";
    }
    
    // worker2现在管理原来的线程
    worker2.join();
    
    // 可以直接移动临时对象
    std::thread worker3 = std::thread(task2);
    worker3.join();
    
    return 0;
}

所有权规则:​

  • std::thread不可复制(不能有两个对象管理同一个线程)

  • 但可以移动(转移线程的所有权)

  • 移动后,原对象不再关联任何线程

  • 最后负责管理线程的对象必须调用join()detach()


完整示例:多线程数据处理

#include <iostream>
#include <thread>
#include <vector>
#include <memory>
#include <functional>

// 数据处理类
class DataProcessor {
public:
    void process_chunk(int chunk_id, std::vector<int>& data) {
        std::cout << "Processing chunk " << chunk_id << "\n";
        for (int& num : data) {
            num *= 2; // 修改数据
        }
    }
};

int main() {
    // 创建共享数据
    std::vector<int> shared_data = {1, 2, 3, 4, 5};
    
    // 创建处理器对象
    DataProcessor processor;
    
    // 创建独占数据
    auto unique_data = std::make_unique<std::string>("Confidential");
    
    // 使用lambda捕获局部变量
    int base_value = 10;
    auto lambda_task = [base_value](const std::string& message) {
        std::cout << message << " with base value " << base_value << "\n";
    };
    
    // 启动多个线程
    std::thread t1([&shared_data] {
        std::cout << "Thread 1: Data size = " << shared_data.size() << "\n";
    });
    
    std::thread t2(&DataProcessor::process_chunk, &processor, 1, std::ref(shared_data));
    
    std::thread t3(lambda_task, "Hello from Lambda");
    
    std::thread t4([](std::unique_ptr<std::string> data) {
        std::cout << "Thread 4 received: " << *data << "\n";
    }, std::move(unique_data));
    
    // 等待所有线程完成
    t1.join();
    t2.join();
    t3.join();
    t4.join();
    
    // 显示修改后的数据
    std::cout << "Modified data: ";
    for (int num : shared_data) {
        std::cout << num << " "; // 应该输出 2 4 6 8 10
    }
    std::cout << "\n";
    
    return 0;
}

关键总结:​

  1. 默认行为​:参数被复制到线程的独立内存空间

  2. 引用传递​:使用std::ref()明确传递引用

  3. 指针危险​:避免传递指向局部变量的指针/引用

  4. 成员函数​:传递对象指针+成员函数指针+参数

  5. 移动语义​:对unique_ptr等不可复制类型使用std::move()

  6. 线程所有权​:std::thread对象本身可移动不可复制

  7. Lambda表达式​:简洁安全地传递参数和捕获局部变量

通过理解这些规则,你可以安全有效地给线程"员工"分派工作所需的"材料",确保多线程程序稳定运行。

完整但是枯燥的文章

2.2 传递参数

如代码2.4所示,向可调用对象或函数传递参数很简单,只需要将这些参数作为 std::thread构造函数的附加参数即可。需要注意的是,这些参数会拷贝至新线程的内存空间中(同临时变量一样)。即使函数中的参数是引用的形式,拷贝操作也会执行。来看一个例子:

void f(int i, std::string const& s);
std::thread t(f, 3, "hello");

代码创建了一个调用f(3, “hello”)的线程。注意,函数f需要一个std::string对象作为第二个参数,但这里使用的是字符串的字面值,也就是char const *类型,线程的上下文完成字面值向std::string的转化。需要特别注意,指向动态变量的指针作为参数的情况,代码如下:

void f(int i,std::string const& s);
void oops(int some_param)
{
  char buffer[1024]; // 1
  sprintf(buffer, "%i",some_param);
  std::thread t(f,3,buffer); // 2
  t.detach();
}

buffer①是一个指针变量,指向局部变量,然后此局部变量通过buffer传递到新线程中②。此时,函数oops可能会在buffer转换成std::string之前结束,从而导致未定义的行为。因为,无法保证隐式转换的操作和std::thread构造函数的拷贝操作的顺序,有可能std::thread的构造函数拷贝的是转换前的变量(buffer指针)。解决方案就是在传递到std::thread构造函数之前,就将字面值转化为std::string

void f(int i,std::string const& s);
void not_oops(int some_param)
{
  char buffer[1024];
  sprintf(buffer,"%i",some_param);
  std::thread t(f,3,std::string(buffer));  // 使用std::string,避免悬空指针
  t.detach();
}

相反的情形(期望传递一个非常量引用,但复制了整个对象)倒是不会出现,因为会出现编译错误。比如,尝试使用线程更新引用传递的数据结构:

void update_data_for_widget(widget_id w,widget_data& data); // 1
void oops_again(widget_id w)
{
  widget_data data;
  std::thread t(update_data_for_widget,w,data); // 2
  display_status();
  t.join();
  process_widget_data(data);
}

虽然update_data_for_widget①的第二个参数期待传入一个引用,但std::thread的构造函数②并不知晓,构造函数无视函数参数类型,盲目地拷贝已提供的变量。不过,内部代码会将拷贝的参数以右值的方式进行传递,这是为了那些只支持移动的类型,而后会尝试以右值为实参调用update_data_for_widget。但因为函数期望的是一个非常量引用作为参数(而非右值),所以会在编译时出错。对于熟悉std::bind的开发者来说,问题的解决办法很简单:可以使用std::ref将参数转换成引用的形式。因此可将线程的调用改为以下形式:

std::thread t(update_data_for_widget,w,std::ref(data));

这样update_data_for_widget就会收到data的引用,而非data的拷贝副本,这样代码就能顺利的通过编译了。

如果熟悉std::bind,就应该不会对以上述传参的语法感到陌生,因为std::thread构造函数和std::bind的操作在标准库中以相同的机制进行定义。比如,你也可以传递一个成员函数指针作为线程函数,并提供一个合适的对象指针作为第一个参数:

class X
{
public:
  void do_lengthy_work();
};
X my_x;
std::thread t(&X::do_lengthy_work, &my_x); // 1

这段代码中,新线程将会调用my_x.do_lengthy_work(),其中my_x的地址①作为对象指针提供给函数。也可以为成员函数提供参数:std::thread构造函数的第三个参数就是成员函数的第一个参数,以此类推(代码如下,译者自加)。

class X
{
public:
  void do_lengthy_work(int);
};
X my_x;
int num(0);
std::thread t(&X::do_lengthy_work, &my_x, num);

另一种有趣的情形是,提供的参数仅支持移动(move),不能拷贝。“移动”是指原始对象中的数据所有权转移给另一对象,从而这些数据就不再在原始对象中保存(译者:比较像在文本编辑的剪切操作)。std::unique_ptr就是这样一种类型(译者:C++11中的智能指针),这种类型为动态分配的对象提供内存自动管理机制(译者:类似垃圾回收机制)。同一时间内,只允许一个std::unique_ptr实例指向一个对象,并且当这个实例销毁时,指向的对象也将被删除。移动构造函数(move constructor)和移动赋值操作符(move assignment operator)允许一个对象的所有权在多个std::unique_ptr实例中传递(有关“移动”的更多内容,请参考附录A的A.1.1节)。使用“移动”转移对象所有权后,就会留下一个空指针。使用移动操作可以将对象转换成函数可接受的实参类型,或满足函数返回值类型要求。当原对象是临时变量时,则自动进行移动操作,但当原对象是一个命名变量,转移的时候就需要使用std::move()进行显示移动。下面的代码展示了std::move的用法,展示了std::move是如何转移动态对象的所有权到线程中去的:

void process_big_object(std::unique_ptr<big_object>);

std::unique_ptr<big_object> p(new big_object);
p->prepare_data(42);
std::thread t(process_big_object,std::move(p));

通过在std::thread构造函数中执行std::move(p),big_object 对象的所有权首先被转移到新创建线程的的内部存储中,之后再传递给process_big_object函数。

C++标准线程库中和std::unique_ptr在所属权上相似的类有好几种,std::thread为其中之一。虽然,std::thread不像std::unique_ptr能占有动态对象的所有权,但是它能占有其他资源:每个实例都负责管理一个线程。线程的所有权可以在多个std::thread实例中转移,这依赖于std::thread实例的可移动不可复制性。不可复制性表示在某一时间点,一个std::thread实例只能关联一个执行线程。可移动性使得开发者可以自己决定,哪个实例拥有线程实际执行的所有权。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

丁金金_chihiro_修行

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值