0%

C++_bind_function_package_task

该笔记作为理解通用线程池代码的基础。包含了异步编程的方法,函数绑定的方法等…

参考链接:

http://cpp11.bitfoc.us/#title-6 https://cloud.tencent.com/developer/article/1584075 https://www.jianshu.com/p/f191e88dcc80

function类

通过std::function 对 C++中各种可调用实体(普通函数、Lambda表达式、函数指针、以及其它函数对象等)的封装,形成一个新的可调用的std::function对象,只要被包装的可调用类型的函数符合相应的函数调用签名(即 返回值类型,输入参数列表类型相同的函数);让我们不再纠结那么多的可调用实体。std::function对象最大的用处就是在实现函数回调 (延迟调用)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
//代码出自链接:http://www.jellythink.com/archives/771
#include <functional>
#include <iostream>
using namespace std;

std::function< int(int)> Functional;

// 普通函数
int TestFunc(int a)
{
return a;
}

// Lambda表达式
auto lambda = [](int a)->int{ return a; };

// 仿函数(functor)
class Functor
{
public:
int operator()(int a)
{
return a;
}
};

// 1.类成员函数
// 2.类静态函数
class TestClass
{
public:
int ClassMember(int a) { return a; }
static int StaticMember(int a) { return a; }
};

int main()
{
// 普通函数
Functional = TestFunc;
int result = Functional(10);
cout << "普通函数:"<< result << endl;

// Lambda表达式
Functional = lambda;
result = Functional(20);
cout << "Lambda表达式:"<< result << endl;

// 仿函数
Functor testFunctor;
Functional = testFunctor;
result = Functional(30);
cout << "仿函数:"<< result << endl;

// 类成员函数
TestClass testObj;
Functional = std::bind(&TestClass::ClassMember, testObj, std::placeholders::_1);
result = Functional(40);
cout << "类成员函数:"<< result << endl;

// 类静态函数
Functional = TestClass::StaticMember;
result = Functional(50);
cout << "类静态函数:"<< result << endl;

return 0;
}

实现技巧

首先 function 的底层实现其实是一个模板类,他使用了函数签名的模板特化形式:

1
2
3
4
5
6
7
8
9
// 默认的模板没实现
template <typename T>
class function;

// 实现有返回值类型和 2 个参数类型的偏特化 根据模板特化的原则可以知道,虽然原本的模板只有一个参数T,而这里偏特化却有三个参数,其实不是这样的,这里也还是一个参数。 < Ret(Arg0, Arg1) > 中等价于一个模板参数,是一个整体
template <typename Ret, typename Arg0, typename Arg1>
class function<Ret(Arg0, Arg1)> {
// ...
};

首先通过上面的 函数签名,就可以匹配到特定类型的函数调用,例如都是 int(int)返回值类型为 int 输入参数为int,只不过调用形式不同,可能是 函数指针,可能是 函数,lambda函数,拟函数等等。因此 function 类内部要对不同的调用对象做一步统一封装。

  1. 首先封装出来的类需要 可执行,因此需要重载 () 操作符

  2. 考虑无论什么调用对象,它都可以归结到一个函数指针,但是不知道函数指针的具体类型,因此 需要增加一个 (void*) p 类型的函数指针,指向输入的可调用对象。

  3. 我们希望 把 所有可调用对象都执行 一次 封装,封装成 这种形式:

  4. 当function<int(int)>( f ) 执行构造函数时,此时有了 f 具体的可调用对象,那么我们可以将构造函数加上 Functor 的模板,进而编译器可以根据 实例 f 自动推断 Functor 的具体类型。

    1
    2
    3
    4
    void* p = f;                           //  f 是实际输入的可调用对象  类型是 Functor 的函数指针
    R invoke_functor(void*p, int args){ // 我们希望将 f 统一包装成 统一的 invoke_functor() 函数,第一个参数是可调用对象的函数指针,后面的是函数的输入参数,统一都在 call_able 函数中首先 将可调用对象的类型转换为 它原本的 Functor类型然后调用。
    return static_cast<Functor>(p)(args);
    }

  5. 有了上面的 Functor 类型,我们可以将 不同的调用对象都封装成上述的 call_able 的形式来完成调用。多以就有如下更完整的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
template <typename R, typename... Args>
class function<R(Args...)>
{
void* functor_ptr; // 由于无法确定函数对象的具体类型, 就用 void* 保存

// 并利用以下模板函数转换类型
template <typename Functor>
static Functor& get_functor(void* p)
{
return *static_cast<Functor*>(p); // (c)
}

// 定义一个函数指针, 当需要调用函数对象时, 从此函数指针上调用
using invoke_fn_t = R(*)(void*, Args&&...);
invoke_fn_t invoke_f;
// 它将以下面这个函数的特化作为有效值
template <typename Functor>
static R invoke_functor(void* p, Args&&... args)
{
return get_functor<Functor>(p)(std::forward<Args>(args)...); // (b)
}
public:
// 对外暴露的函数调用算符重载
R operator()(Args&&... args)
{
// 检查是否为空, 并在为空时抛出异常
// if (!this->invoke_f) throw bad_function_call("function is null");
return this->invoke_f(this->functor_ptr, std::forward<Args>(args)...);
}

// 从函数对象构造
template <typename Functor>
function(Functor f)
{
this->invoke_f = invoke_functor<Functor>; // (a)
this->functor_ptr = new Functor(f);
}

// 允许隐式从 nullptr 转换构造, 此构造函数没有 explicit
function(std::nullptr_t = nullptr)
: invoke_f(nullptr)
{}
};
  1. 首先看 构造函数中的 (a) 部分,根据传入构造函数中的 实际调用对象 Functor,来构造一个通用的 函数指针 。单独看 invoke_functor 函数,是个模板函数,将这个模板函数实例化后成为一个实例化后的函数指针,实例化出的函数指针也是前面说的要封装出的通用调用方式,即第一个参数为 可调用对象的指针,后面是输入参数,在函数中完成函数调用。所以 invoke_f 定义的也是一个R(*)(void*, Args&&...);类型的通用函数指针。然后 再 new 一个指向可调用对象的函数指针,并把它存为 void* 类型,做为后面调invoke_functor的第一个参数。

    1. 部分就是 通用函数中需要干的事儿了,首先将 指向可调用对象的 void* 类型的指针 转化为 其原本的 Functor 类型,这一步是通过get_functor 函数 (c) 部分中使用 static_cast 完成的类型转换。
    1. 部分即为 一个强制类型转换

最终在 重载的 () 函数中,完成函数调用。

总结

总结一下实现的流程:

  1. 当我们只是声明一个 Function<int(int)> 类型时,还不知道具体的需要管理的 可调用对象的类型,因此内部只能先new一个 void* 指针用来指向将来需要指向的可调用对象。
  2. 当我们将 一个可调用对象赋值给 Func传入拷贝或者赋值构造函数时,通过该函数的模板实参推导可以得到 该实例的类型为 Functor
  3. 在拷贝构造/赋值构造函数中,使用Functor实例化一个 通用函数指针,即前面说的 第一个参数为 void 函数指针,后面为输入参数…Functor 目的是作为 通用函数中 static_cast转换的依据
  4. 将一个void指针指向 可调用对象
  5. 当要运行的时候,首先将 void* 指针传入通用函数,在函数中,根据Functor类型完成 void*指针到 Functor类型的类型转换,然后执行 该函数并返回计算结果。

bind函数

首先,std::bind 是标准库中新增的一个函数,std::function是一个类 ,package_task 也是一个类。可将std::bind函数看作一个通用的函数适配器,它接受一个可调用对象,生成一个新的可调用对象来“适应”原对象的参数列表。std::bind将可调用对象与其参数一起进行绑定,绑定后的结果可以使用std::function保存。std::bind主要有以下两个作用:

  • 将可调用对象和其参数绑定成一个防函数;
  • 只绑定部分参数,减少可调用对象传入的参数。

占位符

1
2
3
double my_divide (double x, double y) {return x/y;}  
auto fn_half = std::bind (my_divide,_1,2); //bind的第一个参数是函数名,普通函数做实参时,会隐式转换成函数指针。因此std::bind (my_divide,_1,2)等价于std::bind (&my_divide,_1,2);_1表示占位符,位于<functional>中,std::placeholders::_1;
std::cout << fn_half(10) << '\n'; // 5

普通成员函数的绑定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
struct Foo {
void print_sum(int n1, int n2){
std::cout << n1+n2 << '\n';
}
int data = 10;
};
int main() {
Foo foo;
auto f = std::bind(&Foo::print_sum, &foo, 95, std::placeholders::_1); //因为普通的成员函数 有 this指针,必须传入一个对象才行。如果是静态成员函数,不需要这样。
f(5); // 100
}
// 这是因为 类的成员函数指针的调用方法为 :
struct Foo {
int value;
void f() { std::cout << "f(" << this->value << ")\n"; }
void g() { std::cout << "g(" << this->value << ")\n"; }
};
void apply(Foo* foo1, Foo* foo2, void (Foo::*fun)()) {
(foo1->*fun)(); // call fun on the object foo1
(foo2->*fun)(); // call fun on the object foo2
}
int main() {
Foo foo1{1};
Foo foo2{2};
apply(&foo1, &foo2, &Foo::f); // 使用类的成员函数,传入两个不同的实例对象,该函数指针可以访问对象1中的数据和2的数据
apply(&foo1, &foo2, &Foo::g);
}
  • bind绑定类成员函数时,第一个参数表示对象的成员函数的指针,第二个参数表示对象的地址。
  • 必须显示的指定&Foo::print_sum,因为编译器不会将对象的成员函数隐式转换成函数指针,所以必须在Foo::print_sum前添加&;
  • 使用对象成员函数的指针时,必须要知道该指针属于哪个对象,因此第二个参数为对象的地址 &foo;

被绑定的参数为引用参数

默认情况下,bind的那些不是占位符的参数被拷贝到bind返回的可调用对象中。但是,与lambda类似,有时对有些绑定的参数希望以引用的方式传递,或是要绑定参数的类型无法拷贝。使用 标准库 ref 函数转化为 引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
ostream & print(ostream &os, const string& s, char c){
return os;
}
int main()
{
vector<string> words{"helo", "world", "this", "is", "C++11"};
ostringstream os;
char c = ' ';
for_each(words.begin(), words.end(),
[&os, c](const string & s){os << s << c;} );
cout << os.str() << endl;

ostringstream os1;
// ostream不能拷贝,若希望传递给bind一个对象,
// 而不拷贝它,就必须使用标准库提供的 ref 函数
for_each(words.begin(), words.end(),
bind(print, ref(os1), _1, c));
cout << os1.str() << endl;
}

package_task

std::packaged_task 包装一个可调用的对象,并且允许异步获取该可调用对象产生的结果,从包装可调用对象意义上来讲,std::packaged_task 与 std::function 类似,只不过 std::packaged_task 将其包装的可调用对象的执行结果传递给一个 std::future 对象(该对象通常在另外一个线程中获取 std::packaged_task 任务的执行结果)。

因此,和 function的区别

  • 功能上都可以包装可调用对象,实现都是一个类
  • 但是pack_task 可以实现异步获取对象返回结果的功能,就是在一个线程中执行可调用对象,在另一个线程中 获取该对象的执行结果,达到异步访问的目的。而 function 不行

用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <iostream>     // std::cout
#include <future> // std::packaged_task, std::future
#include <chrono> // std::chrono::seconds
#include <thread> // std::thread, std::this_thread::sleep_for

// count down taking a second for each value:
int countdown (int from, int to) {
for (int i=from; i!=to; --i) {
std::cout << i << '\n';
std::this_thread::sleep_for(std::chrono::seconds(1));
}
std::cout << "Finished!\n";
return from - to;
}

int main ()
{
std::packaged_task<int(int,int)> task(countdown); // 设置 packaged_task
std::future<int> ret = task.get_future(); // 获得与 packaged_task 共享状态相关联的 future 对象.
std::thread th(std::move(task), 10, 0); //创建一个新线程完成计数任务. 10 0 为函数的输入参数
int value = ret.get(); // 等待任务完成并获取结果.
std::cout << "The countdown lasted for " << value << " seconds.\n";
th.join(); // 要么 join 要么 detach 子线程
return 0;
}

std::packaged_task 构造函数

std::packaged_task 构造函数共有 5 中形式,不过拷贝构造已经被禁用了。下面简单地介绍一下上述几种构造函数的语义:

  1. 默认构造函数,初始化一个空的共享状态,并且该 packaged_task 对象无包装任务。
  2. 初始化一个共享状态,并且被包装任务由参数 fn 指定。
  3. 带自定义内存分配器的构造函数,与默认构造函数类似,但是使用自定义分配器来分配共享状态。
  4. 拷贝构造函数,被禁用
  5. 移动构造函数。 move 调用的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>     // std::cout
#include <utility> // std::move
#include <future> // std::packaged_task, std::future
#include <thread> // std::thread

int main ()
{
std::packaged_task<int(int)> foo; // 默认构造函数.
// 使用 lambda 表达式初始化一个 packaged_task 对象.
std::packaged_task<int(int)> bar([](int x){return x*2;});
foo = std::move(bar); // move-赋值操作,也是 C++11 中的新特性.
// 获取与 packaged_task 共享状态相关联的 future 对象.
std::future<int> ret = foo.get_future();
std::thread(std::move(foo), 10).detach(); // 产生线程,调用被包装的任务.
int value = ret.get(); // 等待任务完成并获取结果.
std::cout << "The double of 10 is " << value << ".\n";
return 0;
}

std::future

std::future 可以用来获取异步任务的结果,因此可以把它当成一种简单的线程间同步的手段。std::future 通常由某个 Provider 创建,你可以把 Provider 想象成一个异步任务的提供者,Provider 在某个线程中设置共享状态的值,与该共享状态相关联的 std::future 对象调用 get(通常在另外一个线程中) 获取该值,如果共享状态的标志不为 ready,则调用 std::future::get 会阻塞当前的调用者,直到 Provider 设置了共享状态的值(此时共享状态的标志变为 ready),std::future::get 返回异步任务的值或异常(如果发生了异常)。

一个有效(valid)的 std::future 对象通常由以下三种 Provider 创建,并和某个共享状态相关联,Provider 可以是函数或者类,他们分别是:

  • std::async 函数

  • std::packaged_task::get_future,此时 get_future为 packaged_task 的成员函数,返回即为 std::future 类

  • std::promise::get_future,get_future 为 promise 类的成员函数

  • Future更详细的用法…

C++11异步编程

主要有三个, std::asyncstd::packaged_taskstd::promise

C++11中提供的异步任务高级抽象,包含在 < future>头文件中,它能让我们方便的实现异步地执行一个耗时任务,并在需要的时候获取其结果。例如:

  1. 批量拷贝/下载文件;
  2. 进行一个复杂的计算;
  3. 执行多个嵌套的SQL查询语句;

std::promise

std::promise是一个类模板,它的作用是在不同的线程中实现数据的同步,与future结合使用,也间接实现了future在不同线程间的同步。下面的例子可以看出, promise + future 等同于 package_task,无需用户自己设置future值。相当于后者对前者进行了封装。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>
#include <future>
#include <thread>

int fun(int x, std::promise<int>& p) {
x++;
x *= 10;
p.set_value(x);
std::cout << std::this_thread::get_id() << std::endl;
return x;
}
int main()
{
std::promise<int> p;
std::future<int> fu = p.get_future(); // 并将结果返回给future
std::thread t(fun, 1, std::ref(p));
std::cout << fu.get() << std::endl; // 当promise还没有值的时候在此等待
std::cout << std::this_thread::get_id() << std::endl;
t.join();
return 0;
}

std::package_task

前面已经介绍,是个类模板,用来打包可调用对象的。相对 promise 有着更高级的封装吧

std::async

这个函数是对上面的对象的一个整合async先将可调用对象封装起来,然后将其运行结果返回到promise中,这个过程就是一个面向future的一个过程,最终通过future.get()来得到结果。它的实现方法有两种,一种是std::launch::async,这个是直接创建线程,另一种是std::launch::deferred,这个是延迟创建线程(当遇到future.get或者future.wait的时候才会创建线程),这两个参数是std::async的第一个参数,如果没有使用这个两个参数,也就是第一个参数为空的话,那么第一个参数默认为std::launch::async | std::launch::deferred,这个就不可控了,由操作系统根据当时的运行环境来确定是当前创建线程还是延迟创建线程。那么std::async的第二个参数就是可调用对象的名称,第三个参数就是可调用对象的参数。

这个函数 直接封装 了 中间对象打包过程和线程创建过程,因此封装度更高,操作的灵活性更低。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>
#include <future>
#include <thread>

int fun(int x) {
x++;
x *= 10;
std::cout << std::this_thread::get_id() << std::endl;
return x;
}

int main()
{
// std::launch::deferred 当执行到fu.get才开始创建线程
std::future<int> fu = std::async(std::launch::deferred, fun, 1);
std::cout << fu.get() << std::endl;
std::cout << std::this_thread::get_id() << std::endl;
return 0;
}