-
在日本考驾照
最近最终把日本驾照考出来了,花了整整五个月。考出来的时候决定一定要写点什么,一方面是记录,另一方面也可以作为其他人的参考。 先说一下,我在国内有驾照,MT,手动挡。在日本重新考一个驾照是考虑到自己距离上一次开车时间很久,自己在国内学完之后也只开过半年,还只能算初学者。而且日本的交通规则和国内的很不一样,日本是少数三个靠左行驶的国家之一。 一部分人可能会选择外国驾照转日本驾照,那你可能需要到驾照考试中心(運転免許試験センター)去考一次。我自己没有经历过,从考试内容来看类似日本“仮免試験”。只知道靠左行驶是不够的,标牌的意思,十字路口的优先顺序都很重要,所以考试前你至少需要几个课时的实际训练以及相关的交通知识。 有些人可能会有国际驾照,日本的法律规定国际驾照只能用一年,之后你要么不开车,要么想办法弄个本地驾照。 我花了五个月学开车,主要是因为上班没时间。大大小小总共六次考试全部顺利通过也花了那么多时间。我以前在国内学的驾照有四次考试 科目一(笔试) 科目二(车上,考场内) 科目三(车上,考场外) 科目四(笔试) 我在日本这边的驾校一路下来有六次考试 仮免前効果測定(学科) 修了検定(実技、仮免試験) 仮免学科試験 卒検前効果測定(学科) 卒業検定(実技) 学科試験(試験センター)
-
英语学习笔记
从去年年初开始重拾英语差不多有一年半多了。最近一两个月开始上在线英语对话,训练自己的口语。现在处于可以开口讲,但是词汇和表达不够的状态。我想分享从几个月前没开口讲到现在能开始讲的期间内自己感受到的东西。 在线英语对话 是一个很好的trigger。在线英语对话比自己对着镜子讲,找非native的朋友讲都要好。原因是你必须讲英语,你在一个受控的环境中。你听得到对方的问题,想要回答对方,用英语回答,这一连串的过程是你以前没有的经验。换句话说,在线英语对话可以帮助你训练思维回路,达到相对自然回答的状态。 回答的范例以及and/so 不是说有了在线英语对话,你就肯定能流利地开始讲英语了。我大概在一个月左右的时候感觉自己可以比以前自由地回答了。一个特点是流畅度比以前有所提升,另外一个特点是不过度使用and/so之类的连词。严格来说,我不认为这是在线英语对话的效果。在这一个月内,我买了一本《SPEAKING FOR IELTS》的书,并且开始上第一课。IELTS Speaking考试的Part 1其实和在线英语对话很像,就是闲聊。重点是书中提供了范例答案。 自己一个人学习时最大的问题是,你没有参照,你不知道对错。我发现范例答案中有and/so之类的连词,但是用法不太一样。不过度使用是一方面,不怎么用可能更贴切一点。口语不是写作,不需要特别明确的连词来指出关系。范例中递进也好,并列也好,其实不需要连词也可以理解。而且你可能会发现范例的主语,包括形式主语会有变化,这可能是英语表达的习惯,也是你必须找实际范例才能看出来和理解的东西。电影,书籍包括新闻都不会很明显地体现这些特点,但是各种口语考试的答案是很好的参考。
-
租房还是买房
最近因为某些原因看了很多买房的资料,但是最终放弃买房决定租房。整个过程中了解了很多买房和租房的知识,现在分享出来希望对各位有用。 首先,我希望站立在一个你和你的另一半能够自由选择的角度看待这个标题。因为丈母娘等原因必须买房的情况不在讨论范围内,这种属于社会属性大于实际居住。另外,一个人居住的话选择面比较广,这里的讨论不一定适合你。 买房和租房哪个更好,如果你尝试搜索的话,会找到很多比较的文章。比较买房和租房各自的好处和坏处。但是同时也有各种外部因素影响你。比如说,不知道是不是国人受丈母娘要求买房买车的影响比较大,到了国外也有优先买房的倾向。还有一些租房的钱不如去买房的说法。整体来说,想要理性分析还是有点困难的。但是如果你把租房当作后备选择,把买房作为主要判断对象的话,决定起来会相对容易一些。 是否需要买房,可以从多个角度来看,比如说 买房有什么好处 因为什么买房 你的计划是什么
-
买了保险
2021年已经过去1/4,在这3个月时间内做了好几件事情,其中一件就是买了保险。 保险对于我们这一代人来说可能没有什么好的印象,不要花钱在保险上,甚至保险等于骗钱。不过,对于大部人一般人来说,保险是少数可以保障我们未来的手段。 死亡保险和医疗保险是两个比较重要的保险。 一般来说,到子女独立之前死亡的话,对于家庭的影响比较大,为了避免意外加一个死亡保险比较好。另外,如果你有住房贷款的话,在日本一般会加入“団体信用生命保険”,死亡之后贷款消失,所以住房贷款可以和死亡保险分开考虑。 日本的死亡保险主要有两种,终身和定期。定期的死亡保险到一定时间会自动停止保障。你可能会认为,到时候你没有挂的话,你付的钱全都归保险公司。事实确实如此,不过你要从保险的本质上来看,保险不是赚钱,保险是出意外时的保障。终身一般会比定期贵很多,但是解约时会返钱给你(定期一般不会)。定期里面还分收入保障型和一般意义上的定期。前者是每个月付款,后者是一次性付款。一般每个月付款的类型相对便宜,后者会贵一些。上面只是一般意义上的分类,具体到产品的时候会有不同,比如说有终身和定期混合的产品等等。
-
理想的网络程序架构
在写完自己的Raft算法实现xraft之后,对自己欠缺并发和网络程序方面的知识非常有痛感。之后自己找了很多并发程序方面的资料,比如《多核编程艺术》,相对系统性地学习了并发数据结构的知识。其中比较中意STM(Software Transaction Memory)技术,因为实际的程序中对于共享数据的访问模式不会像典型并发数据结构那么简单,这时候,拆分数据,并且用STM来访问的话可以间接解决并发访问的问题。 到此为止,我一直认为自己是并发程序方面有所欠缺。转变发生在今年年初,自己看到别人写的一篇实现Bittorrent Client的文章,自己也想实现一下。个人在很早以前,就对实现Bittorrent Client很感兴趣,但是当时自己能力只够解析Torrent文件。 在实现Bittorrent Client时,自己有意识地把能够并行执行的代码并行化,并且尝试使用Kotlin的coroutine来写。使用Kotlin的coroutine而不是现在貌似很火的Golang的goroutine来写主要还是因为自己不喜欢Golang的运行模式。Golang的runtime决定了goroutine怎么执行,用户无法直接介入。个人认为应用程序需要一种flexible的机制,能够根据工作负载决定使用基于线程的还是基于work-stealing的coroutine的运行模型。Golang帮你做了决定,在错误处理上也帮你做了决定,有强烈的设计者(C语言)的风格。 回到主题,相比之下,Kotlin的coroutine提供了自定义运行模式的方法,我可以强制要求某些coroutine只在某个线程中之行,利用thread confinement避开锁或者其他同步机制。而且Kotlin的coroutine提供了结构化的coroutine,能够简化复杂的异步编程场景。比如说,我先异步访问服务A,结束之后在并行访问多个服务B,全部完成后再访问服务C,Kotlin里面不需要额外的同步类,只需要使用coroutineScope,launch,async/await就可以实现。整体来说,Kotlin的coroutine非常适合复杂的业务场景,用一个类似DSL的方式类型安全地编写异步/并行编程的代码。 不过我在用Kotlin的coroutine编码的时候,发现一个问题,准确来说这和coroutine没有关系,是你的网络程序该用什么架构?习惯了Web后端的线程模型,NodeJS的单线程模型,UI程序的单事件模型之后,对于网络程序,感觉没有一个确定或者通用的模型。coroutine是一个模型么?个人认为coroutine不是一个模型,coroutine准确来说是一种实现,运行模式的一种实现。你可以用基于线程的运行模式,也可以使用coroutine+work stealing的运行模式,这只是两种不同的实现。对于网络程序来说,你的工作负载决定了你适合哪种运行模式。 一般人可能会按照IO多,还是CPU多来分。很明显CPU工作多的程序不希望频繁切换CPU,而IO非常多的则可以在IO等待时运行其他任务避免浪费CPU。现实中的程序,往往是两者兼具的。在定性你的程序之前,另外一个注意的是,运行模式会影响到你的并行程序的写法。程序调度、共享数据访问、线程管理其实是互相关联的,不只是网络程序,非单线程程序都需要考虑这个问题。 之前也提到通过thread confinement可以避免使用锁,这其实就是一个通过程序调度解决共享数据访问的例子。假如一个语言把程序调度和线程管理都帮你做掉了,那么你必须自己解决剩下的共享数据访问问题。Golang提供了锁和间接解决问题的工具channel,Erlang禁止共享数据访问并且提供了Actor方案。 语言提供的解决方案,往往存在可以优化的空间。比如说我有一个只有单个线程会更新的数据,由于其他线程只会读,所以不需要锁,也不需要发送消息。从另一个角度来说,基于通信的方案可以解决更复杂的问题,但是简单的场景可以使用更简单和高效的方案。另外,基于通信的方案对于一次要求多个数据更新的场景支持并不好,有些人会退回到粗粒度锁,但更好的方案是STM等。 总体来说,网络程序也好,一般的多线程程序也好,需要一个灵活的、能够最大化性能,以及解决共享数据访问的整体方案。个人在尝试了多种方案之后,觉得类似Actor,或者说从SEDA(staged event driven architecture)出发的方案比较合适。SEDA把工作负载按照步骤切分,提供了并行执行的方向,间接地解决了共享数据的问题。你可以把SEDA中的Stage等同于Actor,只不过Erlang用coroutine来运行Actor。这里面的重点不是coroutine,而是Actor在收到消息之后,顺序执行的这点。所以Actor是类似Process的东西,而在Process中执行的指令满足process confinement,理论上不会出现多线程访问下的数据竞争问题。其次Actor额外还有父子结构,自我替换,异常时的处理策略等等。其中父子结构模仿了进程的父子进程,可以用于动态子任务生成,与父Actor在不同逻辑线程上执行,提高并行度。 从解决方案的完整度上来看,Actor确实可以作为一个参考架构。为了进一步提高性能,你可以放宽Actor要求,允许Actor收到消息后乱序执行(或者说没有Queue)。还有就是Erlang是一门函数式语言,只有不可变的变量,假如你用其他语言,可以使用语言本身的内存模型来小心地共享部分变量,减少Actor之间的消息。 个人其实也是这么做的,在几次重写之后,用Kotlin写了一个简单的Actor运行时,使用Actor架构的变体来同时解决并发数据访问和运行模式的选择。整体上来说,不用考虑背后是用线程还是coroutine,并发数据访问也很容易,应用层面不需要锁。到现在为止,应该算是我个人认为比较理想的网络程序架构了。
-
使用RxJava把你的异步调用同步化
每周的定期博客。 本周个人认为的亮点之一是在测试中使用RxJava,把异步调用同步化。让代码变得“清晰”了许多。 如果你使用基于异步的编程方式的话,肯定会碰到一个如何测试的问题。最简单的比如说 AsyncResult<String> foo = asyncService.foo(); 或者基于回调方式的异步代码 asyncSerivce.foo(asyncResult -> { // do stuff }); 前一种还好,后一种在多个异步调用时很容易碰到callback hell的问题。 A.foo(a -> { a.bar(b -> { b.baz(c -> { }); }); }); 比如说上面这种嵌套比较深的代码。一种解决方法是类似JavaScript的Promise的链式调用。 A.foo() .andThen(a -> a.bar()) .andThen(b -> b.baz()) 但是链式调用的一个问题是,在方法调用之间是非简单类型的依赖关系时,你需要一些中间类,而且调用顺序有所变化时,中间类也必须随之改变。比如说上述代码中bar依赖A,baz依赖A与B时 A.foo() .andThen(a -> a.bar().success(b -> (a, b)) .andThen((a, b) -> a.baz(b)) 相比之前的写法会显得繁琐。对此,你可以改用JavaScript的async/await,完全把代码改成同步风格,免去些中间类的需要。 async/await是协程的一种实现,但是Java没有协程,该怎么办?方法很简单,利用类似Future的get把代码同步化就可以了。特别是在测试代码中,异步代码转开成同步代码并没有太大问题。相反的,同步风格的测试代码更好理解。 于是在测试代码中可以这么做 把回调式代码转为Observable事件源 使用Observable的blockGet,blockAwait转换为同步代码 Observable<A>…
-
尝试使用GTD
2020年初,个人开始尝试一种新的日常任务管理的方式:GTD(Getting Things Done)。对于我来说,GTD其实并不算一个新的事物,几年前就尝试过,当时还用了一个以GTD为卖点的软件,软件的名字有点忘记了,只记得使用GTD管理事务有种束手束脚的感觉,以致于最后放弃了。 重新拿起GTD,一方面是因为自己一直使用的RTM(Remember The Milk)在2019年有点荒废,毕竟自己花了钱的,不用有点浪费,另一方面,觉得自己平时管理任务的方式需要改进。个人之前就了解过与一些任务管理相关的方法论,比如说在做计划前把自己心里想的事情全部写出来,给自己减压之类。还有任务最好按照S.M.A.R.T原则进行细化、量化等。这些方法本来没有问题,但是只能算一部分的解决方案,没有成为一个系统的方法论。 于是乎找了GTD相关资料。GTD有书,比如说无压工作之类的。个人按照一篇日文的读写笔记学习了GTD的主要方法论。 个人认为GTD的一个核心,是写下任务之后,怎么处理这个任务。这里说的处理不是“完成”任务,而是怎么分类,怎么列出,怎么管理的问题。刚才提到做计划前把自己心里想的事情都写出来。在GTD中,这些事情都被写入一个叫做Inbox的任务列表中。Inbox类似邮箱,是一个任务第一个被放入的地方。不管你在做计划前,还是突发奇想,心血来潮,在你忘记之前,请写下你的任务。 把你心里所想的东西全部输出后,接下来逐个处理这些任务。如果你在处理过程中发现新的任务,原则上请加到Inbox最后,之后再处理。从Inbox中处理这些任务时,首先判断是否可以立刻行动,如果2分钟内能解决的任务,就立刻行动。你可以想象,假如大部分任务都是2分钟内能完成的话,那么你的Inbox的任务就是一个普通的任务列表,从上到下一件件处理。 如果不是2分钟内能解决的,你需要判断这个任务是不是比较复杂的任务。比较复杂的任务指的是需要多个步骤才能完成的任务,也就是判断是不是类似S.M.A.R.T里所说的足够小,可以实施的任务。比较复杂的任务被放到“项目”的列表中,不算复杂的任务被放到“待办任务”。把复杂任务和普通任务分开之后,你可以直接从待办任务列表中接着处理你要的任务,而不是卡在某个没有被分解的大任务上。 在分发任务的时候,除了待办任务列表和项目列表之外,还有直接丢弃,之后会做的任务列表以及委派给别人的多种方式。在处理Inbox里的任务时,实际上已经把任务进行了分类。我一开始是按照主题,或者说内容分类,比如学习,个人,工作等。这样有一个问题,就是你不知道哪些是接下来要做的任务。在GTD中,你直接有一个待办任务列表,你只需要关注这个任务列表就行了。 对于项目列表中的任务来说,你需要分解并把其中某个子任务加入待办列表中。如果你没法分解,这个任务就不是项目。如果你加入不了待办列表,说明你这个任务现在还不是时候,你应该放到之后会做的任务列表中。这个之后会做的任务列表,作用是把存放你暂时不想做,或者需要时间孵化的任务。那些某天你要做什么的事情,你需要使用日历,而不是任务列表。日历上精确了哪天你需要做什么事情,个人理解任务列表上的任务倾向于没有明显的执行日期。 在分类和分解任务之后,你不会有“我忘记了什么”,“接下来该做什么”的疑问,因为答案就在你的面前。当你有新的想法或者任务之后,把任务加入Inbox,然后按照上面的步骤再执行一次。如果你严格按照上面的步骤执行的话,你不会感觉有压力(记忆上的,工作上的等),因为你把接下来要做什么开始管理起来了。 为了更好地管理你的任务,你可以尝试给你的待办任务,加上tag,这些tag可以是表明任务执行的场景,或者某种关键字,然后可以进一步分类任务,并按照场景筛选任务,按部就班地完成任务。另一方面,你需要定期清理你的任务。比如说个人感觉待办任务不能太多,虽然可能没有结束时间,但是仍旧会给人一种压力。除了常见的优先级,场景分类之外,将任务数保持在一定数量可能更好。在之后再做的任务列表中的任务,如果自己多次判断不会做的话建议直接删除。 GTD的方法老实说,并没有特别依赖TODO工具的功能,你可以用一个纸质的笔记本完成上述的流程。当然,有功能更强大一些的TODO工具肯定更好。比如说,我在Remember The Milk里面,使用Smart List代替普通的List筛选待办列表的任务。筛选的条件如下 1.待办列表里的Action 2.项目列表中的任务的子任务,并且带有next-action这个tag 满足上面一个条件的任务即可显示在Smart List版的待办任务中。这样就不用从项目列表移动任务到待办列表,而且你点击子任务可以看到关联的项目任务。这种列表+tag的管理方式比单纯列表的方式要灵活一些。当然你不能忘记你的目的是完成这些任务,任务管理的目的是没有心理负担并且快速地列出你接下来要做的事情。 最后,虽然个人还调整自己管理任务的方式,但是核心的内容没有变化,并且GTD让我能够没有压力地Getting Things Done。如果你觉得自己被各种事情搞得焦头烂额的话,建议试一下GTD。
-
2019年技术小节
转眼之间2019年已经过去,2020年已经到来。在还未正式开始上班之前,个人想小结一下2019年自己技术相关的学习与感想。 2018年下半年的时候,突然奇想开始实现raft算法之后,个人感觉自己的技术视野一下子拓展了开来。特别是之后补充了学习自己不擅长的多核编程,逐渐看得懂Java并发库中的实现,并撰写了一些分析的文章。个人觉得并发算法如果你只是去看代码,你很难一下子理解代码在做什么,你需要循序渐进地分析和理解,在这其中自己尝试去实现最有效果。 因为觉得自己的这些文章,可以分享到自己一直看的《开发者头条》上,所以就尝试了放了几篇,比如说有关ReentrantLock的三篇实现分析。很幸运,几乎都出现在了次日的精选文章上。之后有图书编辑联系我是否有兴趣写书,于是我花了4个多月写了300多页的书。现在书还在编辑中,内容暂时不能公开。不过对于我来说,是一个很好的输出自己技术能力的一个机会。 写书的同时,其实我仍旧在寻找和尝试更好的实现raft算法的技术。这期间,学习了C++11下的多线程编码,以及尝试了Golang和Rust。从结论上来说,个人都不是很满意。C++的缺点比较明显,难。新版本的C++就是不断加新语法,而且是在没有简单易懂的依赖库机制下。写C++仍旧是必须从零开始的感觉,对个人开发来说很花时间。 相比C++,Rust要好很多,有crate,有标准的测试,有编译器保证你的代码不出现C/C++各种奇怪的问题。但是Rust的Lifecycle机制导致并发算法的代码很难写,越是复杂的代码越是容易编译不过。如果与编译器做斗争的话,我还不如退一步写C++,因为我知道我在做什么。个人知道Rust仍旧在发展,或许将来可以满足我的要求。 Golang是一个看起来不错,但是实际深入之后放弃的语言。我对Golang的协程很有兴趣,查了很多资料,也基本理解了协程的目的、做法以及长处短处。协程的短处是一方面,另一个让我放弃的原因是Golang在Memory Model的部分,文档第一句话是把程序员当笨蛋的赶脚(虽然Golang明显就是为那些入了Google但是不精通C++的人开发的),所谓的Memory Model也只是介绍了Golang提供的并发工具,给人的感觉就是C语言的翻版(你可以想想Google的C++ Guideline里尽量不用C++特有的功能,而是与C兼容的部分)。综合考虑了一下,个人项目里不是很想使用这种矫枉过正的语言。如果工作上要用,则是另外一回事情。 编程语言方面,2019年个人学的Kotlin可能是最好的了,在Java上做加法,特别是extension function,property的delegate等等(当然,也有协程)。个人觉得,比起解决语言痛点(C++对象生命周期)和大肆做减法(Golang对C++的做法),解决业务上的痛点和引入其他语言优秀的语法(C#的property delegate)可能更好。因为构建一个不太过严格但是又不是太过宽容的模型很难,Rust的生命周期管理难以处理复杂场景,Golang的全面协程化导致运行模型的不灵活。老实说,个人觉得这里面也有这两个都是命令语言的原因,如果是函数式语言,使用起来不会有这些问题。 说到函数式编程语言,不得不说现在编程语言不断在融入来自函数式语言的概念,从Scala到Java的lambda,从C++的lambda到Rust天生支持函数式。函数式编程语言的思维方式除了可以简化遍历,还有数据有无(Option/Some/None)和专注于正常流程的异常处理方式(Golang的C语言处理方式绝对是一个反例)。有空学习一些函数式语言的内容将来肯定会有一些收获,毕竟将来多范式编程语言会原来越多。 另一个和函数式编程语言相关的是ReactiveX,个人在2019年年末的时候花了两个星期的时间重新学习了一下(以前学过一次,太难放弃了)。个人觉得如ReactiveX所说,ReactiveX编程方式确实可以作为程序的主要处理方式,而不是命令式语言的从上到下的处理方式。这里面有ReactiveX所对应的异步编程的原因,也有命令式语言在复杂问题上的抽象不足(比如Golang的协程无法处理同时IO处理,必须退化为CSP编程方式)。顺便说一句,ReactiveX一开始来自C#,然后发展到多语言。这其实说明好的思路与语言以及背后的公司没有直接关系。 2019年另外一件事情是,与ReactiveX同时找到了一个框架Vert.x,综合考虑了一下,这可能是我想要的东西。当你把网络,文件,DNS等IO的部分全部异步化之后,基本上和协程没有区别了,而且作为类库形式比直接语言内置的感觉灵活性更好。2020年如果我有空的话想把xraft重写一下,至少把xgossip的核心部分尝试重写一次。 最后,2020年个人的目标,是继续学习一些个人比较重要的东西,比如说 SSTable MerkleTree STM(软件事务内存) 多核编程 等等。
-
Actor模型以及网络程序架构
在去年实现我的Raft实现,考察过一些Raft算法的实现,包括Golang的一些实现。基于Golang的实现给人的第一感觉是,不知道有哪些协程是“持续”运行的,比如说监听网络请求,快照生成等等。而且由于CSP模型不关注发送者和接受者导致数据追踪比较困难,在没有IDE的情况下你可能几眼都看不出数据到了哪个模块的哪行代码。个人不确实是不是开发者的问题,导致这个网络程序架构那么松散,还是说CSP模型下的程序都那么松散。 由于个人长期在Web后端开发,对于MVC模型以及处理复杂业务逻辑的分层架构、组件化和依赖注入都比较熟悉,但是在开发一个传统意义上的网络程序时,还不确定什么是最佳架构。 假如用类似Web容器的做法,即每过来一个请求,开启一个thread或者使用线程池的话,程序逻辑可以采用分层架构。即使请求入口采用非阻塞IO的做法,比如说用Netty,程序逻辑也可以做在Netty的pipeline或者单独开线程处理复杂任务。但是,网络程序与很多没有额外线程的Web后端不同,会有定时任务,会有比较复杂的异步处理,所以可能一开始做成类似事件驱动模型可能更好。 个人在实现xraft时,用类似事件驱动的方式处理定时任务,各种外部的请求,把接受和发送等IO操作交给了Netty。整体上类似JavaScript,但是我把耗时的快照生成与应用发在主事件线程外,严格来说有一些区别。使用事件驱动除了理解起来简单之外,一些多线程下的数据访问问题利用单线程的线程封闭解决掉了。 如果问这种架构是否有改进空间,个人认为是有的。但是多线程下数据访问是一个难点,比起全局锁,个人更看好STM(软件事务内存)。除了数据访问,逻辑的封装也需要考虑,还有比较棘手的组件间双向访问问题。 在进一步找寻解决方案的时候,个人先找到了Cassandra所使用的SEGA,一种基于Stage驱动的架构模式。一个Stage绑定一个queue和相关处理逻辑。复杂逻辑被拆分成多个Stage,从第一个Stage执行到最后一个Stage。类似逻辑可以复用Stage。 老实说,这和Actor模型很像。Actor有一个Mailbox,Actor的内部根据收到的消息进行不同的处理。Actor同时其他一些特点,比如说树一样的管理结构,失败处理,热部署等等。类似组件间双向访问问题,Actor可以通过名字访问来解决。 整体来说,Actor符合我的要求。把Actor作为逻辑单位,有助于松耦合组件,但不是像我看到的CSP代码那么散。Actor除了“无法超过的前辈”Erlang之外(Actor+协程+函数式,理论上最好的并发编程语言),Scala和Java中可以使用Akka。BTW,Akka有针对STM的实现。 为了学习,个人选择了另一个Java下的Actor实现:Vert.x。严格来说,Vert.x并没有完全实现Actor模型,但是Vert.x作者把涉及到的绝大部分IO操作都异步化了,比如网络(基于Netty),文件,数据库(扩展形式)等等。之前个人也撰文分析过,协程的本质是异步编程,假如你把所有操作都异步化了,剩下的你可以用JavaScript的callback方式,或者更直观的ReactiveX。 Vert.x还有一个特点是把Actor的queue统一到一个eventBus中去,这样做的一个效果是用queue的名字代替了Actor。考虑到Actor在失败时以及多个Actor时的处理时,这么做并没有太大问题。 在使用过Vert.x+ReactiveX之后,个人觉得这可能是现代网络编程的一种比较好的组合。当然同时理解这两者会比较难,特别是ReactiveX。好在Vert.x不需要ReactiveX也可以使用,就像JavaScript没有async/await也可以使用。 个人不确定这是不是面向复杂网络程序的最佳架构方案,如果有更好的方案,欢迎留言。
-
协程,异步式编程以及ReactiveX
上半年主要在忙写书的事情,下半年由于工作有点忙以及自己犯懒没有写博客。不过个人还是在持续学习,接下来是个人关于协程等的学习的一个小结。 严格来说协程不是一个新名词,但是因为Golang变得很火。很多博客从进程-线程-协程模型讲Golang如何高效,比那些没有协程的对手,比如Java好不知道到哪里去。先不管这种想法有点偏颇,个人想从协程想要解决什么问题开始,分析各种语言如何解决这些问题,来更深入地了解现代编程。 协程可以解决什么问题?首先三层调度模型(进程-线程-协程)中协程属于用户态任务调度,原先程序用线程来考虑问题,现在用任务粒度。程序怎么划分任务可以由程序员指定,也可以在特定的地方划分,比如IO操作。 IO操作相比计算型操作不怎么需要计算型资源,所以在等待IO操作返回时CPU资源是被“浪费”掉的。其次,非阻塞型IO的出现,使得编程方式发生了很大的变化。当你在考虑用额外的线程来处理IO阻塞时,非阻塞型IO可以少用额外的线程,只需要等待完成事件。 问题是“等待完成事件”对很多人来说个全新的东西,你可能需要一个轮询IO事件的selector(典型的reactor)以及相应的基于事件(event)编程的模式。你可以在JavaScript的运行时中看到这种模式。一般来说,语言层通过callback绑定后续操作。如果你有多个IO操作的话,你可能会写出callback嵌套callback的代码(或称callback hell)。JavaScript针对callback hell的解决方案是Promise,以及之后的aysnc/await。Promise允许链式callback,async/await允许用近似同步的方式编写异步的代码。 不管你是否感觉到,但是async/await确实做了类似协程的事情。 任务开始:IO操作开始,对应await开始 任务结束:IO操作结束,对应await结束 任务后续:callback,对应await之后的代码 注意,JavaScript由于是单线程模型,本身又有event loop,在实现async/await时并不需要对运行模型进行修改。其他语言想要实现async/await的话,除了语法层面,运行层也需要修改(在Erlang和Golang中,使用的协程运行时是一个“多线程版本的event loop”:work-stealing thread pool)。 小结一下,协程可以用于解决异步IO的callback hell问题。事实上,个人认为这是协程的优势。对于同样使用异步IO的程序来说,使不使用协程在性能上没有太大差距,但是用协程写法上会简单很多。至于Golang中使用go块开启协程的写法,个人认为是线程运行模式的补偿。比如你有一个非IO的长期计算任务,你没有IO操作的切入点和结束点,必须由程序员指明(用go关键字。BTW,因为这种任务的存在,协程运行时必须使用work stealing thread pool,否则被放到这个线程末尾的任务很长时间内不会被执行到)。 虽然协程可以简化异步IO调用,但是存在一些问题无法直接解决。比如说,我希望同时执行IO调用1,IO操作2并等待两个结果返回。如果你使用顺序IO操作1,IO操作2写的话,实际执行不会是并行的。在JavaScript下,你需要退回Promise并使用特定的API Promise.all。在Golang下,你需要主动开启两个go块执行IO操作,然后使用channel获取结果。 个人认为这其实显示了基于协程编程的脆弱性,一旦遇到不是简单的单次IO操作的场景,复杂性就会显现。又比如说,执行一个操作,你希望设置一个超时时间,在Golang中你必须同时使用channel/select,外加一个timer才能实现。相对的,同步编程下只需要在参数里加一个timeout。 可以看到,这里的复杂性其实是异步编程,或者说多线程编程的复杂性。JavaScript由于不是多线程,可以干脆放弃async/await的写法回归Promise+链式callback的做法。Golang使用了CSP编程的核心要素channel来处理多线程编程(BTW,Golang的channel可以与协程整合在一起)。 从个人角度来说,JavaScript的做法可能更好:假如你没法用协程了,那就退一步用不那么直接但是足够简单的方式,而不是退十步,用通用(即CSP)的方式。Golang很多人说简洁,但是由于其设计目标(比如工程化),只对基本的场景做了对应,剩下的场景就显得很简陋(C语言风格)。 除去Promise.all这种专用API之外,其他语言和框架中比较容易理解的,个人觉得是ReactiveX的zip,比如如下代码 Observable<Result1> source1 = operation1(); Observable<Result2> source2 = operation2(); Observable.zip(source1, source2, (r1, r2) -> { // do stuff }).subscribe(System.out::println); 这里没有暴露出任何和多线程相关的信息,但是可以达到同时执行的效果。进一步,假如你需要同时执行多个操作,只要有一个返回结果了就结束,在JavaScript、Golang和ReactiveX中你可以考虑下分别该怎么做(一个提示是,ReactiveX可以用amb替换zip)。 你可以把ReactiveX的subscribe当作JavaScript的callback,它会在获取到结果后被执行。和JavaScript的event loop不同,ReactiveX在多线程环境下执行时,subscribe的代码可能会在操作线程中执行。这对于Android开发来说可能会有问题(不能在GUI线程之外修改GUI元素),但是你可以通过简单增加一行observeOn(xxx)来解决这个问题。 注意,ReactiveX的数据流模型是一种PUSH模型,即数据流负责提交数据,触发下游操作。这某种程度和协程结束触发后续代码很像。其次ReactiveX允许你使用函数式语言的方式处理错误,集中精力在核心处理代码上。 当然ReactiveX不是万能的,在多线程编程方面,ReactiveX只能提供有限的帮助。更多的时候,你仍旧需要使用各种针对多线程处理的工具。以及,多种编程范式有助于你用各种高效的方法来解决问题,比如说Kotlin既有协程,也有async/await,也有ReactiveX的实现,当然你也可以用传统线程。个人认为这是现代编程对于愈来愈复杂的问题的解决方案,相应地这也需要程序员对语言使用对问题分析有足够的能力。 最后,以一句个人最近看到的话结尾“协程的本质是异步编程”。假如你能看到协程所要解决的问题,以及了解如何解决这些问题,那么相信你的学识会让你比其他人有更多手段和方法来更好地解决问题。