使用协程实现多线程
协程能够实现一种协作式多线程。每个协程都等于一个线程。一对 yield-resume 可以将执行权在不同线程之间切换。不过,与普通的多线程不同的是,协程是非抢占的。当一个协程正在运行时,是无法从外部停止的。只有当协程显式要求即通过调用函数 yield, 它才会挂起执行。
不过,对于非抢占式多线程来说,只要有一个线程调用了阻塞操作,整个程序在该操作完成前都会阻塞。有一个有趣的方法可以解决这个问题。
首先假设一个多线程场景,譬如通过 HTTP 下载多个远程文件。为了下载多个远程文件,我们必须知道如何下载一个远程文件。要下载一个文件,必须先打开一个到对应站点的连接,然后发送下载文件的请求,接受文件(按块),最后关闭连接。
local socket = require "socket"
//定义主机和要下载的文件
host = "www.lua.org"
file = "/manual/5.3/manual.html"
//打开一个TCP连接,连接到该站口的80端口
c = assert(socket.connect(host, 80))
local request = string.format(
"Get %s HTTP/1.0\r\nhost: %s\r\n\r\n", file, host)
c:send(request)
//接下来,以1KB为一块读取文件,并将每块写入到标准输出中:
repeat
local s, status, partial = c:receive(2^10)
io.write(s or partial)
until status == "closed"
//下载完成关闭
c:close()
最简单的下载多个远程文件的方法就是逐个地串行下载,但这种方式太慢了,程序的大部分时间都用在了等待数据到达上。
我们可以为每一个下载任务创建线程,当一个线程无可用数据时,它就可以将控制权传递给一个简单的调度器,这个调度器再去调用其他的线程。
//下载Web页面
function download (host, file)
local c = assert(sockect.connect(host, 80))
local count = 0
local request = string.format(
"Get %s HTTP/1.0\r\nhost: %s\r\n\r\n", file, host)
c:send(requres)
while true do
local s, status = receive(c)
count = count + #s
if status == "closed" then break end
end
c:close()
print(file, count)
end
//串行下载
function receive (connection)
local s, status, partial = connection:receive(2^10)
return s or partial, status
end
//并行实现中,在没有足够的可用数据时,该函数会挂起
function receive (connection)
connection:settimeout(0)
local s, status, partial = connection:receive(2^10)
if status == "timeout" then
coroutine.yield(connection)
end
return s or partial, status
end
//调度器
tasks = {}
function get (host, file)
local co = coroutine.wrap(function()
download(host, file)
end)
table.insert(tasks,co)
end
function dispatch ()
local i = 1
while true do
if tasks[i] == nil then
if tasks[1] == nil then
break
end
i = 1
end
local res = tasks[i]()
if not res then
talbe.remove(tasks, i)
else
i = i + 1
end
end
end
表 tasks 为调度器保存所有正在运行线程的列表。函数 get 保证每一个下载任务运行在一个独立的线程中。调度器本身就是一个循环,它遍历所有的线程,逐个唤醒它们。调度器同时还需要在线程完成任务后,将线程从列表中删除。所有的线程都完成后,调度器就停止循环。
还有一个很大的优化空间为,如果所有的线程都没有数据可读,调度程序就会陷入忙等待,不断地从一个线程切换到另一个线程来检查是否有数据可读。
为了避免这样的情况可以使用 LuaSocket 中的 select 函数,该函数允许程序阻塞知道一套接字的状态发生改变。
function newDispatch()
local i = 1
local timedout = {}
while true do
if tasks[i] == nil then
if tasks[1] == nil then
break
end
i = 1
timedout = {}
end
local res = tasks[i]()
if not res then
table.remove(tasks, i)
else
i = i + 1
timedout[#timedout + 1] = res
if #timedout == #tasks then
socket.select(timedout)
end
end
end
end
在循环中,调度器将把所有超时的连接收集到表 timedout 中。