1. 程式人生 > >Chromium的無鎖執行緒模型C++程式碼示例

Chromium的無鎖執行緒模型C++程式碼示例

引言

作者:程式設計師bingo,主要關注客戶端架構設計、效能優化、崩潰處理,有多年的Chromium瀏覽器開發經驗。

多執行緒一直是軟體開發中最容易出問題的環節,很多的崩潰、卡死問題都與多執行緒有關。在常用的執行緒模型中,一般會使用執行緒鎖保證執行緒資料安全,但是,在實踐中,這種模式很容易造成漏加鎖、鎖粒度太大、死鎖等問題。

要解決這類問題,一種比較好的方式是採用無鎖的執行緒模型,Chromium就是採用了這種執行緒模型。本文通過Electron基於Chromium執行緒模型實現的開啟檔案對話方塊功能,介紹無鎖執行緒模型的思路。

無鎖執行緒模型簡介

應用層資料不加鎖

chromium的無鎖執行緒模型,不是指完全的不使用執行緒鎖,因為底層的Task佇列是有加鎖的,而是指在應用層使用時,不需要新增執行緒鎖。

不同執行緒不會同時訪問資料

無鎖執行緒模型,主要是保證在同一時間,不同執行緒在同一時間不會同時訪問相同的資料。下面的例子要用到的方法,主要是對不同執行緒訪問資料的能力進行隔離。對資料訪問能力隔離方式主要有

  • 拷貝,在不同執行緒傳遞資料時,對資料進行一份拷貝,讓兩個執行緒訪問的是不同資料。
  • 移動,在不同執行緒傳遞資料時,使用std::move進行右值轉移,讓原執行緒無法訪問這個資料。

無鎖執行緒模型示例

下面以Electron的dialog.showOpenDialog實現程式碼為例,說明Chromium的無鎖執行緒模型的使用原理。

dialog.showOpenDialog的呼叫

Electron的介面函式dialog.showOpenDialog是一個非同步的JavaScript函式,會返回一個promise。showOpenDialog會彈出一個檔案選擇對話方塊,使用者選擇檔案之後,把檔案路徑通過result.filePaths返回。

dialog.showOpenDialog(mainWindow, {
  properties: ['openFile']
}).then(result => {
  console.log(result.canceled)
  console.log(result.filePaths)
}).catch(err => {
  console.log(err)
})

Chromium執行緒相關的幾個基本概念

  • TaskRunner:每一個執行緒有一個TaskRunner,主要通過PostTask把任務投放到執行緒的任務佇列,通過執行緒安全的引用技術管理生命週期,可配合scoped_refptr在不同執行緒使用。
  • PostTask:TaskRunner的一個函式,可向該執行緒的任務佇列中傳送一個閉包,閉包會在該執行緒中執行。

相關程式碼如下:

class BASE_EXPORT TaskRunner
    : public RefCountedThreadSafe<TaskRunner, TaskRunnerTraits> {
 public:
  bool PostTask(const Location& from_here, OnceClosure task);
}

C++程式碼響應JavaScript呼叫

在JavaScript呼叫dialog.showOpenDialog之後,會在UI執行緒中,呼叫到C++程式碼的ShowOpenDialog函式。ShowOpenDialog函式主要是建立一個新的Dialog執行緒,然後通過PostTask把RunOpenDialogInNewThread函式拋到這個Dialog執行緒去執行。這個過程中,需要處理所有權的引數有3個,run_state、settings和promise。

  • run_state、settings是通過拷貝方式隔離不同執行緒的訪問權。run_state除了儲存有Dialog執行緒的指標外,還有UI執行緒的TaskRunner,用於後續Dialog執行緒往UI執行緒傳送回撥函式。
  • promise是通過std::move進行所有權轉移,轉移之後,就只有Dialog執行緒的函式RunOpenDialogInNewThread有權訪問,UI執行緒暫時無許可權訪問。

相關程式碼如下:

struct RunState {
  base::Thread* dialog_thread;
  scoped_refptr<base::SingleThreadTaskRunner> ui_task_runner;
};

bool CreateDialogThread(RunState* run_state) {
  auto thread =
      std::make_unique<base::Thread>(ELECTRON_PRODUCT_NAME "FileDialogThread");
  thread->init_com_with_mta(false);
  if (!thread->Start())
    return false;

  run_state->dialog_thread = thread.release();
  run_state->ui_task_runner = base::ThreadTaskRunnerHandle::Get();
  return true;
}

void ShowOpenDialog(const DialogSettings& settings,
                    gin_helper::Promise<gin_helper::Dictionary> promise) {
  gin_helper::Dictionary dict = gin::Dictionary::CreateEmpty(promise.isolate());
  RunState run_state;
  if (!CreateDialogThread(&run_state)) {
    dict.Set("canceled", true);
    dict.Set("filePaths", std::vector<base::FilePath>());
    promise.Resolve(dict);
  } else {
    run_state.dialog_thread->task_runner()->PostTask(
        FROM_HERE, base::BindOnce(&RunOpenDialogInNewThread, run_state,
                                  settings, std::move(promise)));
  }
}

Dialog執行緒執行任務,並把結果返回給UI執行緒

RunOpenDialogInNewThread函式會在Dialog執行緒中執行,它通過ShowOpenDialogSync函式獲取到選中的檔案路徑paths,並通過拷貝的方式,返回結果result和paths。

這時,promise的所有權再次通過std::move進行了轉移,轉移之後,只有UI執行緒的OnDialogOpened函式有權訪問。

相關程式碼如下:

void RunOpenDialogInNewThread(
    const RunState& run_state,
    const DialogSettings& settings,
    gin_helper::Promise<gin_helper::Dictionary> promise) {
  std::vector<base::FilePath> paths;
  bool result = ShowOpenDialogSync(settings, &paths);
  run_state.ui_task_runner->PostTask(
      FROM_HERE,
      base::BindOnce(&OnDialogOpened, std::move(promise), !result, paths));
  run_state.ui_task_runner->DeleteSoon(FROM_HERE, run_state.dialog_thread);
}

UI執行緒處理返回結果

此時,回到了UI執行緒,OnDialogOpened函式對promise進行處理後,返回給JavaScript的promise的處理結果,最終會回到JavaScript的promise的then函式。

相關程式碼如下:

void OnDialogOpened(gin_helper::Promise<gin_helper::Dictionary> promise,
                    bool canceled,
                    std::vector<base::FilePath> paths) {
  gin_helper::Dictionary dict = gin::Dictionary::CreateEmpty(promise.isolate());
  dict.Set("canceled", canceled);
  dict.Set("filePaths", paths);
  promise.Resolve(dict);
}

處理流程

promise的可訪問權,是跟著showOpenDialog處理順序進行變化,執行屬性如下:

  • 在UI執行緒執行函式:ShowOpenDialog
  • 在Dialog執行緒執行函式:RunOpenDialogInNewThread
  • 在UI執行緒執行函式:OnDialogOpened

相關流程圖如下:

小結

上面通過Electron的dialog.showOpenDailog,介紹了Chromium的無鎖執行緒模型的一些使用思路。這個例子是通過拷貝和移動語義來保證不同執行緒無法同時對同一變數進行訪問,從而不需要加鎖。如果能夠正確使用這種執行緒模型,是可以消除因為資料鎖帶來的一些執行緒同步問題。

關於作者

微信公眾號:程式設計師bingo

Blog: https://bingoli.github.io/
GitHub: https://github.com/bin