skynet阻塞接口的原理剖析:
假设在lua代码中调用如下接口:
local res, err = socket.open("127.0.0.1", 6379)
if not res then
print(err)
else
print("hello")
end
即调用到C代码的driver.connect
,然后调用local function connect
的suspend
函数挂起当前协程,等待connect返回信息。
function socket.open(addr, port)
local id = driver.connect(addr,port)
return connect(id)
end
local function connect(id, func)
local newbuffer
if func == nil then
newbuffer = driver.buffer()
end
local s = {
id = id,
buffer = newbuffer,
pool = newbuffer and {},
connected = false,
connecting = true,
read_required = false,
co = false,
callback = func,
protocol = "TCP",
}
assert(not socket_onclose[id], "socket has onclose callback")
assert(not socket_pool[id], "socket is not closed")
socket_pool[id] = s
suspend(s)
local err = s.connecting
s.connecting = nil
if s.connected then
return id
else
socket_pool[id] = nil
return nil, err
end
end
function socket.open(addr, port)
local id = driver.connect(addr,port)
return connect(id)
end
connect请求返回后,触发了事件类型TYPE是OPEN,因此调用到如下函数,注意到调用wakeup
函数唤醒阻塞的协程
-- SKYNET_SOCKET_TYPE_CONNECT = 2
socket_message[2] = function(id, _ , addr)
local s = socket_pool[id]
if s == nil then
return
end
-- log remote addr
if not s.connected then -- resume may also post connect message
s.connected = true
wakeup(s)
end
end
最后lua代码就可以继续向下执行了,返回了res, err。
skynet中所有的阻塞接口如skynet.call、skynet.sleep、skynet.read都是利用了这个特性:底层是调用C函数,C函数调用真正的系统API,然后挂起调用skynet阻塞接口的协程。等系统API的请求返回后,通过返回消息的类型,调用不同的socket_message
函数处理,最后唤醒协程。
这样做的好处是:
- 在lua代码中,使用这些API就像是同步阻塞调用,其实本质上是一个异步回调的过程。使代码看起来比较简单易懂
- 在一个工作线程处理服务的消息队列的消息时,如果当前消息调用了阻塞API,那么当前消息对应的协程挂起,工作线程继续处理消息队列的下一条消息,运行另一个协程。这提高了一个工作线程的并发性(如果是线程直接调用系统API阻塞,导致线程切换的开销比协程切换开销大得多)。