经过多年的工作,我发现很多问题都是人为搞复杂的,设计上的错误会导致程序的复杂,程序的复杂会进一步增加bug,不正确的对bug的处理又会增加复杂性。。。一个好的设计一定是简单优美的,一定是思考了本质的结果,而不是为了怎样而怎样。
本文主要针对linux下用户态编程,不区分并发和并行,多核cpu和多个cpu等文字上的定义。
为什么要进行协程编程,这事还是要从线程说起,为什么要用多线程呢?为了多任务,这是很多人的答案,但是我认为这不是必须的,因为单线程也可以多任务。我认为多线程/进程的必要理由是充分利用cpu和io,比如多个cpu,多个磁盘,但是没有做raid等,需要他们同时传输数据,但是很多情况下都是单核的,也没有磁盘利用的问题。多线程会带来什么问题?为什么,多线程会带来数据冲突的问题,没有正确的使用锁,没有使用锁或正确的无锁算法,都会带来bug。为什么需要锁?锁是为了解决数据冲突,如何解决的,答 串行, 锁意味着将并行变成串行,同时附带一些其他操作,比如线程切换。既然这样,为什么不直接串行呢,并行是怎么产生的呢?首先多个cpu是一定存在并行的,这是显然的,多个cpu处理相同的数据由于缓存的原因,往往会低效,发挥不出多cpu的优势,同时多个cpu的正确编程也是很难的,c语言不像一些更上层的语言,它需要考虑底层的问题,比如内存读写乱序等,如果不是做OS层开发的c语言程序员,往往不清楚这些内容,一个好的设计是尽量保持多个cpu的独立性,这里面可以用一些技巧来使它们尽量互不干扰。对于一个cpu的情况,没有真正意义上的并行,因为只有一个人干活,这时候“并行”产生的原因是被动,即线程切换是内核决定的,但内核并不知道线程里面的逻辑,它不知道什么时候是合理的切换的时候,一个人干一半另一个人就过来抢了,这就产生了问题。
举个例子,假如你有三个小孩,五个碗,每个碗里有一些吃的,如果是你将吃的分配给小孩吃,没有任何问题,如果不是你控制,小孩自己来拿,可能一个小孩把手伸进了一个碗,另一个小孩随后也把手伸进了同一个碗,就可能抓坏另一个小孩的手,或者导致一个小孩拿不到吃的。
多线程就是这样,你控制不了哪个线程什么时候运行。协程就是完全你来控制,怎么控制? 放在一个线程里,过滤掉内核的调度,然后你自己分配,什么时候该做什么,这样就可以做完一件事之后,再做另一件事,很多情况下,都可以不使用锁,就算有通信,有依赖关系,往往也可以通过其他通知方式来实现,当然即使需要锁,也是很简单的情况,因为很少需要锁了,本身就是串行的,这样编程就回归了简单。
很多情况,都是单线程模型可以搞定的事情,当然协程也是可以运行在多核上的,go routine就是一个例子,这样可以充分利用多个cpu。但我这里实现的是单核下的协程,因为可以适用于很多种情况,并且简单。
协程是怎么实现的?协程并不神秘,它的本质上就是非阻塞方式实现的单线程多任务,一个任务是可能会阻塞的,比如一个io的读写,或者是一个睡眠,如果多任务一定会阻塞其他任务,多线程模型可以解决这个问题,因为不同任务运行在不同的线程上,但是单线程也是可以的,用非阻塞io,select/poll/epoll就是这样的机制,同时可以把定时器timerfd,信号signalfd等都融合到一起,一个地方阻塞,同时阻塞所有的任务,当有任务可以运行的时候,会继续运行它,谁能运行运行谁,这样就实现了主动调度,当然如果碰到while(1);这样的代码别的任务就完了,所以协程吗,协作,要多个任务配合,禁止使用阻塞io,和while(1);这样的代码,如果是计算量特别大的耗cpu的不阻塞的任务,可以适当主动让出执行权。线程是可以对治所有不配合的,但是协程要求的是大家配合,必须每个任务都配合。前面提到非阻塞io,这和协程有什么区别,已经很接近了,一般非阻塞io都是要配合一个回调函数,对于数据收发是很自然的,但是如果除了数据收发之外还有复杂的逻辑需要处理,那么需要再增加回调函数,回调函数多了代码就会变得难读,为了解决这个问题,协程就上场了,它做了什么?
1. 接管io的读写等阻塞的地方,即和epoll等打交道的地方。
2. 为每个任务创建一个栈空间,看起来非常像多线程。
一个任务在运行阻塞(非阻塞,实际不会阻塞)的时候,协程程序负责切换到其他栈空间运行程序,即切换到其他任务,协程程序负责检查epoll_wait和任务队列,当前面阻塞的任务可以恢复运行的时候,切换栈空间到那个任务。我们需要做的只是把逻辑按顺序的放到一个任务里,就好象是在一个线程中写代码一样,至于什么时候挂起,什么时候恢复执行全部都是协程程序负责的,这样就简单多了。
一个event_loop模型往往都会实现一个用户态的定时器,配合epoll_wait等的timeout实现,协程也可以实现,同时也可以实现一个sleep,一个sleep其实就是一个定时器,定时唤醒这个任务,go语言中的channel,是一种很好的多任务间传递数据和通知的机制,有了这个东西,就可以更容易做无锁的设计了,这个也是可以在协程里面实现的。
远离数据冲突,远离单核多线程的画蛇添足,让协程来让简化编程吧。