协程与xpcall

Posted by myc on June 25, 2019

协程与xpcall

在移植busted测试框架的时候,发现不能在测试用例里面调用协程的yield,核心原因是每个用例是运行在xpcall内,此时调用yield会报错

attempt to yield across metamethod/C-call boundary

示例代码


local func = function()
    print("xpcall xxx");
    coroutine.yield()--挂起
    print("yield");
end

local funcList = {func,func,func}
function run()
    local co = coroutine.create(function( ... )
        xpcall(function( ... )
            for i,func in ipairs(funcList) do
                func()
            end
        end, function(errMsg)
            print(errMsg) -- 触发异常 attempt to yield across metamethod/C-call boundary
        end)
    end)
    coroutine.resume(co)
end
run()

想要达到目的

  • 能捕获异常
  • 能调用协程yield

解决办法

捕获异常

xpcall 通常用来捕获异常,当时因为协程的缘故在这里我们不能直接使用。所以得换个思路,除了xpcall能捕获异常让程序不中断执行外,协程也有类似功能,或者说协程内部函数天然带有 异常捕获机制,所以我们可以利用协程替换掉xpcall

实现机制

local xpcall = function(f,err)

    local xco = coroutine.create(f)
    local succ,msg = coroutine.resume(xco)
    if not succ then
        err(msg)
    end
end

调用协程方法

如果我们改造了xpcall函数,此时可以调用协程内部的接口了,但是此时有个问题,如果在func内部调用yield此时挂起的是xpcall内部的xco 而外部的协程没有被挂起,还是会往下走。这里遇到了协程嵌套问题。解决办法是不断透传出去。为了好理解,我们在外部协程叫主协程,xpcall内部的协程叫子协程。此时我们在业务内部挂起了子携程,但是主协程没有yield还是会继续运行,所以此时当我们在yiled子协程的时候,根据yield的返回值控制是否挂起主协程。当要恢复子协程的时候由于拿不到xco句柄。我们得先恢复主协程,然后在恢复子协程。这里有点绕。具体代码如下

local xpcall = function(f,err)
    local xco = coroutine.create(f)
    local succ,msg = coroutine.resume(xco)
    while succ and coroutine.status(xco) == "suspended" do --根据子携程状态决定是否透传挂起主协程
        local code = coroutine.yield() --再次挂起主协程
        if code == "resume_sub" then --根据主协程指令是否恢复子携程
            succ,msg = coroutine.resume(xco) --主携程恢复过来的时候再次恢复子携程
        end
    end
    if not succ and msg then
        err(msg)
    end
end

local func = function()
    print(os.clock());
    coroutine.yield("await")--挂起子协程 此时走到 local succ,msg = coroutine.resume(xco) 
    print("resume sub");
end

local funcList = {func,func,func}
function run()
    local mainCo = coroutine.create(function( ... )
        xpcall(function( ... )
            for i,func in ipairs(funcList) do
                func()
            end
        end, function(errMsg)
            print(errMsg) -- 触发异常 attempt to yield across metamethod/C-call boundary
        end)
    end)
    coroutine.resume(mainCo)
    for i=1,3 do
        coroutine.resume(mainCo,"resume_sub") --模拟恢复子携程,其实是先恢复一次主携程,然后恢复子携程,解决协程嵌套问题
    end
end
run()