c++異步回調函數引用傳遞空指針異常


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>

    image-20220405233306811

    Windows錯誤代碼詳情可見官方文檔說明。同時,微軟官方提供了一個錯誤代碼的含義查找工具,下載鏈接

  • 舉一反三,在其他操作系統上也有定義錯誤代碼的位置,但定義位置可能不同,大家可以自行查找。不過,錯誤代碼及其含義在各系統平台定義基本是一致的,不會有太大出入。

問題分析

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異常。

問題解決

知道了問題所在,解決起來就很簡單了。一個異步執行的線程,除了全局、動態分配的內存等非局部的對象可共享內存數據進行讀寫,局部數據都要進行數據拷貝以實現隔離。基於上面的理論,這里應該是 UI 線程調用時傳來的局部變量執行拷貝,動態申請的對象直接引用。由於實際執行的代碼體中只使用了this指針和傳來的函數參數,所以都執行拷貝即可。

在任務線程的 lambda 執行函數中,將捕捉引用改為捕捉值,內部的doWithSetRunning的 lambda 執行函數捕捉該異步線程捕捉值得到的拷貝的引用,即可實現非局部變量(this指向的內存對象)的共享與局部變量(_callback 回調函數對象)的隔離。

修改代碼如下:

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();
}

最后,提醒大家一定要注意 lambda 的引用傳遞的正確性,因為小編已經遇到多次這里的問題,而在異步場景下就更要注意對象傳遞過程中各對象的傳遞關系與生命周期了。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM