C++并发:线程函数传参(一)

devtools/2024/10/24 12:31:13/

一、问题

当创建 std::thread 对象时,传递给线程的函数的所有参数都会被复制或移动到新创建的线程的内存空间中。这是为了确保线程的执行不会依赖于父线程可能销毁的栈上变量,从这个机制上看,这是很合理的。

在新的线程的栈上,这些变量都会以右值的方式传递给线程函数,这主要是为了提高传参的性能。但是在某些情况下,这些右值并不能满足线程函数的参数。如果线程函数需要引用参数,直接传递普通变量会因为无法从临时对象(复制或移动产生的)绑定到非 const 引用而失败。比如以下的例子:

#include <iostream>
#include <thread>class BigObject {std::string s = "hello";public:const std::string &getData() const { return s; }void upDateData(const std::string &str) { s = str; }void showInfo() const {std::cout << "addr: " << this << " value: " << s << std::endl;}~BigObject(){};BigObject(){};
};
void update_data_for_BigOb(std::string newString, BigObject &data);
void printInfo(BigObject &ob);void oops_again(std::string w) {BigObject data;printInfo(data);std::thread t(update_data_for_BigOb, w, data);t.join();
}int main() {oops_again("hello_new!"); // 函数调用return 0;
}void update_data_for_BigOb(std::string newString, BigObject &data) {// 修改data.upDateData(newString);printInfo(data);
}
void printInfo(BigObject &ob) { ob.showInfo(); }

这样会编译失败:

g++ parameter1.cxx -o main -std=c++11
In file included from parameter1.cxx:2:
/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/c++/v1/thread:280:5: error: attempt to use a deleted function__invoke(_VSTD::move(_VSTD::get<1>(__t)), _VSTD::move(_VSTD::get<_Indices>(__t))...);^
/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/c++/v1/thread:291:5: note: in instantiation of function template specialization 'std::__1::__thread_execute<std::__1::unique_ptr<std::__1::__thread_struct>, void (*)(std::__1::basic_string<char>, BigObject &), int, BigObject, 2, 3>' requested here__thread_execute(*__p, _Index());
...

遇到的错误与尝试在 std::thread 构造器中使用不匹配的参数类型有关。错误的根本原因是在创建 std::thread 实例时传递的参数类型与线程函数所期望的参数类型不兼容。因此我们要考虑,传递的参数如何才能正确兼容线程函数期望的参数类型。这正是上面讨论到的一种情况,接着讨论如何优雅的解决这种问题。

二、自动类型转换

在C++中,可以通过构造函数类型转换操作符来实现类对象之间的隐式类型转换:

#include <iostream>class Number {
private:int value;public:// 构造函数Number(int val) : value(val) {}// 类型转换操作符operator int() const {return value;}
};int main() {Number num1 = 5;int result = num1 + 10;  // 隐式类型转换发生在这里std::cout << result << std::endl;  // 输出 15return 0;
}

上文示例中,可以看到 num1 发生了隐式转换(我们暂不考虑强制显式转换),Number 对象转换成了 int 基本类型。这是因为我们定义了类型转换操作符。当我们在 main 函数中执行 num1 + 10 时,由于 10 是一个整数,C++ 编译器将自动调用 operator int() 来将 num1 隐式转换为 int 类型,然后执行加法操作。

三、包装器

以下是 std::ref 的简化版本源码示例:

namespace std {// 定义 ref 类模板template<class T>class reference_wrapper {public:// 构造函数,接受一个对象的引用reference_wrapper(T& ref) : _ref(ref) {}// 拷贝构造函数和赋值运算符被删除,禁止拷贝和赋值reference_wrapper(const reference_wrapper&) = delete;reference_wrapper& operator=(const reference_wrapper&) = delete;// 重载解引用运算符,返回引用对象// 也可以隐式转换为引用对象operator T&() const { return _ref; }private:T& _ref; // 存储引用对象的引用};// ref 函数模板,接受一个对象,并返回一个 reference_wrapper 包装后的对象template<class T>reference_wrapper<T> ref(T& t) {return reference_wrapper<T>(t);}
}

在使用 std::ref 的时候,实际上是将传递的对象包装成了一个 reference_wrapper<T>(t) 对象,如果我们将 问题 中的代码这样改:

...
void oops_again(int w) {
...std::thread t(update_data_for_BigOb, w, std::ref(data));}
...

这将会发生什么?
这个包装器对象(很轻量)将会被移动到新线程的栈中(类中禁止了复制构造函数),然后这个包装器对象被以右值的方式绑定到线程函数的参数。别忘了,这个包装器类内部定义了隐式转换函数

operator T&() const { return _ref; }

这个函数会将包装器对象隐式转化为被包装的对象的引用,而此时,这个被包装的对象正在另一个栈空间中呢,所以它非常适合被绑定到左值引用。当然,这发生在包装器对象尝试以右值的方式被绑定到线程函数的引用类型参数上时

至此,我们可以运行一下修改后的代码,以作验证:

g++ parameter1.cxx -o main -std=c++11
./main
addr: 0x16af47078 value: hello
addr: 0x16af47078 value: hello_new!

可以看到,尽管线程函数传参的路途再曲折,也会顺利将 data 传进去。


http://www.ppmy.cn/devtools/39321.html

相关文章

深入浅出微前端架构

微前端&#xff08;Micro-frontends&#xff09;是一种设计思想&#xff0c;旨在将大型前端应用分解成小的、独立的、可复用的部分&#xff0c;每个部分都有自己独立的责任域。这种架构模式借鉴了微服务的理念&#xff0c;将其应用于前端开发&#xff0c;使得不同的团队可以独立…

elasticsearch安装配置注意事项

安装Elasticsearch时&#xff0c;需要注意以下几个重要事项&#xff1a; 1、版本选择&#xff1a;选择与你系统和其他组件&#xff08;如Logstash、Kibana&#xff09;兼容的Elasticsearch版本。 2、Java环境&#xff1a;Elasticsearch是基于Java构建的&#xff0c;因此确保已…

java JMH 学习

JMH 是什么&#xff1f; JMH&#xff08;Java Microbenchmark Harness&#xff09;是一款专用于代码微基准测试的工具集&#xff0c;其主要聚焦于方法层面的基准测试&#xff0c;精度可达纳秒级别。此工具由 Oracle 内部负责实现 JIT 的杰出人士编写&#xff0c;他们对 JIT 及…

11.1.k8s中pod的调度-nodeSelector节点选择器

目录 一、概念 二、节点选择器nodeSelector的使用 一、概念 NodeSelector是Kubernetes调度器的一部分&#xff0c;它允许开发者根据节点的标签&#xff0c;精确地控制Pod在集群中的调度位置。通过在Pod的定义中设置NodeSelector&#xff0c;可以确保Pod只会被调度到具有特定标…

浅谈云计算资源和服务

目录 前言 正文 专有名词及其首字母缩写 轻量级应用服务器 云服务器ECS 专有网络VPC 其他类服务 尾声 &#x1f52d; Hi,I’m Pleasure1234&#x1f331; I’m currently learning Vue.js,SpringBoot,Computer Security and so on.&#x1f46f; I’m studying in University o…

信息技术自主可控的意义,针对国产化替换,服务器虚拟化或比公有云更具优势

我们之前在文章《博通收购VMware后&#xff0c;经销商和用户如何应对&#xff1f;新出路&#xff1a;虚拟化国产替代&#xff0c;融入信创云生态》中提到&#xff1a; 从信创整体发展和政策标准来看&#xff0c;供应商必须满足两个条件&#xff1a;一是融入国产信息技术生态&am…

Linux系统编程--初识Linux

目录 一、相关概念 1、Unix系统 2、操作系统 操作系统的分类&#xff1a; 流行的操作系统&#xff1a; 3、Ubuntu系统及特点 二、Ubuntu安装 三、Linux目录 /根目录 路径分类&#xff1a; 四、shell指令 1、命令行提示符&#xff1a; 2、指令 2.1命令基本的操作&…

CSS-页面导航栏实现-每文一言(过有意义的生活,做最好的自己)

&#x1f390;每文一言 过有意义的生活,做最好的自己 目录 &#x1f390;每文一言 &#x1f6d2;盒子模型 &#x1f453;外间距 (margin) &#x1f97c;边框 &#x1f45c;内边距 切换盒子模型计算方案&#xff1a; &#x1f3a2; 浮动布局 浮动特点 &#x1f3c6;导航…