Chromium 无锁线程模型示例之 PostTask
本文通过 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 的无锁线程模型的一些使用思路。这个例子是通过拷贝和移动语义来保证不同线程无法同时对同一变量进行访问,从而不需要加锁。如果能够正确使用这种线程模型,是可以消除因为数据锁带来的一些线程同步问题。