golang 在中国大火的同时,也带火了另外一个概念,协程。个人认为协程之所以会火,其中一个原因就是协程在发生阻塞时能够主动把释放资源给其他协程去使用,在等待的资源满足后能够被重新调度回来,从而保证了机器资源的高效使用,同时使得开发者只要用编写同步代码就能实现异步编程的效果,避免了代码的支离破碎和call back hell。这固然是很美好的,但是在没有协程库的情况下,我们要怎样实现高性能的编程呢?一般的做法就是用线程池+任务回调。线程池的实现大家自行点链接查看,这里我们重点说说回调怎样做。
1、函数指针
用函数指针来实现函数回调应该是大多数同学马上想到的做法了,毕竟指针作为c/c++最强大的功能,就连linux内核中也大量用到了函数指针来实现回调。由于函数指针的用法其实相当简单,所以这里我们直接代码好了
#include <cstdio>
//第一步,声明函数指针。下面是声明一个函数指针,这个函数指针的类型名叫func,它指向的函数返回值为void,调用的参数为int
typedef void(*func)(int);
void Print(int i ){
printf("%d\n", i);
}
int main () {
// 第二步,定义一个函数指针,并且指向一个符合函数指针声明的函数
func f = &Print;
f(1); //调用函数
return 0;
}
是不是很简单?如果想加点面向对象的思想,我们可以多定义一个结构体,然后把回调的函数指针和调用的参数给封装起来,看下面代码
typedef void(*func)(int);
struct MyStruct{
int param;
func callback;
}:
嗯……是不是很熟悉的配方呢?这就是c++的类的套路嘛。
2、使用多态
函数指针用起来固然简单,但是对于c++程序员而言,它并不完美,因为它无法指向成员函数。当然,我们可以把要回调的函数声明为static。不过既然都用到了类封装,那就坚持用到底好了。那我们要的回调又要怎样实现呢?没错,用多态。我们可以定义一个只包含若干个纯虚函数的接口类(这种类在java中叫Interface,我觉得这称呼会更加合理),然后每个具体类都继承这个接口类并且把这个类的指针传给回调者,回调者在回调的时候就能因为多态的特性实现调用对应的实现了。如下
#include <iostream>
class Interface{
public:
virtual void do_something() = 0;
virtual ~Interface(){}
};
class Impl : public Interface {
public:
virtual ~Impl(){}
virtual void do_something(){
std::cout <<"Impl::dosomething" << std::endl;
}
};
int main(){
Interface *f = new Impl();
f->do_something();
return 0;
}
当然了,使用多态是必须用引用或者指针的,而如果为了使用多态而满天的裸指针,其实也会有点得不偿失,所幸的是c++11为我们提供了shared_ptr,大家了解下,能够极大得解放被指针所困的人们。
3、bind和function
其实介绍完上面两种方法之后,对大部分同学的需求而言已经是功德完满的了。不过如果还是有同学说不想用函数指针,也不愿继承一个基类导致在业务意义上变得奇怪,那就只能够祭出c++11为我们带来的另外一个很好的组件bind和function了。
先说function吧。简单来说,function的功能和函数指针类似,都是可以通过改变指向的对象来实现动态修改函数的行为。区别在于函数指针只能针对共有的非成员函数,而function则可以用于调用c++中的各种可调用对象。那什么是可调用对象呢?简单地说就是能够通过()来调用的对象,包括函数,functor还有c++11的lambda表达式。其用法也相当的简单,拿回上面函数指针的例子,我们只要修改一行代码就能把函数指针改成function调用了,如下
#include <cstdio>
#include <functional>
//第一步,声明函数指针。下面是声明一个函数指针,这个函数指针的类型名叫func,它指向的函数返回值为void,调用的参数为int
//typedef void(*func)(int);
typedef std::function< void(int)> func;
void Print(int i ){
printf("%d\n", i);
}
int main () {
// 第二步,定义一个函数指针,并且指向一个符合函数指针声明的函数
func f = &Print;
f(1); //调用函数
return 0;
}
上面只是用function去调用普通函数,那调用成员函数呢?嗯,先别急,逐个来说。在c++11标准中,function其实并不能直接调用成员函数的,因此需要借助下外力,至于手段其实有两种方式的,第一种比较原始的做法,就是把类变成一个functor,然后在operator()的重载中去调用成员函数。例如
#include <cstdio>
#include <functional>
class Foo{
public:
void operator()(int i){
return bar(i);
}
void bar(int i){
printf("%d\n", i);
}
};
int main () {
std::function<void(int)> f=Foo();
f(2);
return 0;
}
上面说了function只能接受可调用对象,所以上面这样把类变成functor这种做法其实就是把类变成一个可调用对象,但是如果一个类有多个成员函数需要被回调的话,这个山寨的做法就没用了,那怎么办呢?这时候就是第二个组件bind出场的时候了。总的来说,bind的作用就是构造出一个新的可调用对象出来,为了容易理解,我们先看看bind的定义
template< class F, class... Args >
/*unspecified*/ bind( F&& f, Args&&... args );
/*
f - 可调用 (Callable) 对象(函数对象、指向函数指针、到函数引用、指向成员函数指针或指向数据成员指针)
args - 要绑定的参数列表,未绑定参数为命名空间 std::placeholders 的占位符 _1, _2, _3... 所替换
调用这个函数等价于绑定 ars 到 f 中,作为f的调用参数。需要注意的是,bind 的参数只是复制或移动,
如果需要传引用,需要使用 std::ref 或者 std::cref 来包装
*/
下面给出bind调用普通函数和成员函数的例子来,比较简单,大家一看就能明白了
#include <iostream>
#include <memory>
#include <functional>
class Foo
{
public:
void bar(int a){
std::cout << "Foo::bar " << a << std::endl;
}
};
int bar(int a, int b)
{
std::cout << "bar " << a+b << std::endl;
return a + b;
}
int main(){
int b = 1;
std::bind(bar, 5, std::placeholders::_1)(b); // 等于执行了 bar(5, b)
Foo x;
std::shared_ptr<Foo> p(new Foo);
int i = 5;
std::bind(&Foo::bar, std::ref(x), std::placeholders::_1)(i); // 相当于执行了 x.bar(i)
std::bind(&Foo::bar, &x, std::placeholders::_1)(i); // 相当于执行了(&x)->bar(i)
std::bind(&Foo::bar, x, std::placeholders::_1)(i); // 复制x,并执行(复制的x).bar(i)
std::bind(&Foo::bar, p, std::placeholders::_1)(i); // 复制智能指针p,并执行(复制的p)->bar(i)
return 0;
}
到了这里,大家应该能够想到把function和bind结合起来调用成员函数的办法了
#include <iostream>
#include <cstdio>
#include <memory>
#include <functional>
class Foo{
public:
void bar(int i){
printf("%d\n", i);
}
};
int main () {
Foo foo;
std::function<void(int)> f = std::bind( &Foo::bar, &foo, std::placeholders::_1 );
f(3);
return 0;
}
到这里,我们用孔乙己纠结回字有多少种写法到态度把c++实现回调的几种常见的做法给过了一遍,当然还有c++11提供的lambda表达式这里没有提到,大家可以自行学习下。
## 关于协程的争论
在文章开头我用golang作为引子来起文章,结果引起了同学们的争论,我对golang的研究有限,另外,我也不是科学家,不想太去深究各位同学提到的grenn thread和goroutine还有协程之间的区别,或者说说有栈协程和无栈协程的区别,这对于做研究而言固然不够严谨,但是对于写代码而言,我个人认为知道线程和协程的根本区别已经足够了。至于线程和协程的根本区别,我的看法是资源控制的主动让出和恢复机制。线程的调度是由则是内核调度的,除非被内核中断了,否则不会主动让出资源,所以当需要等待某些资源时,线程只能被阻塞着,因此如果不想一直起新的线程,在资源等待比较多的场景下,一般会结合异步回调来使用,这也是为什么我会写本文的原因;而协程则是由语言自身语法或者库提供主动中断当接口来完成的调度。所以只要满足这个特性的,我一律归类为协程,不区分是有栈还是无栈。喜欢深究协程这个概念是否符合标准的同学就不用再纠结我给了一个不那么严谨的说法出来了,权当我故意抛出个引子来引起大家的讨论吧。