以swoole為例,學習如何實現協程
聊聊Swoole2.0協程
Swoole 2.0正式版釋出了。2.0版本最大的更新是增加了對協程(Coroutine)的支援。正式版已同時支援PHP5和PHP7。基於Swoole2.0協程PHP開發者可以已同步的方式編寫程式碼,底層自動進行協程排程,轉變為非同步IO。解決了傳統非同步程式設計巢狀回撥的問題。
目前Swoole底層內建的協程客戶端元件包括:udpclient、tcpclient、httpclient、redisclient、mysqlclient,基本涵蓋了開發者常用的幾種通訊協議。協程元件只能在伺服器的onConnect、onRequest、onReceive、onMessage 回撥函式中使用。
注意,Swoole 2.0.5以前的版本還是灰度測試版本,可能會存在問題。 beta是因為協程是全新的版本。
協程的使用示例
/**
只有在Server中才能使用協程。包括 http server,websocket server 和 server。
*/
$server = new SwooleHttpServer(`127.0.0.1`, 9501);
/**
觸發on request事件時,SWOOLE會開闢一個協程棧,對協程棧進行初始化
*/
$server->on(`Request`, function ($request, $response) {
$tcp_cli = new SwooleCoroutineClient(SWOOLE_SOCK_TCP);
/**
client在呼叫connect函式後,SWOOLE會將PHP上下文資訊儲存到當前棧內
然後將協程掛起,待確認連線成功後,觸發epoll事件,然後協程切換
恢復PHP上下文資訊,返回結果,繼續執行PHP程式碼
*/
if ($tcp_cli->connect(`127.0.0.1`, 9906) === false) {
$response->end("connect server failed.");
return;
}
$tcp_cli->send(`test for the coro`);
/**
client在呼叫recv函式後,SWOOLE會將PHP上下文資訊儲存到當前棧內
然後將協程掛起待後端svr回包,觸發epoll事件,然後協程切換
恢復PHP上下文資訊,返回結果,繼續執行PHP程式碼
如果後端在設定的超時時間內,未能回包,返回false
client的errCode定為110
*/
$ret = $tcp_cli->recv(100);
$tcp_cli->close();
if ($ret) {
$response->end(" swoole response is ok");
} else {
$response->end(" recv failed error : {$tcp_cli->errCode}");
}
});
$server->start();
協程的執行流程
主程式->回撥函式: 嘿,有資料到了,麻煩你處理下
Note right of 回撥函式: 遇到非同步IO
回撥函式-->主程式: 已儲存狀態,你執行其他任務吧
Note left of 主程式: 去執行其他任務
主程式->回撥函式: 傳送的資料,有訊息返回啦,你處理下
我們們就以上面的示例程式碼為例,說一說協程的執行流程。
Http Server監聽9051埠。當有相關事件發生時,如有資料到達,就會執行繫結到Request上的回撥函式。在執行回撥函式之前,會建立一個協程。這時,會儲存CPU暫存器的狀態和ZendVM Stack資訊。
在回撥函式執行過程中,如果遇到IO操作,如$tcp_cli->connect(
,就會儲存當前的狀態,並讓出CPU使用權。當前請求執行被掛起。
讓CPU出使用權後,CPU就可以用於處理其他事件。如處理其他客戶端的Request請求。
當被掛起的請求,又有新的事件發生,如上面$tcp_cli->connect()
的資料已經返回。這時,會使用掛起前儲存的狀態資訊恢復,然後繼續執行回撥函式。
如果在執行過程中,再次遇到IO操作,會繼續執行儲存狀態和讓出CPU使用權。
協程的意義
這些IO操作都是非阻塞的,即傳送請求和獲取資料分為兩步。當請求傳送完畢後,就會進行狀態儲存和讓出CPU使用權。在等待請求資料返的這段時間,CPU可以執行一些其他程式。這樣就可以充分利用CPU。
協程的實現
Swoole的協程是基於 setjmp 、 longjmp 實現的。Swoole為每個協程都分配了空間,用於儲存協程切換時的狀態資訊。進行協程切換時會自動儲存Zend VM的記憶體狀態(主要是EG全域性記憶體和vm stack)。當回撥函式執行完畢後,會自動銷燬分配的空間。
建立協程
什麼時候會建立協程?在Server的onConnect、onRequest、onReceive、onMessage 回撥函式被執行前會建立一個協程。
協程建立的方法是coro_create
。相關原始碼可以檢視swoole_coroutine.c
檔案。coro_create
方法中主要進行了如下操作:
int sw_coro_create(zend_fcall_info_cache *fci_cache, zval **argv, int argc, zval **retval, void *post_callback, void* params)
{
// 為回撥函式的執行做一些準備工作
.......
COROG.require = 1;
// 使用setjmp開啟一個協程
if (!setjmp(*swReactorCheckPoint))
{
// setjmp第一次呼叫會進入此程式碼分支,執行回撥函式
zend_execute_ex(execute_data TSRMLS_CC);
......
// 執行完畢後,關閉協程
coro_close(TSRMLS_C);
......
coro_status = CORO_END;
}
else
{
/**
如果執行longjump,會調到上面的setjmp(*swReactorCheckPoint)行。
但是,setjmp的返回值為非0。因此,longjump後,會進入此程式碼分支。
讓出CPU執行權。
*/
coro_status = CORO_YIELD;
}
COROG.require = 0;
return coro_status;
}
協程讓出CPU執行權yield
什麼時候會讓出CPU執行權?當回撥函式中遇到非同步IO的時候,會讓出CPU執行權。如,程式碼中的connect操作。下面,我們就以connect操作為例,看看讓出CPU執行權時都做了那些操作。
connect的相關程式碼在swoole_coroutine.c
檔案中。程式碼如下:
static PHP_METHOD(swoole_client_coro, connect)
{
long port = 0, sock_flag = 0;
......
//nonblock async
// 傳送連線資料,無需等待對方返回資料,就執行下面程式碼
if (cli->connect(cli, host, port, timeout, sock_flag) < 0)
{
......
}
......
// 獲取一個記憶體空間,用於儲存當前執行的上下文資訊。
php_context *sw_current_context = swoole_get_property(getThis(), 0);
......
// 儲存協程資訊
coro_save(sw_current_context);
// 讓出CPU使用權
coro_yield();
}
儲存協程資訊
所謂的協程資訊主要就是當前的上下文執行資訊。coro_save
方法在swoole_coroutine.c
檔案中。程式碼如下:
sw_inline php_context *sw_coro_save(zval *return_value, php_context *sw_current_context)
{
// 下面的程式碼主要是把當前的執行狀態儲存到之前獲取的記憶體空間中
zend_execute_data *current = EG(current_execute_data);
if (ZEND_CALL_INFO(current) & ZEND_CALL_RELEASE_THIS)
{
zval_ptr_dtor(&(current->This));
}
zend_vm_stack_free_args(EG(current_execute_data));
zend_vm_stack_free_call_frame(EG(current_execute_data));
strncpy(SWCC(uid), COROG.uid, 20);
SWCC(current_coro_return_value_ptr) = return_value;
SWCC(current_execute_data) = EG(current_execute_data)->prev_execute_data;
SWCC(current_vm_stack) = EG(vm_stack);
SWCC(current_vm_stack_top) = EG(vm_stack_top);
SWCC(current_vm_stack_end) = EG(vm_stack_end);
SWCC(current_task) = COROG.current_coro;
SWCC(allocated_return_value_ptr) = COROG.allocated_return_value_ptr;
return sw_current_context;
}
讓出CPU執行權
coro_yield
方法的作用是讓出CPU執行權。程式碼在swoole_coroutine.c
檔案中。
sw_inline void coro_yield()
{
SWOOLE_GET_TSRMLS;
// 還原棧資訊
#if PHP_MAJOR_VERSION >= 7
EG(vm_stack) = COROG.origin_vm_stack;
EG(vm_stack_top) = COROG.origin_vm_stack_top;
EG(vm_stack_end) = COROG.origin_vm_stack_end;
#else
EG(argument_stack) = COROG.origin_vm_stack;
EG(current_execute_data) = COROG.origin_ex;
#endif
// 跳轉到coro_create方法中setjmp程式碼行。
longjmp(*swReactorCheckPoint, 1);
}
在這個方法中主要進行了還原棧資訊和longjump操作。
COROG.origin_vm_stack 這些棧資訊的初始化在coro_init
方法中。記錄了協程執行前的狀態。
恢復協程
當非同步IO有資料返回後,會進行協程恢復。協程恢復的方法是coro_resume
。在swoole_coroutine.c
檔案中。程式碼如下:
int sw_coro_resume(php_context *sw_current_context, zval *retval, zval *coro_retval)
{
// 使用之前儲存的協程資訊恢復執行上下文環境。
EG(vm_stack) = SWCC(current_vm_stack);
....
int coro_status;
// 設定跳轉點,方便在執行過程中再遇到非同步IO操作,進行跳轉。
if (!setjmp(*swReactorCheckPoint))
{
//coro exit
// 繼續執行回撥函式
zend_execute_ex(sw_current_context->current_execute_data TSRMLS_CC);
coro_close(TSRMLS_C);
coro_status = CORO_END;
}
else
{
//coro yield
coro_status = CORO_YIELD;
}
if (unlikely(coro_status == CORO_END && EG(exception)))
{
sw_zval_ptr_dtor(&retval);
zend_exception_error(EG(exception), E_ERROR TSRMLS_CC);
}
return coro_status;
}
可見,建立協程和恢復協程的整體程式碼結構差不多。
結束協程
當回到函式執行完畢後,會結束協程。coro_close
方法用於結束協程。原始碼在swoole_coroutine.c
檔案中。
sw_inline void coro_close(TSRMLS_D)
{
// 釋放為協程而申請的相關資源
efree(EG(vm_stack));
efree(COROG.allocated_return_value_ptr);
// 恢復執行棧
EG(vm_stack) = COROG.origin_vm_stack;
EG(vm_stack_top) = COROG.origin_vm_stack_top;
EG(vm_stack_end) = COROG.origin_vm_stack_end;
--COROG.coro_num;
swTrace("closing coro and %d remained. usage size: %zu. malloc size: %zu", COROG.coro_num, zend_memory_usage(0), zend_memory_usage(1));
}
相關文章
- 【學習篇】Swoole 協程
- [學習篇] Swoole 協程
- 【Swoole原始碼研究】深入理解Swoole協程實現原始碼
- swoole協程初探
- swoole 學習筆記-做一頓飯來理解協程筆記
- Swoole協程與Go協程的區別Go
- Swoole 協程與 Go 協程的區別Go
- PHP之Swoole 學習筆記-用做飯的方式來理解協程PHP筆記
- swoole 協程原始碼解讀 (協程的排程)原始碼
- Swoole 實戰:MySQL 查詢器的實現(協程連線池版)MySql
- swoole 協程原始碼解讀 (協程的建立)原始碼
- swoole-MySQL 協程改造 LaravelMySqlLaravel
- swoole 協程原始碼解讀原始碼
- Swoole 4.4:支援 CURL 協程化
- 學習PS、AI日誌總結 (以PS為例)AI
- 學習之路 / goroutine 併發協程池設計及實現Go
- 以 ZGC 為例,談一談 JVM 是如何實現 Reference 語義的GCJVM
- 以dart語言為例說說如何學習一門新的語言Dart
- 前端狀態管理簡易實現(以vuex為例)前端Vue
- Laravel 使用 swoole 協程遇到的坑Laravel
- swoole到底有幾個協程
- 如何基於文件的內容實現 AI 對話功能,以 Documate 為例AI
- swoole 學習
- 以微擎版教育系統開發為例,如何實現redis快取Redis快取
- 以RK3568為例,ARM核心板如何實現NTP精準時間同步?
- Phxrpc協程庫實現RPC
- Kotlin協程學習之路【一】Kotlin
- Swoole 學習指南
- 學習 Swoole(一)
- 如何學習 Python 包並實現基本的爬蟲過程Python爬蟲
- 如何搭建大規模機器學習平臺?以阿里和螞蟻的多個實際場景為例機器學習阿里
- 基於 swoole 協程的 MySQL 連線池MySql
- Golang協程池(workpool)實現Golang
- 以Java專案為例,實現Jenkins對接CCE Autopilot叢集JavaJenkins
- Python學習之路35-協程Python
- Python3.5協程學習研究Python
- 把ChatGPT調教成機器學習專家,以邏輯迴歸模型的學習為例ChatGPT機器學習邏輯迴歸模型
- Laravel 裡面用swoole的協程go報錯 ?LaravelGo
- 以【某程旅行】為例,講述小程式爬蟲技術爬蟲