使用协程处理耗时过程

游戏服务器常常有一些耗时的操作, 比如说给全服玩家发放奖励. 如果直接写一个循环, 遍历全服玩家, 给每个玩家发放奖励, 那么整个过程可能持续几分钟, 十几分钟甚至几十分钟, 整个进程都阻塞在这个过程中了. 解决这个问题的一种做法是使用定时器, 比如说每处理完 50 个玩家, 就停 1 秒, 1 秒后继续处理, 就像这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function deal(list)
local p = 0
local function foo()
local from, to = p + 1, math.min(p + 50, #list)
for i = from, to do
local id = list[i]
dosth(id)
end
p = to
if p < #list then
timer:start_once(1, foo)
end
end
foo()
end

但是这种做法太过麻烦. 特别是, 有的时候这个耗时过程是二重循环或者别的什么奇怪的控制流, 那就无法使用这种方法了. 更好的做法是使用协程(coroutine).

对于一个常规的过程, 一旦返回, 就丢失了全部栈里的信息, 下次调用时就得重新来过. 然而协程不同, 它允许过程在某些时刻切出, 进入挂起状态, 却又保存其全部的栈信息; 然后可以在将来的某些时刻将其唤醒. 唤醒之后的协程会在上次切出的地方继续执行, 就像它从来没有切出过一样. 除此之外, 还能在切入切出的时候传递参数. 举个简单的例子:

result

我们可以用协程处理耗时过程. 具体的思路就是把耗时过程包在一个协程里, 每执行一定的量就调用 coroutine.yield 切出协程, 然后利用定时器延时一段时间再唤醒协程, 直到协程执行完毕. 我们可以简单封装一下, 让使用者不感知协程的存在. 以下是个简单示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
counts = {}
max_counts = {}
function try_yield()
local co = coroutine.running()
assert(co ~= nil)
counts[co] = counts[co] + 1
if counts[co] >= max_counts[co] then
counts[co] = 0
coroutine.yield()
end
end

function with_coroutine(f, n, t)
return function(...)
local co = coroutine.create(f)
counts[co] = 0
max_counts[co] = n

local function foo(...)
coroutine.resume(co, ...)
if coroutine.status(co) == 'dead' then
counts[co] = nil
max_counts[co] = nil
return
end
timer:start_once(t, foo)
end
foo(...)
end
end

这样的话使用起来就很简单了, 现在你就可以真的 “直接写一个循环, 遍历全服玩家, 给每个玩家发放奖励” 了, 只要记得调用 try_yield:

1
2
3
4
5
6
deal = with_coroutine(function(list)
for i, id in ipairs(list) do
dosth(id)
try_yield()
end
end, 50, 1)

你还可以选择使用这篇文章中介绍的装饰器, 让代码更加优雅.

处理耗时过程应该是协程的一个比较常规的操作. 其实对于处理耗时过程, 很多人第一想到的是开辟一条线程去处理. 然而线程会有并发的问题, 况且线程数太多会给CPU带来额外的负担. 这里我们可以利用 Lua 的优势, 使用协程解决这个问题.


使用协程处理耗时过程
https://luyuhuang.tech/2019/11/07/use-coroutines-to-process-time-consuming-procedures.html
Author
Luyu Huang
Posted on
November 7, 2019
Licensed under