C++11的多线程、function和bind、可变函数模板-2

简介: C++11的多线程、function和bind、可变函数模板

1.4 异步操作

  • std::future : 异步指向某个任务,然后通过future特性去获取任务函数的返回结果。
  • std::aysnc: 异步运行某个任务函数
  • std::packaged_task :将任务和feature绑定在一起的模板,是一种封装对任务的封装。
  • std::promise

1.4.1 std::aysnc和std::future

std::future用于在多线程编程中处理异步操作的结果。它提供了一种机制来获取异步任务的返回值或异常,并在需要时等待异步任务完成。


线程可以周期性的在这个future上等待一小段时间,检查future是否已经ready,如果没有,该线程可以先去做另一个任务。一旦future就绪,该future就无法复位(无法再次使用这个future等待这个事件),所以future代表的是一次性事件。


future的类型

在库的头文件中声明了两种future,唯一future(std::future)和共享future(std::shared_future)。


这两个是参照std::unique_ptr和std::shared_ptr设立的,前者的实例是仅有的一个指向其关联事件的实例。而后者可以有多个实例指向同一个关联事件,当事件就绪时,所有指向同一事件的std::shared_future实例会变成就绪


future的使用


std::future是一个模板,例如std::future,模板参数就是期待返回的类型,虽然future被用于线程间通信,但其本身却并不提供同步访问。


future使用的时机是当你不需要立刻得到一个结果的时候,你可以开启一个线程帮你去做一项任务,并期待这个任务的返回。但是std::thread并没有提供这样的机制,这就需要用到std::async和std::future


std::async返回一个std::future对象,而不是给你一个确定的值。当你需要使用这个值的时候,对future使用get(),线程就会阻塞直到future就绪,然后返回该值.


#include <iostream>
#include <future>
#include <thread>
using namespace std;
int find_result_to_add()
{
    std::this_thread::sleep_for(std::chrono::seconds(5)); // 用来测试异步延迟的影响
    std::cout << "find_result_to_add" << std::endl;
    return 1 + 1;
}
int find_result_to_add2(int a, int b)
{
    std::this_thread::sleep_for(std::chrono::seconds(5)); // 用来测试异步延迟的影响
    return a + b;
}
void do_other_things()
{
    std::cout << "do_other_things" << std::endl;
}
int main()
{
   std::future<int> result = std::async(find_result_to_add);  // async把find_result_to_add交给其他线程异步运行,不会阻塞do_other_things();的运行
// 另外两种声明方式,推荐第三中
// std::future<decltype (find_result_to_add())> result = std::async(find_result_to_add); // decltype自动推导参数类型
// auto result = std::async(find_result_to_add);  // 推荐的写法
   do_other_things();
   std::cout << "result: " << result.get() << std::endl;  // get会阻塞等待任务函数的返回值
//    std::future<decltype(find_result_to_add2(int, int))> result2 = std::async(find_result_to_add2, 10, 20); //错误
    std::future<decltype (find_result_to_add2(0, 0))> result2 = std::async(find_result_to_add2, 10, 20);
    std::cout << "result2: " << result2.get() << std::endl;  // 延迟是否有影响?
    return 0;
}

上面代码中,std::async(find_result_to_add)把函数find_result_to_add异步运行,不会阻塞主线程。当需要返回值时候,再通过result.get()返回。


1.4.2 std::packaged_task

std::packaged_task用于将可调用对象(如函数、Lambda 表达式)封装成一个具备异步执行能力的任务对象。它可以帮助我们将任务的执行与结果的获取分离开来,以便在需要时获取任务的返回值。


使用 std::packaged_task 将可调用对象封装成一个任务对象 task。通过调用 get_future() 方法,我们获取了与任务关联的std::future对象 result,用于获取任务的返回值。


#include <iostream>
#include <future>
#include <thread>
using namespace std;
int add(int a, int b, int c)
{
    std::cout << "call add\n";
    return a + b + c;
}
void do_other_things()
{
    std::cout << "do_other_things" << std::endl;
}
int main()
{
    std::packaged_task<int(int, int, int)> task(add);  // 1. 封装任务,还没有运行,意味着不会阻塞
    do_other_things();
    std::future<int> result = task.get_future(); // 2.获取 future,注意这里也不运行
    task(1, 1, 2);   //3.这里才真正运行。并且必须要让任务执行,否则在get()获取future的值时会一直阻塞
    std::cout << "result:" << result.get() << std::endl;
    return 0;
}

1.4.3 std::promise

用于在一个线程中产生某个值,并在另一个线程中获取该值。它提供了一种线程之间传递数据的方式,通过 std::promise 和 std::future 的结合使用,可以实现线程间的数据传递和同步等功能。


#include <future>
#include <string>
#include <thread>
#include <iostream>
using namespace std;
void print1(std::promise<std::string>& p)
{
    std::cout << "print1 sleep" << std::endl;
    std::this_thread::sleep_for(std::chrono::seconds(1));
    p.set_value("set string"); // 履行承诺,设置返回值
}
void do_some_other_things()
{
    std::cout << "do_some_other_things" << std::endl;
}
int main()
{
    std::cout << "main1 -------------" << std::endl;
    // 1.创建了一个 std::promise 对象 promise,用于承诺将计算结果传递给后续处理。
    std::promise<std::string> promise;  // 注意类型:
    // 2.调用 get_future() 方法,我们获取了与承诺关联的 std::future 对象 result,用于获取计算结果。
    std::future<std::string> result = promise.get_future(); // future
    // 3.创建了一个新线程 worker,并将承诺对象 promise 传递给 print1 函数
    std::thread t(print1, std::ref(promise));  // 线程设置 传引用std::ref
    do_some_other_things();
    // 4.等待子线程完成
    t.join();
    std::cout << "wait get result" << std::endl;
    // 5.在其他操作完成后,我们使用 get() 函数从 result 中获取计算结果。
    // 如果结果还没有被设置(即承诺还没有被履行),调用 get() 会导致当前线程阻塞,直到结果可用为止。
    // 一旦结果可用,我们就可以使用该结果进行后续处理。
    std::cout <<"result " << result.get() << std::endl; // 在主线程等待 promise的返回 result set string
    return 0;
}
main1 -------------
do_some_other_things
print1 sleep
wait get result
result set string


二、function和bind用法


std::function 和 std::bind 都是 C++ 的函数工具,用于实现函数对象的封装和绑定。


2.1 function

std::function 是一个通用的函数封装类模板,用于存储、管理和调用可调用对象(如函数、函数指针、Lambda 表达式等)。它提供了一种统一的方式来处理不同类型的可调用对象,并且可以像普通函数一样进行调用。类似C语言的函数指针。


包含头文件:#include <functional>


1)保存普通函数


void func1(int a)
{
  cout << a << endl;
}
std::function<void(int a)> func;
func = func1;
func(2);  //2

2)保存lambda表达式


std::function<void()> func_1 = [](){cout << "hello world" << endl;};  
func_1();  //hello world

3)保存成员函数


class A{
public:
  A(string name) : name_(name){}
  void func3(int i) const {cout <<name_ << ", " << i << endl;}
private:
  string name_;
};
//3 保存成员函数
std::function<void(const A&,int)> func3_ = &A::func3;
A a("darren");
func3_(a, 1);

2.2 bind

用于将可调用对象与其参数进行绑定,生成一个新的可调用对象。


调用bind的一般形式:


auto newCallable = bind(callable, arg_list);

其中,newCallable本身是一个可调用对象,arg_list是一个逗号分隔的参数列表,对应给定的callable的参数。即,当我们调用newCallable时,newCallable会调用callable,并传给它arg_list中的参数。


arg_list中的参数可能包含形如n的名字,其中n是一个整数,这些参数是“占位符”,表示newCallable的参数,它们占据了传递给newCallable的参数的“位置”。数值n表示生成的可调用对象中参数的位置:1为newCallable的第一个参数,_2为第二个参数,以此类推。


范例


#include <iostream>
#include <functional>
using namespace std;
class A
{
public:
    // 重载fun_3,主要bind的时候需要
//    std::bind((void(A::*)(int, int))&A::fun_3
    void fun_3(int k,int m)
    {
        cout << "fun_3 a = " << a<< endl;
        cout<<"print: k="<<k<<",m="<<m<<endl;
    }
//    std::bind((void(A::*)(string))&A::fun_3
    void fun_3(string str) {
        cout<<"print: str="<<str<<endl;
    }
    int a;
};
void fun_1(int x,int y,int z)
{
    cout<<"fun_1 print: x=" <<x<<",y="<< y << ",z=" <<z<<endl;
}
void fun_2(int &a,int &b)
{
    a++;
    b++;
    cout<<"print: a=" <<a<<",b="<<b<<endl;
}
void func2_1(int a, int b)
{
    cout << "func2_1 a + b = " << a+b << endl;
}
int func2_1(string a, string b)
{
    cout << "func2_1 a + b = " << a << b<< endl;
    return 0;
}

1)先看绑定函数


    //f1的类型为 function<void(int, int, int)>
    cout << "\n\nstd::bind(fun_1, 1, 2, 3) -----------------\n";
    auto f1 = std::bind(fun_1, 1, 2, 3); //表示绑定函数 fun 的第一,二,三个参数值为: 1 2 3
    f1(); //print: x=1,y=2,z=3
    cout << "\n\nstd::bind(fun_1, 10, 20, 30) -----------------\n";
    auto f11 = std::bind(fun_1, 10, 20, 30); //表示绑定函数 fun 的第一,二,三个参数值为: 1 2 3
    f11();
    cout << "\n\nstd::bind(fun_1, placeholders::_1,placeholders::_2, 3) -----------------\n";
    auto f2 = std::bind(fun_1, placeholders::_1, placeholders::_2, 3);
    //表示绑定函数 fun 的第三个参数为 3,而fun 的第一,二个参数分别由调用 f2 的第一,二个参数指定
    f2(1,2);//print: x=1,y=2,z=3
    f2(10,21,30); // 传入30也没有用
   cout << "\n\nstd::bind(fun_1,placeholders::_2,placeholders::_1,3) -----------------\n";
   auto f3 = std::bind(fun_1,placeholders::_2,placeholders::_1,3);
   //表示绑定函数 fun 的第三个参数为 3,而fun 的第一,二个参数分别由调用 f3 的第二,一个参数指定
   //注意: f2  和  f3 的区别。
   f3(1,2);//print: x=2,y=1,z=3
   cout << "\n\nstd::bind(fun_2, placeholders::_1, n) -----------------\n";
   int m = 2;
   int n = 3;
   auto f4 = std::bind(fun_2, placeholders::_1, n); //表示绑定fun_2的第一个参数为n, fun_2的第二个参数由调用f4的第一个参数(_1)指定。
   f4(m); //print: a=3,b=4
   cout<<"m="<<m<<endl;//m=3  说明:bind对于不事先绑定的参数,通过std::placeholders传递的参数是通过引用传递的,如m
   cout<<"n="<<n<<endl;//n=3  说明:bind对于预先绑定的函数参数是通过值传递的,如n


std::bind(fun_1, 1, 2, 3) -----------------
fun_1 print: x=1,y=2,z=3
std::bind(fun_1, 10, 20, 30) -----------------
fun_1 print: x=10,y=20,z=30
std::bind(fun_1, placeholders::_1,placeholders::_2, 3) -----------------
fun_1 print: x=1,y=2,z=3
fun_1 print: x=10,y=21,z=3
std::bind(fun_1,placeholders::_2,placeholders::_1,3) -----------------
fun_1 print: x=2,y=1,z=3
std::bind(fun_2, placeholders::_1, n) -----------------
print: a=3,b=4
m=3
n=3

2)再看一下绑定类


    cout << "\n\nstd::bind(&A::fun_3, &a,placeholders::_1,placeholders::_2) -----------------\n";
    A a;
    a.a = 10;
    //f5的类型为 function<void(int, int)>, 使用auto关键字
    auto f5 = std::bind((void(A::*)(int, int))&A::fun_3, &a, 40, 50); //void是返回类型,(A::*) 是指向类 A 的成员的作用域限定符,(int, int) 是函数参数列表
    f5(10,20);
    cout << "\n\nstd::bind(&A::fun_3, &a2,placeholders::_1,placeholders::_2) -----------------\n";
    A a2;
    a2.a = 20;
    //f5的类型为 function<void(int, int)>
    auto f6 = std::bind((void(A::*)(int, int))&A::fun_3, &a2,placeholders::_1,placeholders::_2); //使用auto关键字
    f6(10,20);//
    cout << "\n\nstd::bind(&A::fun_3, a,std::placeholders::_1,std::placeholders::_2) -----------------\n";
    auto f_str = std::bind((void(A::*)(string))&A::fun_3, a,std::placeholders::_1);
    f_str("darren");
std::bind(&A::fun_3, &a,placeholders::_1,placeholders::_2) -----------------
fun_3 a = 10
print: k=40,m=50
std::bind(&A::fun_3, &a2,placeholders::_1,placeholders::_2) -----------------
fun_3 a = 20
print: k=10,m=20
std::bind(&A::fun_3, a,std::placeholders::_1,std::placeholders::_2) -----------------
print: str=darren

说明的一点是,如果没有重载的话,可以直接写

auto f5 = std::bind(&A::fun_3, &a, 40, 50);


三、可变参数模板


3.1 语法

可变参数模板是 C++11 提供的一种特性,用于处理数量可变的参数类型。通过可变参数模板,可以在一个模板函数或类中接受任意数量和类型的参数。

template<class... T, class... Args>
void functionName(T first, Args... args) {
    // 函数体
}

1)使用 template 关键字定义一个模板函数,其模板类型参数由 class... T表示。T 是第一个参数的类型,而 Args 是剩余参数的类型包(parameter pack)。

2)第一个参数 T first 是必传的,用来接收第一个参数。

3)Args... args 是一个参数包扩展(pack expansion),用于接收剩余的参数。通过 … 表示参数包的展开,可以将其中的每个参数依次进行处理。


例如


#include <iostream>
using namespace std;
template <class... T>
void fun(T... args)
{
    cout << sizeof...(args) << endl; //打印变参的个数
}
int main()
{    
    fun();        //0
    fun(1, 2);    //2
    fun(1, 2.5, "");    //3
    return 0;
}

这个例子只是简单的将可变模版参数的个数打印出来,如果我们需要将参数包中的每个参数打印出来的话就需要通过一些方法了。展开可变模版参数函数的方法一般有三种:

1)通过递归函数来展开参数包,

2)是通过逗号表达式来展开参数包。

3)c++ 17的折叠表达式


3.2 递归函数来展开参数包

通过递归函数展开参数包,需要提供一个参数包展开的函数和一个递归终止函数,递归终止函数正是用来终止递归的


#include <iostream>
using namespace std;
//递归终止函数
void print()
{
   cout << "empty" << endl;
}
//展开函数
template <class T, class ...Args>
void print(T head, Args... rest)
{
   cout << "parameter " << head << endl;
   print(rest...);
}
int main(void)
{
   print(1,2,3,"zxm", "xxjp");
   return 0;
}
parameter 1
parameter 2
parameter 3
parameter zxm
parameter xxjp
empty

上例会输出每一个参数,直到为空时输出empty。展开参数包的函数有两个,一个是递归函数,另外一个是递归终止函数,参数包Args...在展开的过程中递归调用自己,每调用一次参数包中的参数就会少一个,直到所有的参数都展开为止,当没有参数时,则调用非模板函数print终止递归过程。


上面的递归终止函数还可以写成这样:


template <class T>
void print(T t)
{
 cout << t << endl;
}

3.3 逗号表达式展开参数包

递归函数展开参数包是一种标准做法,也比较好理解,但就是必须要一个重载的递归终止函数,即必须要有一个同名的终止函数来终止递归。


逗号表达式是 C++ 中的一种表达式,由逗号(,)操作符连接多个子表达式而形成。逗号表达式的求值顺序是从左到右,每个子表达式都会被依次求值,最终整个逗号表达式的结果是最后一个子表达式的值。

int a = 1, b = 2, c;
c = (a++, b++, a + b);  // 逗号表达式 (a++, b++) 的结果是 b,c 的值为 a + b 的结果

初始化列表(initializer list)是一种在 C++ 中用于初始化数组、结构体、类等复合类型对象的语法。它使用花括号 {} 将初始化的值括起来,多个值之间用逗号分隔。


int arr[] = {1, 2, 3, 4, 5};  // 初始化整型数组

逗号表达式可以不通过递归方式来展开参数包,这种方式需要借助逗号表达式和初始化列表。比如前面print的例子可以改成这样:

#include <iostream>
using namespace std;
template <class T>
void printarg(T t)
{
    cout << t << endl;
}
template <class ...Args>
void expand(Args... args)
{
    int arr[] = {(printarg(args), 0)...};
}
int main()
{
    expand(1,2,3,"zxm");
    return 0;
}

这种展开参数包的方式,不需要通过递归终止函数,是直接在expand函数体中展开的, printarg不是一个递归终止函数,只是一个处理参数包中每一个参数的函数。


expand函数中的逗号表达式:(printarg(args), 0),先执行printarg(args),再得到逗号表达式的结果0。

同时还用到了C++11的另外一个特性——初始化列表,通过初始化列表来初始化一个变长数组,{(printarg(args), 0)...}将会展开成((printarg(arg1),0), (printarg(arg2),0), (printarg(arg3),0), etc... ),最终会创建一个元素值都为0的数组int arr[sizeof...(Args)]。


由于是逗号表达式,在创建数组的过程中会先执行逗号表达式前面的部分printarg(args)打印出参数,也就是说在构造int数组的过程中就将参数包展开了,这个数组的目的纯粹是为了在数组构造的过程展开参数包。


我们可以把上面的例子再进一步改进一下,将函数作为参数,就可以支持lambda表达式了,从而可以少写一个递归终止函数了,具体代码如下:

#include <iostream>
using namespace std;
template<class F, class... Args>void expand(const F& f, Args&&...args)
{
 //这里用到了完美转发
 initializer_list<int>{(f(std::forward< Args>(args)),0)...};
}
int main()
{
  expand([](int i){cout<<i<<endl;}, 1,2,3);
  return 0;
}

3.4 折叠表达式

#include <iostream>
template<typename... Args>
void printArgs(Args... args) {
    ((std::cout << args << std::endl), ...);  // 折叠表达式展开参数包并换行
//    ((std::cout << args << ' '), ...);  // 折叠表达式展开参数包并添加空格
}
int main() {
    printArgs(1, 2.5, "hello", 'a');
    return 0;
}

需要注意的是,编译要加上-std = c++17

目录
相关文章
|
1月前
|
程序员 C++ 容器
在 C++中,realloc 函数返回 NULL 时,需要手动释放原来的内存吗?
在 C++ 中,当 realloc 函数返回 NULL 时,表示内存重新分配失败,但原内存块仍然有效,因此需要手动释放原来的内存,以避免内存泄漏。
|
1月前
|
存储 前端开发 C++
C++ 多线程之带返回值的线程处理函数
这篇文章介绍了在C++中使用`async`函数、`packaged_task`和`promise`三种方法来创建带返回值的线程处理函数。
48 6
|
1月前
|
C++
C++ 多线程之线程管理函数
这篇文章介绍了C++中多线程编程的几个关键函数,包括获取线程ID的`get_id()`,延时函数`sleep_for()`,线程让步函数`yield()`,以及阻塞线程直到指定时间的`sleep_until()`。
25 0
C++ 多线程之线程管理函数
|
1月前
|
编译器 C语言 C++
C++入门6——模板(泛型编程、函数模板、类模板)
C++入门6——模板(泛型编程、函数模板、类模板)
46 0
C++入门6——模板(泛型编程、函数模板、类模板)
|
1月前
|
存储 消息中间件 资源调度
C++ 多线程之初识多线程
这篇文章介绍了C++多线程的基本概念,包括进程和线程的定义、并发的实现方式,以及如何在C++中创建和管理线程,包括使用`std::thread`库、线程的join和detach方法,并通过示例代码展示了如何创建和使用多线程。
48 1
C++ 多线程之初识多线程
|
30天前
|
Java 开发者
在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口
【10月更文挑战第20天】在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口。本文揭示了这两种方式的微妙差异和潜在陷阱,帮助你更好地理解和选择适合项目需求的线程创建方式。
20 3
|
30天前
|
Java 开发者
在Java多线程编程中,选择合适的线程创建方法至关重要
【10月更文挑战第20天】在Java多线程编程中,选择合适的线程创建方法至关重要。本文通过案例分析,探讨了继承Thread类和实现Runnable接口两种方法的优缺点及适用场景,帮助开发者做出明智的选择。
19 2
|
30天前
|
Java
Java中多线程编程的基本概念和创建线程的两种主要方式:继承Thread类和实现Runnable接口
【10月更文挑战第20天】《JAVA多线程深度解析:线程的创建之路》介绍了Java中多线程编程的基本概念和创建线程的两种主要方式:继承Thread类和实现Runnable接口。文章详细讲解了每种方式的实现方法、优缺点及适用场景,帮助读者更好地理解和掌握多线程编程技术,为复杂任务的高效处理奠定基础。
30 2
|
30天前
|
Java 开发者
Java多线程初学者指南:介绍通过继承Thread类与实现Runnable接口两种方式创建线程的方法及其优缺点
【10月更文挑战第20天】Java多线程初学者指南:介绍通过继承Thread类与实现Runnable接口两种方式创建线程的方法及其优缺点,重点解析为何实现Runnable接口更具灵活性、资源共享及易于管理的优势。
34 1
|
30天前
|
安全 Java 开发者
Java多线程中的`wait()`、`notify()`和`notifyAll()`方法,探讨了它们在实现线程间通信和同步中的关键作用
本文深入解析了Java多线程中的`wait()`、`notify()`和`notifyAll()`方法,探讨了它们在实现线程间通信和同步中的关键作用。通过示例代码展示了如何正确使用这些方法,并分享了最佳实践,帮助开发者避免常见陷阱,提高多线程程序的稳定性和效率。
38 1

热门文章

最新文章

下一篇
无影云桌面