当前位置: 首页 > news >正文

【多线程】c++11多线程编程(二)——理解线程类的构造函数

构造函数的参数

std::thread类的构造函数是使用可变参数模板实现的,也就是说,可以传递任意个参数,第一个参数是线程的入口函数,而后面的若干个参数是该函数的参数

第一参数的类型并不是c语言中的函数指针(c语言传递函数都是使用函数指针),在c++11中,增加了可调用对象(Callable Objects)的概念,总的来说,可调用对象可以是以下几种情况:

  • 函数指针
  • 重载了operator()运算符的类对象,即仿函数
  • lambda表达式(匿名函数)
  • std::function

函数指针示例

// 普通函数 无参
void function_1() {
}

// 普通函数 1个参数
void function_2(int i) {
}

// 普通函数 2个参数
void function_3(int i, std::string m) {
}

std::thread t1(function_1);
std::thread t2(function_2, 1);
std::thread t3(function_3, 1, "hello");

t1.join();
t2.join();
t3.join();

实验的时候还发现一个问题,如果将重载的函数作为线程的入口函数,会发生编译错误!编译器搞不清楚是哪个函数,如下面的代码:

// 普通函数 无参
void function_1() {
}

// 普通函数 1个参数
void function_1(int i) {
}
std::thread t1(function_1);
t1.join();
// 编译错误
/*
C:\Users\Administrator\Documents\untitled\main.cpp:39: 
error: no matching function for call to 'std::thread::thread(<unresolved overloaded function type>)'
     std::thread t1(function_1);
                              ^
*/

仿函数

// 仿函数
class Fctor {
public:
    // 具有一个参数
    void operator() () {

    }
};
Fctor f;
std::thread t1(f);  
// std::thread t2(Fctor()); // 编译错误 
std::thread t3((Fctor())); // ok
std::thread t4{Fctor()}; // ok

一个仿函数类生成的对象,使用起来就像一个函数一样,比如上面的对象f,当使用f()时就调用operator()运算符。所以也可以让它成为线程类的第一个参数,如果这个仿函数有参数,同样的可以写在线程类的后几个参数上。

t2之所以编译错误,是因为编译器并没有将Fctor()解释为一个临时对象,而是将其解释为一个函数声明,编译器认为你声明了一个函数,这个函数不接受参数,同时返回一个Factor对象。解决办法就是在Factor()外包一层小括号(),或者在调用std::thread的构造函数时使用{},这是c++11中的新的同意初始化语法。

但是,如果重载的operator()运算符有参数,就不会发生上面的错误。

匿名函数

std::thread t1([](){
    std::cout << "hello" << std::endl;
});

std::thread t2([](std::string m){
    std::cout << "hello " << m << std::endl;
}, "world");

std::function

class A{
public:
    void func1(){
    }

    void func2(int i){
    }
    void func3(int i, int j){
    }
};

A a;
std::function<void(void)> f1 = std::bind(&A::func1, &a);
std::function<void(void)> f2 = std::bind(&A::func2, &a, 1);
std::function<void(int)> f3 = std::bind(&A::func2, &a, std::placeholders::_1);
std::function<void(int)> f4 = std::bind(&A::func3, &a, 1, std::placeholders::_1);
std::function<void(int, int)> f5 = std::bind(&A::func3, &a, std::placeholders::_1, std::placeholders::_2);

std::thread t1(f1);
std::thread t2(f2);
std::thread t3(f3, 1);
std::thread t4(f4, 1);
std::thread t5(f5, 1, 2);

传值还是引用

先提出一个问题:如果线程入口函数的的参数是引用类型,在线程内部修改该变量,主线程的变量会改变吗?

代码如下:

#include <iostream>
#include <thread>
#include <string>

// 仿函数
class Fctor {
public:
    // 具有一个参数 是引用
    void operator() (std::string& msg) {
        msg = "wolrd";
    }
};



int main() {
    Fctor f;
    std::string m = "hello";
    std::thread t1(f, m);

    t1.join();
    std::cout << m << std::endl;
    return 0;
}

// vs下: 最终是:"hello"
// g++编译器: 编译报错

事实上,该代码使用g++编译会报错,而使用vs2015并不会报错,但是子线程并没有成功改变外面的变量m

我是这么认为的:std::thread类,内部也有若干个变量,当使用构造函数创建对象的时候,是将参数先赋值给这些变量,所以这些变量只是个副本,然后在线程启动并调用线程入口函数时,传递的参数只是这些副本,所以内部怎么操作都是改变副本,而不影响外面的变量。g++可能是比较严格,这种写法可能会导致程序发生严重的错误,索性禁止了。

首先,&是类型说明符,而std::ref是一个函数,返回std::reference_wrapper(类似于指针)。

为什么需要std::ref?(std::cref类似)

主要是考虑到c++11中的函数式编程,例如:std::bind。

示例:

#include <iostream>
#include <functional>



void foo(int& a) {
    ++a;

}


void test_function(std::function<void(void)> fun) {
    fun();

}


int main() {
    int a = 1;

    std::cout << "a = " << a << "\n";

    test_function(std::bind(foo, a));

    std::cout << "a = " << a << "\n";

    test_function(std::bind(foo, std::ref(a)));

    std::cout << "a = " << a << "\n";

    return 0;
}

输出:

a = 1a = 1a = 2

注意第二行"a = 1",为什么a没有发生改变呢?因为std::bind将参数拷贝了。因而foo改变的是拷贝,不影响a。问题来了:为什么std::bind要拷贝参数,而不是直接保存引用呢?这主要是考虑到调用者的需求。确实存在一些情况,我们并不希望std::bind直接对参数进行操作。为了满足不同的调用者的需求,只能统一进行拷贝。同时,为了可以使用引用,提供了std::ref,std::cref让用户进行选择。此时,用户相当于传入了一个会自动解引用的指针,可以复制,且影响原始参数。

而如果可以想真正传引用,可以在调用线程类构造函数的时候,用std::ref()包装一下。如下面修改后的代码:

std::thread t1(f, std::ref(m));

然后vsg++都可以成功编译,而且子线程可以修改外部变量的值。

当然这样并不好,多个线程同时修改同一个变量,会发生数据竞争。

同理,构造函数的第一个参数是可调用对象,默认情况下其实传递的还是一个副本。

#include <iostream>
#include <thread>
#include <string>

class A {
public:
    void f(int x, char c) {}
    int g(double x) {return 0;}
    int operator()(int N) {return 0;}
};

void foo(int x) {}

int main() {
    A a;
    std::thread t1(a, 6); // 1. 调用的是 copy_of_a()
    std::thread t2(std::ref(a), 6); // 2. a()
    std::thread t3(A(), 6); // 3. 调用的是 临时对象 temp_a()
    std::thread t4(&A::f, a, 8, 'w'); // 4. 调用的是 copy_of_a.f()
    std::thread t5(&A::f, &a, 8, 'w'); //5.  调用的是 a.f()
    std::thread t6(std::move(a), 6); // 6. 调用的是 a.f(), a不能够再被使用了
    t1.join();
    t2.join();
    t3.join();
    t4.join();
    t5.join();
    t6.join();
    return 0;
}

对于线程t1来说,内部调用的线程函数其实是一个副本,所以如果在函数内部修改了类成员,并不会影响到外面的对象。只有传递引用的时候才会修改。所以在这个时候就必须想清楚,到底是传值还是传引用!

线程对象只能移动不可复制

线程对象之间是不能复制的,只能移动,移动的意思是,将线程的所有权在std::thread实例间进行转移。

void some_function();
void some_other_function();
std::thread t1(some_function);
// std::thread t2 = t1; // 编译错误
std::thread t2 = std::move(t1); //只能移动 t1内部已经没有线程了
t1 = std::thread(some_other_function); // 临时对象赋值 默认就是移动操作
std::thread t3;
t3 = std::move(t2); // t2内部已经没有线程了
t1 = std::move(t3); // 程序将会终止,因为t1内部已经有一个线程在管理了

参考

  1. C++并发编程实战
  2. C++ Threading #8: Using Callable Objects


作者:StormZhu
链接:https://www.jianshu.com/p/109df8a7e627
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

相关文章:

  • 【函数式编程】什么是函数式编程? C语言为何不是函数式语言?
  • 【多线程】c++11多线程编程(三)——竞争条件与互斥锁
  • 【多线程】c++11多线程编程(四)——死锁(Dead Lock)
  • 【多线程】c++11多线程编程(六)——条件变量(Condition Variable)
  • 【多线程】c++11多线程编程(五)——unique_lock和lock_guard
  • 【C/C++】内存和字符操作整理
  • 【知识】如何高效地在github上找开源项目学习?
  • 【mySQL】比explain更加详细的分析计划:Query Profiler
  • 【mySQL】mysql是行级锁还是表级锁
  • 【mySQL】提升mysql性能的关键参数之innodb_buffer_pool_size、innodb_buffer_pool_instances
  • 【mySQL】数据库优化 方案
  • 【interview】遇到的困难
  • 【排序】常见排序算法及其时间复杂度
  • 【mySQL】数据库[配置]优化 方案(MySQL并行写入、查询性能调优(多核CPU))
  • 【C++11】C++ 中using 的使用
  • [译] React v16.8: 含有Hooks的版本
  • 4. 路由到控制器 - Laravel从零开始教程
  • JavaScript/HTML5图表开发工具JavaScript Charts v3.19.6发布【附下载】
  • JSDuck 与 AngularJS 融合技巧
  • JS字符串转数字方法总结
  • Tornado学习笔记(1)
  • 从tcpdump抓包看TCP/IP协议
  • 关键词挖掘技术哪家强(一)基于node.js技术开发一个关键字查询工具
  • 马上搞懂 GeoJSON
  • 巧用 TypeScript (一)
  • 腾讯视频格式如何转换成mp4 将下载的qlv文件转换成mp4的方法
  • 新版博客前端前瞻
  • 栈实现走出迷宫(C++)
  • 正则表达式小结
  • media数据库操作,可以进行增删改查,实现回收站,隐私照片功能 SharedPreferences存储地址:
  • ​【C语言】长篇详解,字符系列篇3-----strstr,strtok,strerror字符串函数的使用【图文详解​】
  • #{} 和 ${}区别
  • #快捷键# 大学四年我常用的软件快捷键大全,教你成为电脑高手!!
  • #微信小程序:微信小程序常见的配置传值
  • (39)STM32——FLASH闪存
  • (c语言)strcpy函数用法
  • (Spark3.2.0)Spark SQL 初探: 使用大数据分析2000万KF数据
  • (附源码)apringboot计算机专业大学生就业指南 毕业设计061355
  • (论文阅读32/100)Flowing convnets for human pose estimation in videos
  • (删)Java线程同步实现一:synchronzied和wait()/notify()
  • (实战篇)如何缓存数据
  • (算法)求1到1亿间的质数或素数
  • (原創) 如何讓IE7按第二次Ctrl + Tab時,回到原來的索引標籤? (Web) (IE) (OS) (Windows)...
  • .net core使用RPC方式进行高效的HTTP服务访问
  • .NET/C# 反射的的性能数据,以及高性能开发建议(反射获取 Attribute 和反射调用方法)
  • .netcore 获取appsettings
  • .NET导入Excel数据
  • .Net转Java自学之路—SpringMVC框架篇六(异常处理)
  • /proc/stat文件详解(翻译)
  • [ NOI 2001 ] 食物链
  • [ 隧道技术 ] 反弹shell的集中常见方式(四)python反弹shell
  • []常用AT命令解释()
  • [<MySQL优化总结>]
  • [BJDCTF2020]The mystery of ip
  • [bzoj 3124][sdoi 2013 省选] 直径