该笔记作为理解通用线程池代码的基础。包含了异步编程的方法,函数绑定的方法等…
参考链接:
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 | //代码出自链接:http://www.jellythink.com/archives/771 |
实现技巧
首先 function 的底层实现其实是一个模板类,他使用了函数签名的模板特化形式:
1 | // 默认的模板没实现 |
首先通过上面的 函数签名,就可以匹配到特定类型的函数调用,例如都是 int(int)
返回值类型为 int 输入参数为int,只不过调用形式不同,可能是 函数指针,可能是 函数,lambda函数,拟函数等等。因此 function 类内部要对不同的调用对象做一步统一封装。
首先封装出来的类需要 可执行,因此需要重载 () 操作符
考虑无论什么调用对象,它都可以归结到一个函数指针,但是不知道函数指针的具体类型,因此 需要增加一个 (void*) p 类型的函数指针,指向输入的可调用对象。
我们希望 把 所有可调用对象都执行 一次 封装,封装成 这种形式:
当function<int(int)>( f ) 执行构造函数时,此时有了 f 具体的可调用对象,那么我们可以将构造函数加上 Functor 的模板,进而编译器可以根据 实例 f 自动推断 Functor 的具体类型。
1
2
3
4void* p = f; // f 是实际输入的可调用对象 类型是 Functor 的函数指针
R invoke_functor(void*p, int args){ // 我们希望将 f 统一包装成 统一的 invoke_functor() 函数,第一个参数是可调用对象的函数指针,后面的是函数的输入参数,统一都在 call_able 函数中首先 将可调用对象的类型转换为 它原本的 Functor类型然后调用。
return static_cast<Functor>(p)(args);
}有了上面的 Functor 类型,我们可以将 不同的调用对象都封装成上述的 call_able 的形式来完成调用。多以就有如下更完整的代码
1 | template <typename R, typename... Args> |
首先看 构造函数中的 (a) 部分,根据传入构造函数中的 实际调用对象 Functor,来构造一个通用的 函数指针 。单独看
invoke_functor
函数,是个模板函数,将这个模板函数实例化后成为一个实例化后的函数指针,实例化出的函数指针也是前面说的要封装出的通用调用方式,即第一个参数为 可调用对象的指针,后面是输入参数,在函数中完成函数调用。所以invoke_f
定义的也是一个R(*)(void*, Args&&...);
类型的通用函数指针。然后 再 new 一个指向可调用对象的函数指针,并把它存为 void* 类型,做为后面调invoke_functor
的第一个参数。- 部分就是 通用函数中需要干的事儿了,首先将 指向可调用对象的 void* 类型的指针 转化为 其原本的 Functor 类型,这一步是通过
get_functor
函数 (c) 部分中使用 static_cast 完成的类型转换。
- 部分就是 通用函数中需要干的事儿了,首先将 指向可调用对象的 void* 类型的指针 转化为 其原本的 Functor 类型,这一步是通过
- 部分即为 一个强制类型转换
最终在 重载的 () 函数中,完成函数调用。
总结
总结一下实现的流程:
- 当我们只是声明一个
Function<int(int)>
类型时,还不知道具体的需要管理的 可调用对象的类型,因此内部只能先new一个 void* 指针用来指向将来需要指向的可调用对象。 - 当我们将 一个可调用对象赋值给
Func
传入拷贝或者赋值构造函数时,通过该函数的模板实参推导可以得到 该实例的类型为 Functor - 在拷贝构造/赋值构造函数中,使用
Functor
实例化一个 通用函数指针,即前面说的 第一个参数为 void 函数指针,后面为输入参数…Functor 目的是作为 通用函数中static_cast
转换的依据 - 将一个void指针指向 可调用对象
- 当要运行的时候,首先将 void* 指针传入通用函数,在函数中,根据
Functor
类型完成void*
指针到 Functor类型的类型转换,然后执行 该函数并返回计算结果。
bind函数
首先,std::bind 是标准库中新增的一个函数,std::function是一个类 ,package_task 也是一个类。可将std::bind函数看作一个通用的函数适配器,它接受一个可调用对象,生成一个新的可调用对象来“适应”原对象的参数列表。std::bind将可调用对象与其参数一起进行绑定,绑定后的结果可以使用std::function保存。std::bind主要有以下两个作用:
- 将可调用对象和其参数绑定成一个防函数;
- 只绑定部分参数,减少可调用对象传入的参数。
占位符
1 | double my_divide (double x, double y) {return x/y;} |
普通成员函数的绑定
1 | struct Foo { |
- bind绑定类成员函数时,第一个参数表示对象的成员函数的指针,第二个参数表示对象的地址。
- 必须显示的指定&Foo::print_sum,因为编译器不会将对象的成员函数隐式转换成函数指针,所以必须在Foo::print_sum前添加&;
- 使用对象成员函数的指针时,必须要知道该指针属于哪个对象,因此第二个参数为对象的地址 &foo;
被绑定的参数为引用参数
默认情况下,bind的那些不是占位符的参数被拷贝到bind返回的可调用对象中。但是,与lambda类似,有时对有些绑定的参数希望以引用的方式传递,或是要绑定参数的类型无法拷贝。使用 标准库 ref 函数转化为 引用
1 | ostream & print(ostream &os, const string& s, char c){ |
package_task
std::packaged_task 包装一个可调用的对象,并且允许异步获取该可调用对象产生的结果,从包装可调用对象意义上来讲,std::packaged_task 与 std::function 类似,只不过 std::packaged_task 将其包装的可调用对象的执行结果传递给一个 std::future 对象(该对象通常在另外一个线程中获取 std::packaged_task 任务的执行结果)。
因此,和 function的区别
- 功能上都可以包装可调用对象,实现都是一个类
- 但是pack_task 可以实现异步获取对象返回结果的功能,就是在一个线程中执行可调用对象,在另一个线程中 获取该对象的执行结果,达到异步访问的目的。而 function 不行
用法
1 |
|
std::packaged_task 构造函数
std::packaged_task 构造函数共有 5 中形式,不过拷贝构造已经被禁用了。下面简单地介绍一下上述几种构造函数的语义:
- 默认构造函数,初始化一个空的共享状态,并且该 packaged_task 对象无包装任务。
- 初始化一个共享状态,并且被包装任务由参数 fn 指定。
- 带自定义内存分配器的构造函数,与默认构造函数类似,但是使用自定义分配器来分配共享状态。
- 拷贝构造函数,被禁用。
- 移动构造函数。 move 调用的
1 |
|
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::async, std::packaged_task, std::promise
C++11中提供的异步任务高级抽象,包含在 < future>头文件中,它能让我们方便的实现异步地执行一个耗时任务,并在需要的时候获取其结果。例如:
- 批量拷贝/下载文件;
- 进行一个复杂的计算;
- 执行多个嵌套的SQL查询语句;
std::promise
std::promise是一个类模板,它的作用是在不同的线程中实现数据的同步,与future结合使用,也间接实现了future
在不同线程间的同步。下面的例子可以看出, promise + future
等同于 package_task
,无需用户自己设置future值。相当于后者对前者进行了封装。
1 |
|
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 |
|