Chrome의 스레딩과 태스크 처리 방식

개요

크롬은 높은 수준의 멀티스레딩을 활용하는 제품입니다. 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를 통한 게시

병렬 TaskRunnerbase::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));

현재 시퀀스에 게시

현재 태스크가 게시된 SequencedTaskRunnerSequencedTaskRunnerHandle::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});
};

태그: Chrome 멀티스레딩 TaskScheduler base::BindOnce SequencedTaskRunner

6월 25일 22:52에 게시됨