实现了一个coroutine

在学习了lua之后, 总算是搞清楚了颇为流传的coroutine是怎么回事. 我发现coroutine就是我一直希望使用的那种多任务处理方式, 即有线程的清晰抽象, 又可以避免锁带来的开销以及其他并发问题, 可以说是线程和异步处理的一个最佳折中点.

在大约一年前, 我一直在为不能控制多线程的调度而苦恼(那里还不知道在Windows下Sleep(0)可以暂时让出cpu的控制权), 我甚至试图使用状态机来模拟多线程以便我可以控制线程调度, 后来因为这个方法对于代码的可维护性来讲付出的代价太大, 最终没敢使用. 再后来又饱受线程同步, 锁碰撞造成上下文切换的困扰. 在coroutine概念中, 每一个coroutine自已来决定什么时间来让出cpu时间, 由于是coroutine自行让出cpu执行时间, 那么也就不存在有些操作被中断的问题, 自然也就避免了锁碰撞的问题.

lua的coroutine是半对称的, 因此最坏的情况要比对称的coroutine多出一倍的开销. 但是相比对称协程而言半对称协程的调度可以更灵活, 逻辑上也更为清晰, 而对称协程则使得程序略显不清晰, 每一个协程可能需要对整个协作过程有所了解, 当然是否要使用对称coroutine就要看情况而定. 花了大概一天半业余时间终于实现linux和Windows版本的coroutine, 以后会尽量采用coroutine而非thread来解决多任务工作.

btw, 协程与线程相比, 开销就小在不会有锁碰撞造成上下文切换, 但是coroutine的切换不当有时候可能会比线程的锁碰撞带来的开销更大, 因此在写coroutine时一定要注意.


2月1号补充:
原生协程是在用户态切换上下文因此要比类似swapcontext之类的系统调用销小很多.
2月4号补充:
线程开销除了context切换开销之外还有调度开销.

又是权限问题

上周五发布了Beta版之后, 老板觉得这次加的功能挺多就试用了一下, 结果瞬间就崩了, 上去一顿猛批啊。 请他们试用了很久才发现又是因为权限问题。

在%ProgramFiles%下普通用户只有读取和执行的权限, 由于历史原因, 我们Client有一部分DLL是从其他地方copy到client.exe的当前目录下来动态加载的, 而这一版本恰好将管理员权限去掉了(因为Win7及以上版本在管理员权限不能访问网络共享路径), 两个巧合就碰撞到一起导致了这个bug的产生, 当然其实有些代码不是很规范, 不然应该只是加载某个DLL失败而已。

将所有需要动态释放的文件放到了一个与用户相关的目录, 使用DLL的绝对路径去LoadLibrary, 一开始没有任何问题, 直到完全卸载之后安装时就开始发现有些DLL加载不成功, 明明路径存在, LoadLibrary就是会fail。 在MSDN上找到这篇文章之后才找到问题所在, 使用绝对路径去LoadLibrary时, 被使用绝对路径去LoadLibrary的DLL如果静态依赖于另一个DLL, 那么这个被依赖的DLL就会被按照一定的目录顺序搜索, 不幸的是我们释放的DLL所在的路径恰好不在Windows的搜索中径范围之内。 使用MSDN推荐LoadLibrary(path, NULL, LOAD_WITH_ALTERED_SEARCH_PATH)将使用绝对路径加载的DLL所在的目录加入搜索范围问题即可解决。

又一次踩到权限的坑, 以后写代码一定要注意如非有必要尽量不使用root权限, 这样就不会碰到这种中途取消管理员权限之后产生的各种权限及附带引发的其他问题。

POSIX线程中的条件变量与互斥量

在看到UNP第26章时发现对于linux下的线程模型还很不是非常了解. 翻到APUE的第11章, 发现POSIX线程中通用的两个线程交互与同步手段有互斥量和条件变量两种.

对于这两种同步手段我想了很久也没想到每种到底适用于哪些场景, 甚至于我觉得几乎mutex就够完成绝大部分线程同步功能了. 以前在写代码时都是自然而然的用出来了, 也从来没有认真总结过到底什么情况下使用Mutex, 什么下使用条件变量(类似Windows下的事件).

google到这篇文章终于豁然开朗. 其实问题很简单, 也许换个描述方式就更容易说明问题, Mutex是用来在线程内来保证操作的原子性, 可以称为同步手段. 而条件变量是用来线程间通信使用的. 如Mutex在某一线程Get之后, 从此这个Mutex就属于这个线程, 只有Get到这个Mutex的线程才能将其释放. 而条件变量则不然, 线程A设置过条件变量后, 线程B即可访问, 反之线程B设置条件变量之后线程A也可以访问. 因此多线程编码中当需要保证于某一操作的原子性时则使用Mutex, 当线程间需要相互通信时则需要使用条件变量.


在下班回来的路上, 大概回忆了一下代码中多线程的地方, 发现其实并非没有用到条件变量(Windows下的事件)的地方, 只是因为我都是顺手就写成while (flag == flase) sleep(x)这种查询的方式, 如果将查询换成条件变量之后, 此线程被唤醒的次数将大大降低对CPU时间的浪费将会更少.


1月14日补充:
条件变量只有在pthread_cond_wait进入sleep之后, pthread_cond_signal函数执行才能将pthread_cond_wait函数唤醒, 如果一个线程先执行pthread_cond_signal函数, 随后另一个线程执行到pthread_cond_wait时依然会被挂起. 这点与Windows下的Event并不相同, Windows下的Event不论是先于WaitForSingleObject去设置Event还是后于WaitForSingleObject, 只要Event被设置WaitForSingleObject都能正常返回.

迟到的2014年总结

本来这篇总结早该写了, 只是最近一直在纠结网络编程问题没能静下心来去总结, 今天终于有时间就仔细总结一下以勉2015. 说是2014年总结, 其实应该算是毕业之后的总结, 因为13年才工作半年实在没什么好可以总结的, 因此拖到14年一并总结了.

13年7月毕业后, 我装着MFC 2K行的项目经验和几个玩具的MCU代码以及二千块钱便和同学一块来到了上海. 刚到没两天, 火车上的觉都没补够就被昂贵的消费打醒了. 房租隔断间都要700多(因为住隔断间我同学把笔记本也丢了), 每顿伙食费也要12块以上. 更要命的是找工作总是需要简历和坐地铁, 可是简历一张打印至少1块钱, 一趟地铁也至少要3块. 于是托师兄到他们公司偷偷打了两张简历, 后来又怕不够就又请还没来上海的同学帮忙打了一叠拿过来. 比较悲催的是我的简历才用了6章, 我的资金链就快断了. 在无奈的情况下, 只能在面试的六家公司中比较了一下, 挑了一家稍微正式点的公司, 也就是我现在所呆的公司.

在面试时说的是做Firmware然后偶尔可能需要用汇编开发(在毕业之初, 我其实是想做Firmware工程师的), 但是入职第一天, 给了我一份自定义汇编的英文文档、一个示波器和一个m25p16说这是试用期考核项目. 我一下子就傻眼了, MCU中C去驱动SPI_FLASH很随意, 甚至于说用FPGA都是小意思. 但是给我一个自定义汇编的文档, 然后就让我自己用示波器边写边调是我万万没想到的, 在上学时只用过电子束的示波器, 哪见过这么高端的数字示波器. 后来阿汤哥见到我一筹莫展的样子好心教了我示波器怎么用, 于是终于开始了我人生工作中的第一次代码经历. 经过一周多之后我终于可以用那自定义汇编操作M25P16了, 自以为万事大吉了. 原来还不算完, 还有要MFC dll, 还好有MFC功底, 搞定.

我以为原来这些工作熟悉之后是如此的随意, 等到正式分配给我的第一个芯片后才发现没我想的那么简单, 原来之前的考核项目中少了第一个步骤, 画原理图并layout. 原本画板对于我来说也不算什么, 但我只是会画数字电路PCB而已, 对于模拟电路和电源部分根本连了解都算不上, 于是我的第一关就卡死在电源上, 最后以学会一个新名词”纹波”(用于形容电压的稳定度)为代价解决了这个问题. 第二关就是协议,那颗芯片的调试接口几乎没有文档, 最后终于在某角落里找到一个中文文档后来还发现有错误, 过了这两关我总算是成功支持了一颗芯片, 再回头已经是一个月后的事了.

做完这颗芯片之后我心里很矛盾, 工作内容与我想的完全不一样, 这哪里是Fimrware工程师, 这其实是杀死脑细胞而又没收获的苦力. 之后就一直在想辞职的事, 恰好这时候又分给我一颗很变态的芯片, 半个月后发现自已依然如上一次一样处处碰坎, 别人一周做完的到我这就得一个月, 总觉得自己实在不适合做这个工作. 于是打定主意第二天找老板辞职. 每每回想起来, 都不得不感叹这是天意, 在回去路上碰到了一个熟人, 我与他在地铁上一直讨论到下地铁, 然后就决定熬到年底再说.

在第三芯片毫无起色时, 公司正在准备做新架构, 他们又出重新制定了一套汇编语言, 汇编器已经完成, 但是他们没有调试器UI. 这里要再次感谢阿汤哥的帮助, 他们当时都有人在忙, 然后看我芯片支持做的毫无起色,在阿汤哥的提议下这个任务终于放到我头上了, 当时高兴了老半天了. 他们让我估时间, 我不想失去这个难得的机会, 于是直接说一周内搞定. 后来才发现一周内搞定简直是天方夜谈, 就凭我那半调子的MFC基础, 周一到周日每天都加班, 最后也才出了个bug百出的简化版.

后来公司出了一件不大不小的事, 两位软件工程师先后离职, 公司正准备开始做新架构软件, 但是又找不出做软件的人. 于是我就如愿以偿的顶了过去, 当时想终于跟芯片支持说拜拜啦.

然后我就开始做新架构软件demo啦, 最初开始做时只是做了一个MFC程序, 然后多任务模拟进度条而已. 然而一切都没有我想象的那么简单, 到这时我才发现我的软件知识弱爆了. 第一步就碰到了一个很奇怪的问题, 我开了8个线程去跑8个进度条, 这8个假进度条函数是完全一样的, 但是他们的进度却完全不同步, 于是我第一次知道了在多线程中其实sleep(0)也是有用的, 虽然windows号称是抢占式操作系统, 但是还是需要我们在恰当的时机调用sleep(0)让出cpu的使用权来让程序跑的更均匀. 在后来总结多线程时总觉得这么多线程怕cpu会跑死, 于是就有了使用状态机去模拟多线芯程的做法, 当然其实也只是想想, 虽然可以做到但是很麻烦. 直到前不久才发现其实coroutine这种工作模式应该比状态机模拟多线程要好的多, 据说在windows下可以使用fiber来实现, 在linux下可以用swapcontext函数来实现, 具体还没有深入研究.

demo UI做完之后, 老板提出新的设想, 他觉得把数据处理部分和UI逻辑处理部分混合做在一起不好, 他觉得整个软件应该分成两块, 一个只是处理用户逻辑, 一个用于处理所有数据作业. 提到这种架构,我不得不说一句, 虽然老板是搞FPGA的, 但是他这一观点真的很不错, 与大名鼎鼎的MVC模式有不谋而合的味道, 后来我在做UI时采用了类似的方法. 然后我们就把逻辑处理部分定义为后台, 也就是TCP端的server端. 但是Server一词被引入后老板就开始有点胡思乱想了, 他觉得要把软件做成UI可远程控制的模式, 于是我们花大力气去做远程文件浏览器等很多不必要的复杂设计. 现在软件开发已经快结束了, 我可以很负责任的说这完全是过度设计, 为了所谓远程操作功能, 我们不得不在设计上牺牲很多其他功能以保证这个功能的实现, 而且至目前为止这种设计可能还残留着一个大坑.

在demo做完之后, server刚做了个开头, 公司招来了一位有多年经验的软件工程师, 所以最初socket通信模式是他来实现的, 但是他也只实现了socket通信那一块就离职了. 他走之后我来维护他那部分socket代码时总是很容易搞错, 于是把socket部分给重写了. 在实现socket命令处理之初我在把socket命令放在每一个模块的顶层去调用, 但是没多久我就发现这种实现的坏处了, socket相关的操作到处都是, 完全不便于移值和维护. 于是花了一周时间将所有socket命令集中实现为一个模块,也可以称为中间件, 这样以后不管是socket, CLI还是PIPE, 甚至于将TLV的通信协议换成文本通信都不会产生太大的问题, 我要做的只是换掉这个中间件而已, 这也是我第一次在软件开发中引用了分层的概念.

当我将socket单独实现为一个模块之后, 其实之前就存在但是不明显的问题现在就很明显了. 一些structure, Server要用, Client要用, socket通信要用, 为了满足高内聚低耦合, 在实现之初我都是定义三个structure, struct server, struct protocol, struct client, 然后在在模块与模块交互处一个字段一个字段的去转换, 这个问题一直困扰到我今年下半年, 后来才被我想通. 其实软件开发除了设计上的智慧外可能还需要一个度的取舍. 比如如上需求, 有些structure几乎是与整个软件相关的, 使用这些structure的模块其实是为这些structure来服务的, 那么这些structure就应该单独抽出来时做成全局头文件, 让各模块包含. 当然这也是需要自己去衡量哪些structure定义为全局最好,哪些structure定义为局部分有利于高内聚低耦合.

软件的重中之重就是数据解析部分, 设计之初只有一个数据解析模块, 后来随着需求的增加我发现不同的模式下其实数据解析除了接口一样之外, 其UI逻辑和解析逻辑完全不同, 而且不能保证对于数据解析模块的需求总是固定不变的. 在一次讨论上我提议根据open-close原则, 将数据解析模块拆分成三个具有相同接口的DLL, 然后根据ini文件来配置何时使用哪个数据解析模块, 以减少不断变更需求时更改代码对于其他数据解析模块误修改的风险. 由于数据解析模块也需要UI, 所以每一个数据解析模块被拆分成两部分, UI逻辑部分和数据解析部分. UI逻辑部分和数据解析部分中间采用tcp协议进行通信. 由于不同的数据解析模块的UI完全不一样, 所以更不要想定义一个结构体去通用所有数据解析模块的UI配置信息了, 更何况有可能还有第四个数据解析模块的出现. 于是我们在软件中首次采用了透明的概念, 即在UI的DLL接口中出来的是unsigned char数据和实际size, 在server端的DLL接口处进去的是unsigned char数据和实际的size, 然后在各自的DLL中转为相应的结构体.

这个软件其实严格来说算是一个平台, 中间还需要一些工程师写DLL插件. 当基础功能实现差不多时, 平台也总算初具雏形, 终于迎来了换架构热潮. 在一波又一波的问题之下, 最终发现其实那些DLL插件原本可以更简单, 于是我用实现了一个DLL导出基类, 然后给写DLL的插件的工程师用. 这个DLL插件和插件基类DLL是在Client下使用的, 正如前面所说由于不得不考虑Client和Server可能不运行于同一台PC上, 最终我提出使用将DLL通过socket复制到Client当前目录, 然后再使用LoadLibrary进行本地加载.

平台的基本功能该有的都差不多了, 随着写DLL插件的工程师编写插件的增加, 每天依然会有很多问题, 但是有些问题其实只是关于代码的实现问题. 当时下班后在观摩nignx, 恰好看到服务端的所有程序都是有log的, 而且log都分级的. 为了不影响量产使用时程序的执行效率, 我改写了log模块, 为log分级, 然后在debug级别打印了许多代码的执行过程以便其他工程师可以方便debug, 如果需要很底层的log信息, 则去Server的配置文件去定义log级别为DEBUG即可.

在软件基本功能还没完成时就提出了uniquekey功能, 结果这个功能反而是最后一个实现的. 这个模块的功能就是用来生成一个惟一序号, 供其他线程使用, 典型的生产者消费者模式. 最坏情况是1个线程生产, 64个线程消费. 以前对锁的效率测试过, 所以对于这种线程极多的情况下我是很忌讳用锁的. 在研究过DisruptorDisruptor框架中的队列实现方式之后, 仿写了一无锁队列出来, 使生产者消费者通过队列来消费, 由于无锁队列的数据拷贝上是可以并行的, 因此会快上不少.

在软件使用过程中, 开始有人报怨server启动慢的问题. 最后发现有一部分原因是软件使用xml来充当数据库的角色(大概几十M), 而且采用了非常规的定义方式(xml中几乎全是属性, 没有值的存在)导至了普通的xml解析器速度不是非常理想. 为此我专门针对这种特殊xml的用法重写了个xml解析器.

在软件不断添加小功能和改bug的过程中, 我一直在反思软件的模块化到底应该怎么进行. 虽我尽了最大努力, 但是在不断维护过程中, 发现我的代码组织方式依然不够理想. 为此我先是使用代码模块化(一)的方式去重构代码, 使代码依赖关系变得清晰, 最后却发现我的代码模块成图状完全没有层次的概念. 经过一个月的再次反思, 我借用了少许面向对象的思想, 采用代码模块化(二)的方式重构了代码. 重构完成之后发现代码像是树形了, 而且只要接口定义好之后结构就会非常清晰. 遗憾的是在重构完之后由于其他非技术原因没有使用上.

总得来说这一年半, 从无到有的开发一款软件还是感触颇多, 长了很多在书本上不能体会的知识. 有些东西只有做过再去看书上写的, 才会有共鸣. 不去经历永远也不会知道, 度的权衡永远也不会提高. 最近越来越觉得并发和网络是两大流行趋势, 可惜windows在此都不占优势, 于是准备转向linux平台, 在1月1日格了win8装上Debian 7.7也算有象征意义吧.

tcp的close和shutdown

今天看到”Unix网络编程”第六章中半关闭连接, 对close和shutdown的功能区别产生了疑惑. 代码运行情况如下:

情况1:C(client)与S(server)建立链接之后, 当C向S发送数据之后调用shutdown来关闭写操作(断开链接的四次挥手中的前两次)告诉S, C端已发送数据完成, 此时S依然可向C发送数据.
情况2:C(client)与S(server)建议链接之后, 当C向S发送数据之后调用close来关闭socket(同样发送断开链接的四次挥手的前两次, 后两次挥手将由S端调用close来完成), 此时S端被其他条件阻赛并不调用close函数. 然后此时S端向C端发送数据将会引起C端回应rst数据包.

那么此时,情况1与情况2的socket状态机走到相同的地方, 所作的回应却完全不一样, 行为很令人奇怪.

在网上看了众多blog多后, 比较发现, close除了相当于C和S各发了shutdown之外, 还多了一个释放socket file descriptor的操作. 因此,猜测当调用close之后函数应该是瞬间返回的, 剩余的四次挥手以及TIME_WAIT状态应该是靠kernel来实现的, 那么就容易解释上述情况的原因了.

情况1:虽然调用了shutdown, 但是C依然拥有这个socket file descriptor, 因此算是半关闭链接, 情况2在调用close之后, C所拥有的socket file desciptor即被归还与kernel, 那么剩余的四次挥手及TIME_WAIT即由kernel来完成, 当此时S向C发送数据之后, kernel发现此socket file descriptor已经被释放, 即向S返回rst数据包.

以上均为猜测, 如找到相关文档再进行修正.

tcp的端口号

虽然写了一年多的网络编程, 但是总觉得有种浮沙筑高台的感觉,于是去买了本unix网络编程卷一去读. 果不其然看到第五章使用netstat去调试第一个程序时就产生了一个疑惑.

这个例子每accept一个连接就去fork一个进程去处理, 当我开了一个server和2个client时发现, server一共会有三个进程存在, 一个是listen, 另外两个是accpet出来的, 但是使用netstat去调试时却发现这三个进程其实共享server进程的bind的端口,以前只是知道”端口是用来标识同一个IP的不同进程之间数据收发”, 却从没有深究其中原理. 而之前在windows写socket程序一般都是多线程, 多进程的方案从来没用过, 所以从未觉得这句话有什么不妥.

可能这个问题太简单了,google了半天都没找到有人解答, 最终在tcp/ip详解卷一的第18章P194上找到这样一句话”tcp使用由本机地址和远端地址组成的4元组:目的IP地址, 目的端口号, 源IP地址和源端口号来处于是传入的多个链接请求”, 于是一下豁然开朗, 找到tcp的协议头翻了一下, 果然其实每个tcp segment都是有这个四元素的. 那其实就问题就很明显了, 根本不是进程与端口的问题, 其实应该是说”端口和IP地址(源端口, 目的端口, 源IP和目的IP)是用来标识不同socket之间的数据收发”.

再仔细回忆一下server端的完整步骤:
socket //获取一个socket descriptor
bind //将刚获取socket descriptor绑定相应的地址或port(一般地址为INADDR_ANY)
listen //向kernel标识接收向该socket上的连接请求
while accept //接收client的请求


大致猜测一下kernel的方式, 当收到tcp segment时首先去查看tcp segment中的目的端口并找到与目的端口相关的socket, 然后如果发现SYN此类与连接有关的segment的时候直接转发给被listen的那个socket, 否则就根据tcp segment的源端口号和源IP地址去匹配相应的socket进行数据收发.