Lua协程
协同程序(coroutine)与多线程情况下的线程比较类似:有自己的堆栈、局部变量、指令指针,但与其它协程共享全局变量等很多信息。
协程类似一种多线程,但与多线程还有很多区别:
1. 协程并非os线程,所以创建、切换开销比线程相对要小。
2. 协程与线程一样有自己的栈、局部变量等,但是协程的栈是在用户进程空间模拟的,所以创建、切换开销很小。
3. 多线程程序是多个线程并发执行,也就是说在一瞬间有多个控制流在执行。而协程强调的是一种多个协程间协作的关系,只有当一个协程主动放弃执行权,另一个协程才能获得执行权,所以在某一瞬间,多个协程间只有一个在运行。
4. 由于多个协程时只有一个在运行,所以对于临界区的访问不需要加锁,而多线程的情况则必须加锁。
5. 多线程程序由于有多个控制流,所以程序的行为不可控,而多个协程的执行是由开发者定义的所以是可控的。
Lua的协程是不对称的(asymmetric coroutines),是指“挂起一个正在执行的协同函数” 和 “使一个被挂起的协程再次执行的函数”是不同的。
有些语言使用对称协同(symmetric coroutines),即使用同一个函数负责“执行与挂起间的状态切换”。
1、状态切换
协程有3个状态:
1、suspended(挂起);
2、running(运行);
3、dead(停止);
协程API:
co = coroutine.create(function) -->创建协程,返回thread类型 coroutine.status(co) -->检查协程状态 coroutine.resume(co) -->恢复运行 coroutine.yield() -->挂起
协程创建成功后,处于suspended状态,此时并未运行。
在协程中使用yield函数,可以让正在运行的代码挂起。
一个例子:
co = coroutine.create(function() for i=1,5 do print("co", i) coroutine.yield() end end) coroutine.resume(co) -- 1 coroutine.resume(co) -- 2 coroutine.resume(co) -- 3 coroutine.resume(co) -- 4 coroutine.resume(co) -- 5 print(coroutine.resume(co) ) -- true print(coroutine.resume(co) ) -- false cannot resume dead coroutine
注意:resume运行在保护模式下,如果协程内部存在错误,Lua并不会抛出错误,而是把错误返回给resume函数。
协程的另一个作用是通过resume-yield来交换数据:
co = coroutine.create(function(a, b) coroutine.yield(a+b, a-b) return 6,7 end) print(coroutine.resume(co, 20, 10)) -- true 30 10 print(coroutine.resume(co)) -- true 6 7
可见,resume的参数会传递给协同函数,yield的参数会作为resume的返回值。并且,协程结束时的返回值也会传递给 resume。
2、管道
协同最具有代表性的例子是用来解决生产者-消费者问题,假定有一个函数不断地生产数据(比如从文件读取),另一个函数不断的处理这些数据(比如写到一个文件中),函数如下:
function producer() while true do local x=io.read() send(x) end end function consumer() while true do local x=receive() io.write(x, ' ') end end
上面的代码中,生产者和消费者都在不停的循环,而对对方的状态一无所知,我们需要改变一下结构,使得两者能够协调工作。
以消费者驱动模型为例,一开始我们调用消费者,当消费者需要值时唤起生产者,生产者生产处数据后停止,直到消费者再次请求。
完整的示例代码如下:
function receive(prod) local status, value = coroutine.resume(prod) return value end function send(x) local coroutine.yield(x) end function producer() return coroutine.create(function () while true do local x=io.read() send(x) end end) end function consumer(prod) while true do local x=receive(prod) io.write(x, ' ') end end function filter(prod) return coroutine.create(function() local line = 1 while true do local x=receive(prod) x=string.format("%5d %s", line, x) send(x) line = line+1 end end) end consumer(filter(producer()))
上面这个例子的工作方式非常类似UNIX的管道(pipe),协程是一种非抢占式的多线程。
在pipe的方式下,每个任务在独立的进程中运行,进程间的切换代价比较高;
而在协同中,每个任务运行在独立的协同代码中,任务间的切换代价较小,与函数调用相当。
3、非抢占式多线程
协程是一种非抢占式的多线程,这句话的含义是:
当一个协程正在运行时,不能在外部终止它,只能通过显式调用yield挂起它的执行。
显然,非抢占式的多线程比较容易写,因为不需要考虑线程同步带来的bug。
非抢占式的多线程的弊端在于,不管什么时候,只要有一个线程调用一个阻塞操作(blocking operation),整个程序在阻塞操作完成之前都将停止。
协同的这种弊端有点让人难以忍受!
看一个多线程的例子,通过HTTP协议从远程服务器下载一些文件。
在下载过程中,如果遇到阻塞,挂起线程,并使用一个调度器去resume另一个线程。
require "luasocket" function receive(connection) connection:timeout(0) -- do not block local s,status=connection:receive(2^10) if status=="timeout" then coroutine.yield(connection) end return s, status end function download(host, file) local c=assert(socket.connection(host, 80)) local count = 0 c:send("GET" .. file .. " HTTP/1.0 ") while ture do local s, status=receive(c) count=count+string.len(s) if status=="closed" then break end end c:close() print(file, count) end threads = {} function get(host, file) local co=coroutine.create(function() download(host, file) end) table.insert(threads, co) end
function dispatcher() while ture do local n=#threads if n==0 then break end local connections={} for i=1,n do local status,res=coroutine.resume(threads[i]) if not res then -- finish table.remove(threads, i) break else -- timeout table.insert(connections, res) end end if #connections == n then socket.select(connections) end end end host="http://news.163.com/" get(host, "/14/0330/17/9OJO5ML800014JB6.html") get(host, "/14/0330/08/9OIQKNS90001124J.html") get(host, "/14/0330/10/9OJ2M5PN000915BF.html") dispatch()