개요
크롬은 높은 수준의 멀티스레딩을 활용하는 제품입니다. UI 반응성을 최대한 유지하기 위해 UI 스레드에서 블로킹 I/O나 무거운 연산을 수행하지 않습니다. 스레드 간 통신에는 메시지 전달 방식을 사용하며, 락이나 스레드 안전 객체 사용을 지양합니다. 대신 객체는 단일 스레드에만 존재하고, 스레드 간 통신은 메시지를 주고받으며, 대부분의 교차 스레드 요청에는 콜백 인터페이스(메시지 전달 기반)를 활용합니다.
스레드 구조
모든 크롬 프로세스는 다음과 같은 스레드를 가집니다:
- 메인 스레드: 브라우저 프로세스에서는 UI 업데이트를, 렌더러 프로세스에서는 Blink의 주요 기능을 담당합니다.
- IO 스레드: 브라우저 프로세스에서는 IPC 및 네트워크 요청을, 렌더러 프로세스에서는 IPC를 처리합니다.
- 몇 가지 특수 목적 스레드
- 일반 목적 스레드 풀
대부분의 스레드는 큐에서 태스크를 가져와 실행하는 루프를 가지고 있습니다(큐는 여러 스레드가 공유할 수 있음).
태스크 이해하기
base::OnceClosure는 비동기 실행을 위해 큐에 추가되는 태스크입니다. 함수 포인터와 인자를 저장하며, Run() 메서드를 통해 바인딩된 인자로 함수를 호출합니다. base::BindOnce를 사용해 생성합니다.
void TaskA() {}
void TaskB(int v) {}
auto task_a = base::BindOnce(&TaskA);
auto task_b = base::BindOnce(&TaskB, 42);
태스크 그룹은 다음 방식으로 실행됩니다:
- 병렬: 실행 순서 보장 없음, 여러 스레드에서 동시 실행 가능
- 순차: 게시 순서대로 한 번에 하나씩, 모든 스레드에서 실행 가능
- 단일 스레드: 게시 순서대로 한 번에 하나씩, 단일 스레드에서 실행
- COM 단일 스레드: COM이 초기화된 단일 스레드의 변형
단일 스레드보다 순차 실행을 선호
단순한 스레드 안전성만 필요한 경우에는 순차 실행 모드가 단일 스레드보다 훨씬 선호됩니다. 순차 실행은 스레드 간 이동이 가능하여 전용 스레드의 관련 없는 작업에 막히지 않고, 스레드 수를 동적으로 조정할 수 있습니다(대형 머신에서는 병렬성 증가, 소형 머신에서는 리소스 낭비 방지).
많은 핵심 API가 최근 순차 실행에 친화적으로 변경되었습니다. 하지만 코드베이스는 오랫동안 단일 스레드 컨텍스트를 가정하고 진화해왔습니다. 클래스가 순차 실행에서 동작할 수 있지만 ThreadChecker/ThreadTaskRunnerHandle/SingleThreadTaskRunner에 의해 제한된다면, 해당 의존성을 수정하는 것을 고려하세요.
병렬 태스크 게시
태스크 스케줄러에 직접 게시
어떤 스레드에서든 실행 가능하고 다른 태스크와 순서나 상호 배제가 필요 없는 태스크는 base/task/post_task.h에 정의된 base::PostTask*() 함수를 사용합니다.
base::PostTask(FROM_HERE, base::BindOnce(&Task));
base::PostTask*WithTraits() 함수는 TaskTraits를 통해 추가 정보를 제공할 수 있습니다.
base::PostTaskWithTraits(
FROM_HERE, {base::TaskPriority::BEST_EFFORT, MayBlock()},
base::BindOnce(&Task));
TaskRunner를 통한 게시
병렬 TaskRunner는 base::PostTask*()를 직접 호출하는 대안입니다. 태스크가 병렬, 순차, 단일 스레드 중 어떤 방식으로 게시될지 미리 알 수 없을 때 유용합니다.
class A {
public:
A() = default;
void set_task_runner_for_testing(
scoped_refptr<base::TaskRunner> task_runner) {
task_runner_ = std::move(task_runner);
}
void DoSomething() {
task_runner_->PostTask(FROM_HERE, base::BindOnce(&A));
}
private:
scoped_refptr<base::TaskRunner> task_runner_ =
base::CreateTaskRunnerWithTraits({base::TaskPriority::USER_VISIBLE});
};
테스트에서 태스크 실행 방식을 정밀하게 제어할 필요가 없다면 base::PostTask*()를 직접 호출하는 것이 선호됩니다.
순차 태스크 게시
시퀀스는 게시 순서대로 한 번에 하나씩 실행되는 태스크 집합입니다(반드시 같은 스레드일 필요 없음). SequencedTaskRunner를 사용합니다.
새 시퀀스에 게시
scoped_refptr<SequencedTaskRunner> sequenced_task_runner =
base::CreateSequencedTaskRunnerWithTraits(...);
// TaskB는 TaskA 완료 후 실행됩니다.
sequenced_task_runner->PostTask(FROM_HERE, base::BindOnce(&TaskA));
sequenced_task_runner->PostTask(FROM_HERE, base::BindOnce(&TaskB));
현재 시퀀스에 게시
현재 태스크가 게시된 SequencedTaskRunner는 SequencedTaskRunnerHandle::Get()으로 얻을 수 있습니다.
// 현재 태스크가 게시된 SequencedTaskRunner에 이미 게시된
// 모든 태스크 이후에 실행됩니다.
base::SequencedTaskRunnerHandle::Get()->
PostTask(FROM_HERE, base::BindOnce(&Task));
참고: 병렬 태스크에서 SequencedTaskRunnerHandle::Get()을 호출하는 것은 유효하지 않지만, 단일 스레드 태스크에서는 유효합니다.
락 대신 시퀀스 사용하기
크롬에서는 락 사용을 권장하지 않습니다. 시퀀스는 본질적으로 스레드 안전성을 제공합니다. 락으로 직접 스레드 안전성을 관리하는 대신 항상 같은 시퀀스에서 접근되는 클래스를 선호하세요.
class A {
public:
A() {
DETACH_FROM_SEQUENCE(sequence_checker_);
}
void AddValue(int v) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
values_.push_back(v);
}
private:
SEQUENCE_CHECKER(sequence_checker_);
std::vector<int> values_;
};
A a;
scoped_refptr<SequencedTaskRunner> task_runner_for_a = ...;
task_runner_for_a->PostTask(FROM_HERE,
base::BindOnce(&A::AddValue, base::Unretained(&a), 42));
task_runner_for_a->PostTask(FROM_HERE,
base::BindOnce(&A::AddValue, base::Unretained(&a), 27));
락은 여러 스레드에서 접근 가능한 공유 데이터 구조를 교체할 때만 사용해야 합니다. 한 스레드가 계산이나 디스크 접근을 통해 데이터를 업데이트한다면, 느린 작업은 락을 잡지 않고 수행하고 결과가 준비되었을 때만 락을 사용해 새 데이터로 교체합니다.
같은 스레드에 여러 태스크 게시
여러 태스크가 같은 스레드에서 실행되어야 한다면 SingleThreadTaskRunner에 게시합니다.
브라우저 프로세스의 메인/IO 스레드에 게시
base::PostTaskWithTraits(FROM_HERE, {content::BrowserThread::UI}, ...);
base::CreateSingleThreadTaskRunnerWithTraits({content::BrowserThread::IO})
->PostTask(FROM_HERE, ...);
메인 스레드와 IO 스레드는 이미 매우 바쁩니다. 가능하면 일반 목적 스레드에 게시하는 것을 선호하세요.
커스텀 SingleThreadTaskRunner에 게시
scoped_refptr<SequencedTaskRunner> single_thread_task_runner =
base::CreateSingleThreadTaskRunnerWithTraits(...);
single_thread_task_runner->PostTask(FROM_HERE, base::BindOnce(&TaskA));
single_thread_task_runner->PostTask(FROM_HERE, base::BindOnce(&TaskB));
중요: 대부분의 크롬 클래스는 스레드 친화성(thread-affinity)이 아닌 스레드 안전성(sequences)만 필요합니다. 잘못 스레드 친화적으로 설계된 API를 발견하면 수정을 고려하세요.
현재 스레드에 게시
// 현재 스레드에서 나중에 실행됩니다.
base::ThreadTaskRunnerHandle::Get()->PostTask(
FROM_HERE, base::BindOnce(&Task));
참고: 병렬 또는 순차 태스크에서 ThreadTaskRunnerHandle::Get()을 호출하는 것은 유효하지 않습니다.
COM STA 스레드에 태스크 게시 (Windows)
COM STA 스레드에서 실행되어야 하는 태스크는 CreateCOMSTATaskRunnerWithTraits()가 반환하는 SingleThreadTaskRunner에 게시합니다.
auto com_sta_task_runner = base::CreateCOMSTATaskRunnerWithTraits(...);
com_sta_task_runner->PostTask(FROM_HERE, base::BindOnce(&TaskAUsingCOMSTA));
com_sta_task_runner->PostTask(FROM_HERE, base::BindOnce(&TaskBUsingCOMSTA));
TaskTraits로 태스크 주석 달기
TaskTraits는 태스크 스케줄러가 더 나은 스케줄링 결정을 내리도록 돕는 정보를 캡슐화합니다.
// 명시적 TaskTraits 없음. 블로킹 불가.
base::PostTask(FROM_HERE, base::BindOnce(...));
// 최우선 순위
base::PostTaskWithTraits(
FROM_HERE, {base::TaskPriority::USER_BLOCKING},
base::BindOnce(...));
// 최하위 순위, 블로킹 허용
base::PostTaskWithTraits(
FROM_HERE, {base::TaskPriority::BEST_EFFORT, base::MayBlock()},
base::BindOnce(...));
// 셧다운 블로킹
base::PostTaskWithTraits(
FROM_HERE, {base::TaskShutdownBehavior::BLOCK_SHUTDOWN},
base::BindOnce(...));
// 브라우저 UI 스레드에서 실행
base::PostTaskWithTraits(
FROM_HERE, {content::BrowserThread::UI},
base::BindOnce(...));
브라우저 응답성 유지
메인 스레드, IO 스레드, 또는 낮은 지연시간이 필요한 시퀀스에서 무거운 작업을 수행하지 마세요. 대신 base::PostTaskAndReply*()를 사용해 비동기로 처리합니다.
// 잘못된 예: 메인 스레드 블로킹
AddHistoryItemsToOmniboxDropdown(GetHistoryItemsFromDisk("keyword"));
// 올바른 예: 비동기 처리
base::PostTaskWithTraitsAndReplyWithResult(
FROM_HERE, {base::MayBlock()},
base::BindOnce(&GetHistoryItemsFromDisk, "keyword"),
base::BindOnce(&AddHistoryItemsToOmniboxDropdown));
지연 태스크 게시
일회성 지연 태스크
base::PostDelayedTaskWithTraits(
FROM_HERE, {base::TaskPriority::BEST_EFFORT}, base::BindOnce(&Task),
base::TimeDelta::FromHours(1));
반복 태스크
class A {
public:
void StartDoingStuff() {
timer_.Start(FROM_HERE, TimeDelta::FromSeconds(1),
this, &MyClass::DoStuff);
}
void StopDoingStuff() {
timer_.Stop();
}
private:
void DoStuff() { }
base::RepeatingTimer timer_;
};
태스크 취소
base::WeakPtr 사용
class A {
public:
A() : weak_ptr_factory_(this) {}
void ComputeAndStore() {
base::PostTaskAndReplyWithResult(
FROM_HERE, base::BindOnce(&Compute),
base::BindOnce(&A::Store, weak_ptr_factory_.GetWeakPtr()));
}
private:
void Store(int value) { value_ = value; }
int value_;
base::WeakPtrFactory<A> weak_ptr_factory_;
};
base::CancelableTaskTracker 사용
auto task_runner = base::CreateTaskRunnerWithTraits(base::TaskTraits());
base::CancelableTaskTracker cancelable_task_tracker;
cancelable_task_tracker.PostTask(task_runner.get(), FROM_HERE,
base::DoNothing());
cancelable_task_tracker.TryCancelAll();
테스트
class MyTest : public testing::Test {
protected:
base::test::ScopedTaskEnvironment scoped_task_environment_;
};
TEST(MyTest, MyTest) {
base::ThreadTaskRunnerHandle::Get()->PostTask(FROM_HERE, base::BindOnce(&A));
base::SequencedTaskRunnerHandle::Get()->PostTask(FROM_HERE, base::BindOnce(&B));
base::RunLoop().RunUntilIdle();
base::RunLoop run_loop;
base::ThreadTaskRunnerHandle::Get()->PostTask(FROM_HERE, run_loop.QuitClosure());
run_loop.Run();
base::PostTaskWithTraits(FROM_HERE, base::TaskTraits(), base::BindOnce(&F));
base::TaskScheduler::GetInstance()->FlushForTesting();
scoped_task_environment_.RunUntilIdle();
}
새 프로세스에서 TaskScheduler 사용
// 기본 파라미터로 초기화 및 시작
base::TaskScheduler::CreateAndStartWithDefaultParams("process_name");
// 또는 수동 초기화
base::TaskScheduler::Create("process_name");
base::TaskScheduler::GetInstance()->Start(params);
// 종료
base::TaskScheduler::GetInstance()->Shutdown();
TaskRunner 소유권
TaskRunner는 여러 컴포넌트를 통해 전달되지 않아야 합니다. TaskRunner를 사용하는 컴포넌트가 직접 생성해야 합니다. 테스트를 위한 의존성 주입은 드물게 필요할 수 있으며, 다음과 같이 구현합니다:
class FooWithCustomizableTaskRunnerForTesting {
public:
void SetBackgroundTaskRunnerForTesting(
scoped_refptr<base::SequencedTaskRunner> background_task_runner);
private:
scoped_refptr<base::SequencedTaskRunner> background_task_runner_ =
base::CreateSequencedTaskRunnerWithTraits(
{base::MayBlock(), base::TaskPriority::BEST_EFFORT});
};