异步编程的两种模型,闭包回调,和Lua的coroutine,到底哪种消耗更大

今天和人讨论了一下CPS变形为闭包回调(典型为C#和JS),以及Lua这种具备真正堆栈,能够yield和resume的coroutine,两种以同步的形式写异步处理逻辑的解决方案的优缺点。以后生出疑问,这两种作法,到底哪种会更消耗。我本身的判断是,在一次调用只有一两个异步调用中断时(即有2次回调,或者2次yield),闭包回调的方式性能更好,由于coroutine的方式须要建立一个具备彻底堆栈的协程,相对来讲仍是过重度了。可是若是一次调用中的异步调用很是多,那么coroutine的方式性能更好,由于无论多少次yield,coroutine始终只须要建立一次协程,而闭包回调的每一次调用都必须建立闭包函数,GC的开销不算小。直接上测试代码编程

CPS:闭包

local count = 1000000

local list1 = {}
local list2 = {}
local clock = os.clock
local insert = table.insert
local remove = table.remove

local function setcb(fn)
    insert(list1, fn)
end

local function test1()
    setcb(function()
        
    end)
end

local time1 = clock()--开始
for i = 1, count do
    test1()
end
local time2 = clock()--调用
while true do
    list1, list2 = list2, list1
    for i = 1, #list2 do
        remove(list2)()
    end
    if #list1 == 0 then
        break
    end
end
local time3 = clock()--回调彻底结束

print(time2 - time1, time3 - time2)

coroutine:异步

local count = 1000000

local list1 = {}
local list2 = {}
local clock = os.clock
local insert = table.insert
local remove = table.remove
local create = coroutine.create
local yield = coroutine.yield
local running = coroutine.running
local resume = coroutine.resume

local function setcb()
    insert(list1, running())
    yield()
end

local function test2()
    setcb()
end


local function test1()
    resume(create(test2))
end

local time1 = clock()--开始
for i = 1, count do
    test1()
end
local time2 = clock()--调用
while true do
    list1, list2 = list2, list1
    for i = 1, #list2 do
        resume(remove(list2))
    end
    if #list1 == 0 then
        break
    end
end
local time3 = clock()--回调彻底结束

print(time2 - time1, time3 - time2)

输出:编程语言

image

coroutine的调用和唤醒/回调,比闭包回调慢很多函数

(PS. 这里有个插曲,我以前设置的count = 10000000,可是测试coroutine时报内存不足的错误,所以只能降低一个数量级来测试了)性能

接下来我把单次调用的回调次数增多测试

CPS:lua

local count = 1000000

local list1 = {}
local list2 = {}
local clock = os.clock
local insert = table.insert
local remove = table.remove

local function setcb(fn)
    insert(list1, fn)
end

local function test1()
    setcb(function()
        setcb(function()
            setcb(function()
                setcb(function()
                    setcb(function()
                        setcb(function()
                            setcb(function()
                                
                            end)
                        end)
                    end)
                end)
            end)
        end)
    end)
end

local time1 = clock()--开始
for i = 1, count do
    test1()
end
local time2 = clock()--调用
while true do
    list1, list2 = list2, list1
    for i = 1, #list2 do
        remove(list2)()
    end
    if #list1 == 0 then
        break
    end
end
local time3 = clock()--回调彻底结束

print(time2 - time1, time3 - time2)

coroutine:spa

local count = 1000000

local list1 = {}
local list2 = {}
local clock = os.clock
local insert = table.insert
local remove = table.remove
local create = coroutine.create
local yield = coroutine.yield
local running = coroutine.running
local resume = coroutine.resume

local function setcb()
    insert(list1, running())
    yield()
end

local function test2()
    setcb()
    setcb()
    setcb()
    setcb()
    setcb()
    setcb()
    setcb()
end


local function test1()
    resume(create(test2))
end

local time1 = clock()--开始
for i = 1, count do
    test1()
end
local time2 = clock()--调用
while true do
    list1, list2 = list2, list1
    for i = 1, #list2 do
        resume(remove(list2))
    end
    if #list1 == 0 then
        break
    end
end
local time3 = clock()--回调彻底结束

print(time2 - time1, time3 - time2)

输出:3d

image

回调的消耗仍然是coroutine处于劣势,但已经比较接近了。启动的消耗,因为coroutine须要建立比较大的堆栈,相对于闭包来讲仍是比较重度,所以启动仍然远远慢于闭包回调的方式。

最后,我把一次调用里的异步接口调用次数,改为到10000次(须要封装成多个函数,不然lua会报错:chunk has too many syntax levels),对好比下(此时次数都改为了count = 1000):

image

这个时候coroutine的回调消耗优点就上来了。不过通常来讲,实际应用中一次调用不可能调用这么屡次异步接口。

 

以后再来测试内存占用

CPS:

local count = 100000

local list1 = {}
local list2 = {}
local clock = os.clock
local insert = table.insert
local remove = table.remove

local function setcb(fn)
    insert(list1, fn)
end

local function test1()
    setcb(function()setcb(function()setcb(function()setcb(function()setcb(function()setcb(function()setcb(function()setcb(function()setcb(function()setcb(function()setcb(function()setcb(function()setcb(function()setcb(function()setcb(function()setcb(function()setcb(function()setcb(function()setcb(function()setcb(function()
    setcb(function()setcb(function()setcb(function()setcb(function()setcb(function()setcb(function()setcb(function()setcb(function()setcb(function()setcb(function()setcb(function()setcb(function()setcb(function()setcb(function()setcb(function()setcb(function()setcb(function()setcb(function()setcb(function()setcb(function()
    setcb(function()setcb(function()setcb(function()setcb(function()setcb(function()setcb(function()setcb(function()setcb(function()setcb(function()setcb(function()setcb(function()setcb(function()setcb(function()setcb(function()setcb(function()setcb(function()setcb(function()setcb(function()setcb(function()setcb(function()
    end)end)end)end)end)end)end)end)end)end)end)end)end)end)end)end)end)end)end)end)
    end)end)end)end)end)end)end)end)end)end)end)end)end)end)end)end)end)end)end)end)
    end)end)end)end)end)end)end)end)end)end)end)end)end)end)end)end)end)end)end)end)
end

collectgarbage("collect")
collectgarbage("stop")

local count1 = collectgarbage("count")
for i = 1, count do
    test1()
end
local count2 = collectgarbage("count")
while true do
    list1, list2 = list2, list1
    for i = 1, #list2 do
        remove(list2)()
    end
    if #list1 == 0 then
        break
    end
end
local count3 = collectgarbage("count")

print(count2 - count1, count3 - count2, count3 - count1)

coroutine:

local count = 100000

local list1 = {}
local list2 = {}
local clock = os.clock
local insert = table.insert
local remove = table.remove
local create = coroutine.create
local yield = coroutine.yield
local running = coroutine.running
local resume = coroutine.resume

local function setcb()
    insert(list1, running())
    yield()
end

local function test2()
    setcb() setcb() setcb() setcb() setcb() setcb() setcb() setcb() setcb() setcb()
    setcb() setcb() setcb() setcb() setcb() setcb() setcb() setcb() setcb() setcb()
    setcb() setcb() setcb() setcb() setcb() setcb() setcb() setcb() setcb() setcb()
    setcb() setcb() setcb() setcb() setcb() setcb() setcb() setcb() setcb() setcb()
    setcb() setcb() setcb() setcb() setcb() setcb() setcb() setcb() setcb() setcb()
    setcb() setcb() setcb() setcb() setcb() setcb() setcb() setcb() setcb() setcb()
end


local function test1()
    resume(create(test2))
end

collectgarbage("collect")
collectgarbage("stop")

local count1 = collectgarbage("count")
for i = 1, count do
    test1()
end
local count2 = collectgarbage("count")
while true do
    list1, list2 = list2, list1
    for i = 1, #list2 do
        resume(remove(list2))
    end
    if #list1 == 0 then
        break
    end
end
local count3 = collectgarbage("count")

print(count2 - count1, count3 - count2, count3 - count1)

输出:

image

coroutine的内存占用确实比闭包回调少不少。

所以,要内存仍是要性能,这个看本身的取舍了。

本次测试并不全面,还有不少状况没有测试(好比加上多个局部变量,闭包回调的性能和内存占用可能会受影响)。而且由于lua没有自带的CPS变形,callback hell的存在,致使写代码的体验比coroutine差了太多。所以这个测试主要为打算本身实现编程语言的读者作为参考。

相关文章
相关标签/搜索