一、多核并發(fā)編程方式
(1)多線程。
在一個進程中開啟多線程,為了充分利用多核,一般設(shè)置工作線程的個數(shù)為 cpu 的核心數(shù)。memcached 就是采用這種方式。
多線程在一個進程當中,所以數(shù)據(jù)共享來自進程當中的虛擬內(nèi)存;這里會涉及到很多臨界資源的訪問,所以需要考慮加鎖。
(2)多進程。
在一臺機器當中,開啟多個進程充分利用多核,一般設(shè)置工作進程的個數(shù)為 cpu 的核心數(shù)。nginx 就是采用這種方式,nginx 當中的 worker 進程,通過共享內(nèi)存來進行共享數(shù)據(jù);也需要考慮使用鎖。
(3)CSP。
以 go 語言為代表,并發(fā)實體是協(xié)程(用戶態(tài)線程、輕量級線程);內(nèi)部也是采用多少個核心開啟多少個線程來充分利用多核。
(4)Actor。
erlang 從語言層面支持 actor 并發(fā)模型,并發(fā)實體是 actor(在skynet 中稱之為服務(wù));skynet 采用 c + lua來實現(xiàn) actor 并發(fā)模型;底層也是通過采用多少個核心開啟多少個內(nèi)核線程來充分利用多核。
二、skynet
2.1、skynet簡介
它是一個輕量級游戲服務(wù)器框架,但也不僅僅用于游戲。
輕量級體現(xiàn)在:
- 實現(xiàn)了 actor 模型,以及相關(guān)的腳手架(工具集):actor 間數(shù)據(jù)共享機制以及c 服務(wù)擴展機制。
- 實現(xiàn)了服務(wù)器框架的基礎(chǔ)組件。實現(xiàn)了 reactor 并發(fā)網(wǎng)絡(luò)庫;并提供了大量連接的接入方案;基于自身網(wǎng)絡(luò)庫,實現(xiàn)了常用的數(shù)據(jù)庫驅(qū)動(異步連接方案),并融合了 lua 數(shù)據(jù)結(jié)構(gòu);實現(xiàn)了網(wǎng)關(guān)服務(wù);時間輪用于處理定時消息。
skynet抽象了actor并發(fā)模型,用戶層抽象進程;sknet通過消息的方式共享內(nèi)存;通過消息驅(qū)動actor運行。
skynet的actor模型使用lua虛擬,lua虛擬機非常?。ㄖ挥袔资甼b),代價不高;每個actor對應(yīng)一個lua虛擬機;系統(tǒng)中不能啟動過多的進程就是因為資源受限,lua虛擬機占用的資源很少,可以開啟很多個,這就能抽象出很多個用戶層的進程。lua虛擬機可以共享一些數(shù)據(jù),比如常量,達到資源復(fù)用。
抽象進程而不抽象線程的原因在于進程有獨立的工作空間,隔離的運行環(huán)境。
sknet的所有actor都是對等的,通過公平調(diào)度實現(xiàn)。
2.2、環(huán)境準備
ubuntu:
# 或者
sudo apt-get install git build-essential libreadline-dev autoconf
centos:
mac:
2.3、編譯安裝
cd skynet
# centos or ubuntu
make linux
# mac
make macosx
2.4、Actor 模型
有消息的 actor 為活躍的 actor,沒有消息為非活躍的 actor。
定義:
- 用于并行計算;
- Actor 是最基本的計算單元;
- 基于消息計算;
- Actor 通過消息進行溝通。
組成:
- 隔離的環(huán)境。主要通過 lua 虛擬機來實現(xiàn)。
- 消息隊列。用來存放有序(先后到達)的消息。
- 回調(diào)函數(shù)。用來運行 Actor;從 Actor 的消息隊列中取出消息,并作為該回調(diào)函數(shù)的參數(shù)來運行 Actor。
2.5、消息隊列
Actor 模型基于消息計算,在 skynet 框架中,消息包含 Actor(之間)消息、網(wǎng)絡(luò)消息以及定時消息。
生產(chǎn)者生產(chǎn)消息,消息放入消息隊列中,由線程池去消費消息。
2.6、actor公平調(diào)度
所有的actor都是對等的,每個actor都有自己的消息隊列;skynet的并發(fā)實體是actor,因此需要公平的調(diào)度actor。
skynet會有非常的actor,公平調(diào)度就非常重要;采用兩級隊列。
首先,查找活躍的消息隊列(有消息的消息隊列);
然后,通過隊列組織所有活躍的消息隊列;
最后,調(diào)度隊列。
線程池從一級隊列取出一個消息,消費消息,消費完消息后如果還有消息,則添加到一級隊列的末尾。因為用戶問題造成消息不均勻,在應(yīng)用上不一定公平;skynet在工作線程賦予權(quán)重來解決這個問題。
static int weight[] = {
-1, -1, -1, -1, 0, 0, 0, 0,
1, 1, 1, 1, 1, 1, 1, 1, // 1/2
2, 2, 2, 2, 2, 2, 2, 2, // 1/4
3, 3, 3, 3, 3, 3, 3, 3, }; // 1/8
當工作線程的權(quán)重為 -1 時,該工作線程每次只 pop 一條消息;當工作線程的權(quán)重為 0 時,該工作線程每次消費完所有的消息;當工作線程的權(quán)重為 1 時,每次消費消息隊列中二分之一的消息;當工作線程的權(quán)重為 2 時,每次消費消息隊列中四分之一 的消息;以此類推;通過這種方式,完成消息隊列梯度消費,從而不至于讓某些隊列過長。
三、skynet的使用
github 上打開 skynet 點擊 wiki 里面有所有函數(shù)的詳細說明。
3.1、第一個skynet程序
(1)在sknet源碼的父目錄下創(chuàng)建一個config文件,用于定制skynet的行為。 內(nèi)容可參考如下:
logger=nil
harbor=0
start="main"
lua_path="./skynet/lualib/?.lua;./skynet/lualib/?/init.lua;"
luaservice="./skynet/service/?.lua;./app/?.lua"
lualoader="./skynet/lualib/loader.lua"
cpath="./skynet/cservice/?.so"
lua_cpath="./skynet/luaclib/?.so"
參數(shù)說明:
(2)在app文件下面創(chuàng)建main.lua。每次啟動時都是從skynet.start(…)中運行,相當于入口函數(shù)。
skynet.start(function()
-- body
print("hello skynet")
end)
(3)編寫一個自己項目的makefile。首先編譯skynet的源碼,如果有自己的c代碼直接加入makefile文件。
all:
cd $(SKYNET_PATH) && $(MAKE) PLAT='linux'
clean:
cd $(SKYNET_PATH) && $(MAKE) clean
(4)編譯,執(zhí)行自己的makefile。
(5)執(zhí)行skynet,指定config。
[:00000003] LAUNCH snlua launcher
[:00000004] LAUNCH snlua cdummy
[:00000005] LAUNCH harbor 0 4
[:00000006] LAUNCH snlua datacenterd
[:00000007] LAUNCH snlua service_mgr
[:00000008] LAUNCH snlua main
hello skynet
[:00000002] KILL self
可以看到打印了hello skynet。
3.2、skynet網(wǎng)絡(luò)消息
skynet 當中采用一個 socket 線程來處理網(wǎng)絡(luò)信息;skynet 基于 reactor 網(wǎng)絡(luò)模型。
網(wǎng)絡(luò)消息驅(qū)動actor運行。需要skynet.socket模塊。
首先綁定一個監(jiān)聽 listenfd,然后調(diào)用socket.start(listenfd, function)綁定fd到進程函數(shù)function中處理。
示例:
main.lua
local socket=require "skynet.socket"
skynet.start(function()
-- body
print("hello skynet")
local listenfd=socket.listen("0.0.0.0",8888);
socket.start(listenfd,function(clientfd,addr)
-- body
print("receive a client: ",clientfd,addr);
end)
end)
[:00000003] LAUNCH snlua launcher
[:00000004] LAUNCH snlua cdummy
[:00000005] LAUNCH harbor 0 4
[:00000006] LAUNCH snlua datacenterd
[:00000007] LAUNCH snlua service_mgr
[:00000008] LAUNCH snlua main
hello skynet
[:00000002] KILL self
receive a client: 2 192.168.0.105:50024
3.3、skynet定時消息
定時任務(wù)推送給定時線程,定時線程檢測完時間后往消息隊列推送一個消息,然后actor開始執(zhí)行callback函數(shù)。
使用示例: main.lua
skynet.start(function()
-- body
print("hello skynet")
skynet.timeout(100,function()
-- body
print("timer 1s");
end)
end)
3.4、skynet actor間消息
所有的服務(wù)都是通過消息來交換數(shù)據(jù)。actor間消息通信通過將消息放到對方的消息隊列實現(xiàn)。
示例,創(chuàng)建兩個lua文件。
main發(fā)送“ping”給slave,協(xié)程掛起;slave回復(fù)“pong”給main,協(xié)程喚醒。整個過程使用協(xié)程實現(xiàn)異步操作,消除異步中的回調(diào)。
main.lua
skynet.start(function()
-- body
print("hello skynet")
local slave=skynet.newservice("slave")
local response=skynet.call(slave,"lua","ping")
print("main ",response)
end)
slave.lua
local CMD={}
function CMD.ping()
-- body
skynet.retpack("pong")
end
skynet.start(function()
skynet.dispatch("lua",function(session,source,cmd,...)
local func=assert(CMD[cmd])
func(...)
end)
end)
執(zhí)行效果:
[:00000003] LAUNCH snlua launcher
[:00000004] LAUNCH snlua cdummy
[:00000005] LAUNCH harbor 0 4
[:00000006] LAUNCH snlua datacenterd
[:00000007] LAUNCH snlua service_mgr
[:00000008] LAUNCH snlua main
hello skynet
[:00000009] LAUNCH snlua slave
main pong
[:00000002] KILL self
四、vscode調(diào)試skynet
首先要知道整個系統(tǒng)/框架 是怎么應(yīng)用的。skynet會自己產(chǎn)生一個skynet執(zhí)行程序,需要指定配置文件來啟動服務(wù)。
項目下的.vscode文件夾需要如下三個文件。
launch.json
"version": "0.2.0",
"configurations": [
{
"name": "啟動 app",
"type": "cppdbg",
"request": "launch",
"program": "${workspaceFolder}/skynet/skynet",
"args": ["config"],
"stopAtEntry": false,
"cwd": "${workspaceFolder}",
"environment": [],
"externalConsole": false,
"MIMode": "gdb",
"setupCommands": [
{
"description": "為 gdb 啟用整齊打印",
"text": "-enable-pretty-printing",
"ignoreFailures": true
},
{
"description": "將反匯編風格設(shè)置為 Intel",
"text": "-gdb-set disassembly-flavor intel",
"ignoreFailures": true
}
],
"preLaunchTask": "build-skynet",
"miDebuggerPath": "/usr/bin/gdb"
}
]
}
settings.json
"C_Cpp.default.configurationProvider": "ms-vscode.makefile-tools",
"files.associations": {
"string.h": "c",
"stdlib.h": "c",
"pthread.h": "c"
},
}
tasks.json
"tasks": [
{
"type": "cppbuild",
"label": "build-skynet",
"command": "/usr/bin/make",
"args": [],
"options": {
"cwd": "${workspaceFolder}"
},
"problemMatcher": [
"$gcc"
],
"group": {
"kind": "build",
"isDefault": true
},
"detail": "..."
}
],
"version": "2.0.0"
}
實現(xiàn)這三個文件就可以開始調(diào)試了。
skynet是一個網(wǎng)絡(luò)服務(wù),需要處理網(wǎng)絡(luò)連接,需要知道數(shù)據(jù)是怎么流向,理解框架的運行機制(多線程并發(fā));所有的actor的運行都是由線程池來驅(qū)動,actor的運行又是由消息隊列的消息驅(qū)動(網(wǎng)絡(luò)消息、定時消息、actor間消息)。
線程池從消息隊列中取出消息,找到回調(diào)函數(shù),驅(qū)動actor運行。
總結(jié)
不要通過共享內(nèi)存來通信,而應(yīng)該通過通信來共享內(nèi)存。CSP 和 Actor 都符合這一哲學;通過通信來共享數(shù)據(jù),其實是一種解耦合的過程。并lua發(fā)實體之間可以分別開發(fā)并單獨優(yōu)化,而它們唯一的耦合在于消息;這能讓我們快速地進行開發(fā);同時也符合我們開發(fā)的思路,將一個大的問題拆分成若干個小問題。
actor模型是在用戶層抽象了進程。
actor調(diào)度:將活躍的 actor 通過全局隊列組織起來;actor 當中的消息隊列有消息就是活躍的 actor; 線程池去全局隊列中取出 actor 的消息隊列,接著運行actor。
-
服務(wù)器
+關(guān)注
關(guān)注
12文章
8701瀏覽量
84550 -
內(nèi)存
+關(guān)注
關(guān)注
8文章
2903瀏覽量
73539 -
模型
+關(guān)注
關(guān)注
1文章
3032瀏覽量
48360 -
虛擬機
+關(guān)注
關(guān)注
1文章
888瀏覽量
27812
發(fā)布評論請先 登錄
相關(guān)推薦
評論