同步Vs异步编程, 一直是大家讨论的热点。 同步编程以其逻辑简单,受到了一些对时间不敏感的程序青睐,而异步编程以其高效深受处女座程序员喜爱。
作为一只处女座程序猿,当然要异步异步异步。。。
说到异步, C++11之前, 用的最多的就是各种callback function。举个例子, 假如你是一个老板,告诉自己的秘书把文件打印出来,打完了送给我。然后你就可以干别的活,等打印好了,秘书会到你的办公室给你文件,你就可以继续处理文件。
在这里打印文件就是我们设置的callback function。 等文件打印完成这个事件发生,秘书就会带着文件给你。也就是调用你的callback function。秘书可能就是你新启动的线程。
同步编程就是你自己去打印文件,等文件打印完了再干其它的事情。
显然异步编程更有效率,但是现在有几个问题:
问题一 : 秘书怎么把打印完的文件交给你。可能现实中很简单,但是在编程中就涉及到线程之间通信,线程间通知机制,这个以后在说。
问题二 : 人类的思维是同步的,连续的,由于引入太多的callback function。这会打断人类的思维。
下面就讨论第二个问题。
第一种解决方法是最近很火的名词“协程”当然不是那个携程。GO语言把这个概念带火了,它的思想就是线程的调度开销太大,所以在线程里又起了很多轻量级的“线程”,也就是“协程”。线程自己调度,无非是存下当前的各种寄存器信息后执行其它程序,完事后在将之前存的东西在还原,继续执行, 但是C语言并不像GO语言这种天生自带“携程”,确实有很多框架比如boost::croroutine微信也开源了一套。 有了携程就可以按照人类的思维,一步一步的执行想干的活,协程的痛点是如何处理“阻塞”程序,比如socket,I/O等等。对于协程,还是持观望态度。
下面写一个小例子,看看封装后的协程写异步程序是多么爽的一件事,至于为什么爽是因为同步编程才是符合人类的思维习惯的。以前设置异步读取操作后,数据的处理都必须在回调函数中处理,现在可以直接在异步操作后接着处理啦!
#include <boost/asio.hpp>
#include <boost/asio/spawn.hpp>
#include <boost/bind.hpp>
#include <boost/shared_ptr.hpp>
#include <boost/make_shared.hpp>
#include <string>
#include <ctime>
#include <iostream>
#include <boost/enable_shared_from_this.hpp>
using namespace boost::asio;
using std::cerr;
using std::endl;
io_service io_service_;
class session : public boost::enable_shared_from_this<session>
{
public:
explicit session(ip::tcp::socket socket) : sock_(std::move(socket)),
strand_(io_service_),
uuid_(std::rand())
{
}
~session() { cerr << "~sessoin ->" << uuid_ << endl; }
void go()
{
auto self(shared_from_this());
boost::asio::spawn(strand_,
boost::bind(&session::do_echo, self, _1));
}
void do_echo(yield_context yield)
{
char data[128];
std::size_t n = sock_.async_read_some(boost::asio::buffer(data), yield);
cerr << "RECVED:【" << data << "】->" << uuid_ << endl;
std::time_t now = std::time(nullptr);
std::string time_str = std::ctime(&now);
async_write(sock_, buffer(time_str), yield);
sock_.shutdown(ip::tcp::socket::shutdown_send);
}
private:
ip::tcp::socket sock_;
io_service::strand strand_;
std::size_t uuid_;
};
void start_accept(yield_context yield)
{
ip::tcp::acceptor acceptor(io_service_, ip::tcp::endpoint(ip::tcp::v4(), 2016));
for (;;)
{
boost::system::error_code ec;
ip::tcp::socket socket(io_service_);
acceptor.async_accept(socket, yield[ec]);
if (!ec)
boost::make_shared<session>(std::move(socket))->go();
}
}
int main(int argc, char *argv[])
{
boost::asio::spawn(io_service_, start_accept);
io_service_.run();
}
编译命令g++ -std=c++11 test.cpp -lboost_system -lboost_coroutine -lboost_context -o test
其实,感觉现实中协程更多的是对编程方式的改变,对控制流的操控可以用同步的结构写出异步的效果,但是协程是用户态的而不是原生的多线程,所以并不能并行执行提高并发率。但是协程能够在各个协程间进行高效的切换,这一点可以做到比传统依赖于异步调度的效率更高,这才体现出协作的本质吧!
第二种:
C++11中引入了一个新的东东 std::future,有个这个就可以让你设置一个工作,然后在需要结果时调用get()拿到你想要的值。
前面说了,人类的思维是连续的,同步的。所以人类更适应连续的链式调用,比如告诉秘书打印一份文件然后把文件交给XXX。
也就是future .then()
可是令人失望的是future.then()作为候选并没有进入C++17,而是进入了std::experimental。
https://en.cppreference.com/w/cpp/experimental/future/then
不过还好我们有boost这个神器。
下面我们尝试一下:
#include <iostream>
#include <string>
#define BOOST_THREAD_VERSION 4
#define BOOST_THREAD_PROVIDES_FUTURE
#define BOOST_THREAD_PROVIDES_FUTURE_CONTINUATION
#include <boost/thread/future.hpp>
#include <stdio.h>
using namespace boost;
int main()
{
future<int> f1 = async([]() { return 123; });
future<std::string> f2 = f1.then([](future<int> f) {
std::cout << f.get() << std::endl; // here .get() won't block
return std::string("sgfsdfs");
})
.then([](future<std::string> f) {
std::cout << f.get() << std::endl;
return std::string("sgfsdfs");
})
.then([](future<std::string> f) {
std::cout << f.get() << std::endl;
return std::string("sgfsdfs");
});
getchar();
}
这里要吐槽一下, 首先编译命令g++ then.cpp -std=c++11 -lboost_system -lboost_thread
官方网站只是写了一些例子,完全不能编译, 首先你要设置版本信息,要不然编译不过#define BOOST_THREAD_VERSION 4
应该2以上就行,还要定义一些宏#define BOOST_THREAD_PROVIDES_FUTURE
要用then还要定义下边的宏
#define BOOST_THREAD_PROVIDES_FUTURE_CONTINUATION
编译问题解决后,是不是很爽?完全不用考虑是哪个线程干的工作, 只要then then then,活就干完了。
通过GDB 看了一下运行流程,发现每个then都会起一个新的线程。 首先起线程很耗资源。其次还有个线程安全的问题。所以这种task适合task之间没有依赖的工作。
为了解决这个问题很多大厂已经有相应的解决方案,例如微软的PPL
https://msdn.microsoft.com/en-us/library/dd492418.aspx
在linux下阉割版pplx
https://github.com/Microsoft/cpprestsdk/wiki/Programming-with-Tasks
intel有TBB
https://www.threadingbuildingblocks.org/tutorial-intel-tbb-task-based-programming
他们都有完善的task schedule 功能。但是都不是标准的实现。
现在我的想法是实现和std::promise&&std::future类似的功能,future有then也就是有continuation功能。
同时为了解决线程间数数据问题,将scheule功能用boost::asio实现,所有task都schedule到一个thread(后续到多个thread类似boost::srand)
有机会实现这个想法。
完