遇到c++非同步回調函數引用傳遞空指針異常 std::bad_function_call 錯誤的問題分析與解決方案。 發生錯誤“進程已結束,退出代碼-1073741819 (0xC0000005)”。 ...
c++非同步回調函數引用傳遞空指針異常
問題描述
最近使用 c++ / qt 開發的一個桌面應用,運行到一處非同步執行python腳本任務的方法處報錯:
進程已結束,退出代碼-1073741819 (0xC0000005)
此處是單獨開一個線程非同步執行一個python腳本後,回調 UI 線程傳來的回調函數將結果返回給 UI 線程,大致代碼如下:
void TestCaseProject::initProTestCasesEnvAsync(const std::function<void(std::vector<std::pair<std::string, Json::Value>>)>& _callback) {
std::thread t{[&] () {
doWithSetRunning([&]() {
auto result = this->initProTestCasesEnv();
_callback(result);
});
}};
t.detach();
}
void callBackFunc(const std::vector<std::pair<std::string, Json::Value>>& rps) {
// do some things
}
// UI線程某函數調用initProTestCasesEnvAsync
void uiFunc() {
// create project and other code
project->initProTestCasesEnvAsync(callBackFunc);
// do other things
}
解決方案
問題解讀
搜索進程已結束,退出代碼-1073741819 (0xC0000005)
,網上對其的準確描述很少,於是進行總結:
-
退出代碼就是執行的程式退出時的返回值,如
main
函數直接返回、調用程式退出的函數void exit(int _Code)
、有未解決的異常從程式拋出到系統後返回系統定義的錯誤退出碼等,通常是一個十六進位 int 值。 -
退出代碼中括弧內的才是實際的十六進位退出代碼(一般使用這個),前面是其十進位表示(因為起始有一個十六進位數 c 所以變成負數,類似一個標識,用來區分各系列錯誤代碼)。
-
錯誤代碼無法確定錯誤的詳細信息,只能大致進行判斷,具體情況需要進一步分析代碼上下文,或者捕捉異常、進行調試來確定。
-
錯誤退出代碼一般由未處理的異常觸發,而不是直接退出程式並返回該代碼。
-
在Windows系統下,錯誤代碼定義在頭文件
<winerror.h>
-
舉一反三,在其他操作系統上也有定義錯誤代碼的位置,但定義位置可能不同,大家可以自行查找。不過,錯誤代碼及其含義在各系統平臺定義基本是一致的,不會有太大出入。
問題分析
1. 錯誤代碼分析
使用微軟錯誤代碼查找工具查找錯誤代碼0xC0000005
,結果如下:
PS D:\tools> .\Err_6.4.5.exe C0000005
# for hex 0xc0000005 / decimal -1073741819
ISCSI_ERR_SETUP_NETWORK_NODE iscsilog.h
# Failed to setup initiator portal. Error status is given in
# the dump data.
STATUS_ACCESS_VIOLATION ntstatus.h
# The instruction at 0x%p referenced memory at 0x%p. The
# memory could not be %s.
USBD_STATUS_DEV_NOT_RESPONDING usb.h
# as an HRESULT: Severity: FAILURE (1), FACILITY_NONE (0x0), Code 0x5
# for hex 0x5 / decimal 5
WINBIO_FP_TOO_FAST winbio_err.h
# Move your finger more slowly on the fingerprint reader.
# as an HRESULT: Severity: FAILURE (1), FACILITY_NULL (0x0), Code 0x5
ERROR_ACCESS_DENIED winerror.h
# Access is denied.
# 5 matches found for "C0000005"
經過分析,其中,第5條查找結果(第20行)就是問題的主要原因(主要看定義在<winerror.h>
中的代碼)。
ERROR_ACCESS_DENIED:Access is denied.
表示訪問被拒絕,這是訪問了無權訪問的記憶體地址空間,常見的場景有:
- 空指針
- 數組越界
- 釋放記憶體後產生的野指針
以上場景都會造成未定義行為,並可能拋出異常觸發ERROR_ACCESS_DENIED
錯誤並退出。
2. 代碼調試
在調試模式運行,查看拋出的異常信息如下:
terminate called after throwing an instance of 'std::bad_function_call'
what(): bad_function_call
異常std::bad_function_call
在調用空的函數對象(std::function)時拋出。空的函數對象一般情況是未給函數對象賦值或賦值null。
我們回到問題描述的代碼部分,回調函數的函數對象是 UI 主線程中某個函數將全局函數的指針傳入構造的,initProTestCasesEnvAsync
方法的參數是常量引用,被線程執行的 lambda 函數捕捉其引用,又被線程執行函數內的doWithSetRunning
的 lambda 函數參數捕捉其引用,併在其內調用該函數對象。
經過單行調試,發現異常就是在非同步線程執行該回調函數對象是觸發的。
機智的小伙伴可能已經發現,根據上面描述的變數傳遞關係,最終執行的回調函數對象就是 UI 線程調用initProTestCasesEnvAsync
時傳入callBackFunc
函數指針並構建的局部函數對象的引用。正常一個串列執行的程式,這樣自然沒有問題,在initProTestCasesEnvAsync
返回時已完成callBackFunc
的調用。但若創建回調函數對象與執行該回調函數對象處在不同線程,就會發生局部的回調函數對象因為其上下文的函數非同步執行結束而釋放記憶體,導致執行線程保存的回調函數的引用內部空指針,調用時觸發std::bad_function_call
異常。
問題解決
知道了問題所在,解決起來就很簡單了。在doWithSetRunning
的 labmda 函數參數中,將 _callback 改為基於值的捕捉,使其在此處複製 _callback 函數對象。修改代碼如下:
void TestCaseProject::initProTestCasesEnvAsync(const std::function<void(std::vector<std::pair<std::string, Json::Value>>)>& _callback) {
std::thread t{[&] () {
doWithSetRunning([&, _callback]() {
auto result = this->initProTestCasesEnv();
_callback(result);
});
}};
t.detach();
}
最後,提醒大家一定要註意 lambda 的引用傳遞的正確性,因為小編已經遇到多次這裡的問題,而在非同步場景下就更要註意對象傳遞過程中各對象的傳遞關係與生命周期了。
本文來自博客園,作者:_哲思,轉載請註明原文鏈接:https://www.cnblogs.com/zhe-si/p/16104721.html