前言
在异步编程实践中,曾经遇到的最大的问题就是thread safe 问题。
我所在的团队是Database团队,主要是为APP提供和redis交互的API。场景就是application thread调用我们的API。我们有个worker thread负责网络I/O。我们的API基于hiredis,hiredis声明自己不是thread-safe。
这也很正常,看了看hireids的代码,关于网络通信的部分,其中有个i-buffer和一个o-buffer这两个buffer负责缓存和网络通信的信息。hiredis将“poll”这部分做了抽象,可以兼容很多异步框架比如redis的ae,libevent等等。hiredis的做法是当网络可写的时候,调用send,如果send没有将o-buf发送完全时,调用eladdwrite,告诉系统,网络可写时告诉我。
那么为什么它不是线程安全的呢? 首先如果要做到线程安全,这两个buffer就需要“加锁”来防止两个线程通知操作相同的buffer,但是hiredis中没有相应的处理代码
既然hiredis没有提供线程安全的API,那么如何解决这个问题呢?
异步事件通知机制
异步编程无法避免都要面对多个线程,这个就要涉及到线程之间的通信。解决方法有很多,比如上篇文章中提到过的boost furure-then, 但是这样的缺点是每个then多会新建一个线程处理task,起线程会很耗资源。所以有人就会想,一开始就建立多个线程形成一个线程池,这样就消除了不断起线程的损耗。这确实是一个好办法,但是这也引入一个问题–如何拖住这个线程,不让线程退出。解决方法同样有很多。比如用condition wait,poll…
至此就正式引出异步事件通知机制。异步事件通知机制有很多种类,其大多数都是基于epoll kqueue等linux底层API。下面我就举几个比较著名的框架:
libevent
libevent是我最早接触的此类框架,其支持linux各种file discriptor(fd),之前写了一个小demo。link:https://github.com/maxcong001/task_base
想法就是运用libevent作为poll机制,监听eventfd。APP线程当有事情的时候就将事情push到一个queue中(加锁),同时eventfd加一,这样libeventthread就会被唤醒,看看有多少事件需要处理,并从queue中提取相应的事情并处理。
libuv
libuv和libevent相似,相比libevent更加轻量,同时实现稍有不同,同样我写了一个小的demo,link:https://github.com/maxcong001/task_base_libuv
这里libuv支持uv_async_send这样的API不用我们再去监听fd。同时这个小demo中还用的线程安全的无锁queue(以后会写有关无锁队列的文章,这里就不介绍了)。
boost::asio::io_service(io_context 新版本boost)
boost这个神器总不会让人失望,io_service抽象了底层I/O, 所有异步io事件都是通过它来分发处理的,当然也包括我们需要的异步事件通知机制,主要我们可以调用post方法, post用于发布io事件,如timer,socket读写等,一般由asio框架相应对象调用,无需我们显式调用。新版本的io_context 的post直接可以带handler。本来写了个小程序,由于种种原因不能在公司之外分享。
异步事件数据的线程安全
两个线程之间互动,不可避免的是共享一些数据结构,上一个章节,讲到libevent时,提到了一个queue,这里存着要处理的任务,由于可能有多个线程操作这个queue,为了解决这个问题,简单粗暴的加锁。
众所周知加锁是一个很耗的操作。同时等待锁也很绝望,用不好还会产生死锁
上一章节讲到libev时就升级了一下用了无锁queue
当讲到asio时候更进一步。这里我们可以使用lambda函数捕获一些数据,关于捕获,C++11以后我们会谈更多
异步事件结果的返回
对于结果的返回,有两种例子,第一种就是结果在当前线程中直接处理,第二种就是将结果传回发起task的线程
传回发起task的线程
如果两个线程都是消息驱动的, 可以发一个消息给原线程。
如果只有一个线程是消息驱动的,可以参考future.get()。当需要用某个变量时,调用get,但这可能会阻塞。
在当前线程处理
这就涉及到一个编程思想的问题。如何将一个task做到thread不感知。