Lua 协程

并发是现实世界的本质特征,而聪明的计算机科学家用来模拟并发的技术手段便是多任务机制。多任务机制大致可划分为两种:

  • 抢占式多任务(preemptive multi tasking)
    抢占式多任务让操作系统来决定合适执行哪个任务
  • 协作式多任务(cooperative multi tasking)
    协作式多任务将决定权交给任务,让它们在自己任务合适的时候自愿放弃执行。

这两种度偶任务各有优缺,抢占式多任务固有的同步问题使得程序经常出现不可预知的行为,协作式多任务则要求任务具备相当的自律精神。

协程(Coroutine)技术是一种程序控制机制,早在上世纪60年代已被提出,用它可以很方便地实现协作式多任务。在主流的编程语言如C++、Java、Pascal...中,很少能看见协程的身影。但在不少动态脚本语言如Python、Perl...中却都提供了协程或与之相似的机制,其中最为突出便是Lua。

协程(Coroutine)是指协作的例程,是多任务机制。最早由Melvin Conway于1963年提出并实现。跟主流程序语言中的线程不一样,线程是属于侵入式的组件,线程实现的系统称为抢占式多任务系统,而协程实现的多任务系统称为协作式多任务系统。

协程(Coroutine)拥有4种状态:正常(normal)、挂起(suspended)、运行(running)、停止(dead)

  • 挂起:协程创建后或yield之后即挂起状态,此时不能自动运行。
  • 运行:如果在协程函数中调用status,传入协程自身句柄,那么执行到这里的时候会返回running的状态。
  • 停止:函数处理完毕后的状态,此时不能再重新resume

由于线程缺乏yield语义,所以运行过程中不可避免的需要调度、休眠挂起、上下文切换等系统开销,而且还需要小心的使用同步机制保证多线程的正常运行。而协程(Coroutine)的运行指令是固定的,无需同步机制。协程之间切换也只涉及到控制权的交换,相比较线程来所是非常轻便的。不过同一时刻可以有多个线程运行,但仅能有一个协程运行。

Lua语言实现的协程(Coroutine)是一种非对称式(asymmetric)协程,或称之为半对称式(semi-symmetric)协程,又或叫半协程(semi-coroutine)。这种协程机制之所以称为非对称,是因为它提供了两种传递程序控制权的操作。

  1. (重)调用协程,即通过coroutine.resume实现。
  2. 挂起协程并将程序控制权返回给协程的调用者,即通过coroutine.yield实现。

一个半对称协程可以看作是从属于它的调用者的,二者的关系非常类似于例程(routine)与其调用者之间的关系。

既然有非对称式协程,当然也就有对称式协程(symmetric)。对称式协程的特点是只有一种传递程序控制权的操作,即将控制权直接传递给指定的协程。

Lua中的协程(Coroutine,协同程序,协同式多线程)和多线程(Thread)和相似,每个协程都有自己的堆栈、局部变量、PC计数器,同时又与其他协程共享全局变量等,并通过yield-resume实现协程间的切换。不同之处在于Lua的协程是非抢占式的多线程,也就是说,你必须手动的在协程间进行切换,而且同一时刻只能有一个协程在运行。另外,Lua中的协程无法在外部将其停止,而且是有可能导致程序堵塞的。

相关概念

  • 进程是正在运行的程序的实例

  • 线程(Thread)是进程内一个相对独立的、可调度的执行单元,是系统独立调度和分配CPU的基本单位,是运行中的程序的调度单位。
    每个线程代表一个执行序列,当在程序中创建多线程的时候,同一时刻多个线程是同时执行的,不过实质上多个线程是并发的,因为只有一个CPU,所以同一个时刻只会有一个线程在执行。在一个时间片内哪个线程执行时不确定的,可通过控制线程的优先级,不过真正的线程调度是由CPU来决定的。

  • 协程是指协作程序,协程之间通过协作(函数调用)来完成一个既定的任务。
    协程是把线程中不确定的地方尽可能地去掉,执行序列间的切换不再由CPU隐藏的进行,而是由程序显式地进行。所以,使用协程实现并发,是需要多个协程彼此协作的。

4933701-0063eea2b7fc388f.png
进程

协程(Coroutine)具有两个非常重要的特性:

  1. 私有数据在协程间断式运行期间一直有效
  2. 协程每次yield后让出控制权,下次被resume后从停止点开始继续执行。

通俗来说,类似一个带有静态数据且具有多个进入点和返回点的函数。

协程函数

  • coroutine.create()
    协程的创建,传入函数参数,返回thread线程对象。
  • coroutine.yield()
    协程的暂停,使正在执行中的函数挂起。
  • coroutine.resume() 协程的调用,用于启动或再次启动一个协程,使其由挂起状态转变为运行状态。