Многопоточность в браузере. Модель акторов —...
DESCRIPTION
Я расскажу о том, как наладить многопоточное взаимодействие в браузере без использования мьютексов и условных переменных. Доклад будет интересен тем, кто пишет на языке C++ и разрабатывает многопоточные сервисы и приложения.TRANSCRIPT
Многопоточность в браузере. Модель акторов Константин Крамлих
Инициализируем плеер
void Player::Init() { OpenFile(); InitializeDecoder(); InitializeRenderer(); StartDecodeLoop(); StartDisplayLoop(); }
Читаем и показываем кадры
// Producer thread void Player::DecodeLoop() { ... while (!end) { ... decoder->ReadFrame(&frame); queue->Push(frame); ... } }
// Consumer thread void Player::DisplayLoop() { while (!end) { ... queue->Pop(&frame); if (!frame) continue; end = frame->IsLastFrame(); ShowFrame(frame); ... } }
Почему так нельзя
▌ Греем воздух
▌ Ненужная очередь
▌ Мьютексы/Условные переменные для синхронизации
8
9
Требования к браузеру
▌ Отзывчивый UI
▌ Скорость работы
▌ Выполнение большого количества задач одновременно
10
Основные потоки в браузере
▌ UI thread
▌ IO thread
▌ File thread
▌ DB thread
▌ SafeBrowsing thread
▌ History thread
11
12
Actor Actor
Actor Actor
Main thread
Важные классы в Chromium
▌ Callback & Closure
▌ TaskRunner & SequencedTaskRunner & SingleThreadTaskRunner
▌ MessageLoop & MessageLoopProxy
▌ Thread & PlatformThread
▌ BrowserThread
13
Callback & Closure
void DoSomeJob(base::Callback<void (bool)> result_cb); void OnJobCompleted(bool success); void Foo() { DoSomeJob(base::Bind(&OnJobCompleted)); } void OnJobCompletedExtraData(const std::string& extra_data, bool success); void Bar() { DoSomeJob(base::Bind(&OnJobCompletedWithExtraData, "Called from Bar")); }
14
Почему не std::function
▌ std::function еще не было
▌ Функционал std::function шире
▌ std::function делает копию внутреннего состояния при копировании
▌ Неявные касты вида std::function<int(int)> -> std::function<void(int)>
MessageLoop & MessageLoopProxy
void DoSomeJob(base::Callback<void (bool)> result_cb); void OnJobCompleted(bool success); void Bar() { MessageLoopProxy::current()->PostTask(
FROM_HERE, base::Bind(&DoSomeJob(base::Bind(&OnJobCompleted)))); }
16
Thread & PlatformThread
void Bar(); void Foo() { base::Thread thread("MyWorkerThread"); thread.message_loop_proxy()->PostTask(
FROM_HERE, base::Bind(&Bar));
}
BrowserThread
void Foo(int); void Bar() { BrowserThread::PostTask(BrowserThread::IO, FROM_HERE, base::Bind(&Foo, 42)); }
base::Bind для методов классов
class SomeClass : public base::RefCountedThreadSafe<SomeClass> { public: void A() {}; void B() { base::Callback<void()> cb = base::Bind(&Foo::A, this); cb.Run(); } };
base::Bind для методов классов
class SomeClass { public: void A() {}; void B() { base::Callback<void()> cb = base::Bind(&Foo::A, base::Unretained(this)); cb.Run(); } };
Удаление на нужном потоке
class SomeClass : public base::RefCountedThreadSafe<SomeClass, BrowserThread::DeleteOnIOThread> { ... }
Обнаружение опасности void SafeBrowsingResourceThrottle::OnCheckBrowseUrlResult( const GURL& url, SBThreatType threat_type, const std::string& metadata) { ... SafeBrowsingUIManager::UnsafeResource resource; resource.url = url; ... resource.callback = base::Bind( &SafeBrowsingResourceThrottle::OnBlockingPageComplete, AsWeakPtr()); content::BrowserThread::PostTask( content::BrowserThread::UI, FROM_HERE, base::Bind(&SafeBrowsingResourceThrottle::StartDisplayingBlockingPage, AsWeakPtr(), ui_manager_, resource)); }
Блокировка страницы void SafeBrowsingUIManager::DisplayBlockingPage( const UnsafeResource& resource) { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); ... if (IsWhitelisted(resource)) { if (!resource.callback.is_null()) { BrowserThread::PostTask( BrowserThread::IO, FROM_HERE, base::Bind(resource.callback, true)); } return; } ... SafeBrowsingBlockingPage::ShowBlockingPage(this, resource); }
Завершение блокировки void SafeBrowsingUIManager::OnBlockingPageDone( const std::vector<UnsafeResource>& resources, bool proceed) { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO)); for (std::vector<UnsafeResource>::const_iterator iter = resources.begin(); iter != resources.end(); ++iter) { const UnsafeResource& resource = *iter; if (!resource.callback.is_null()) resource.callback.Run(proceed); if (proceed) { BrowserThread::PostTask( BrowserThread::UI, FROM_HERE, base::Bind(&SafeBrowsingUIManager::UpdateWhitelist, this, resource)); } } }
Выводы
▌ Данные привязаны к конкретным потокам
▌ Нет нужды в мьютексах
▌ Проверяем, что работаем на нужном потоке
Привязка к нужному потоку #define BIND_TO_RENDER_LOOP(function) \ (DCHECK(main_task_runner_->BelongsToCurrentThread()), \ BindToCurrentLoop(base::Bind(function, AsWeakPtr()))) void WebMediaPlayerImpl::StartPipeline() { DCHECK(main_task_runner_->BelongsToCurrentThread()); ... pipeline_.Start( demuxer_.get(), CreateRenderer(), BIND_TO_RENDER_LOOP(&WebMediaPlayerImpl::OnPipelineEnded), BIND_TO_RENDER_LOOP(&WebMediaPlayerImpl::OnPipelineError), BIND_TO_RENDER_LOOP1(&WebMediaPlayerImpl::OnPipelineSeeked, false), BIND_TO_RENDER_LOOP(&WebMediaPlayerImpl::OnPipelineMetadata), BIND_TO_RENDER_LOOP(&WebMediaPlayerImpl::OnPipelineBufferingStateChanged), BIND_TO_RENDER_LOOP(&WebMediaPlayerImpl::OnDurationChanged), BIND_TO_RENDER_LOOP(&WebMediaPlayerImpl::OnAddTextTrack)); } 26
Неявное использование на другом потоке
void Pipeline::BufferingStateChanged(BufferingState new_buffering_state) { DVLOG(1) << __FUNCTION__ << "(" << new_buffering_state << ") ”; DCHECK(task_runner_->BelongsToCurrentThread()); buffering_state_cb_.Run(new_buffering_state); }
Скрытие потоков исполнения virtual void QueryForMatches( const std::set<SourceFrameRef>& candidates, const MatchesCallback& results_callback) OVERRIDE { BrowserThread::PostTask( BrowserThread::UI, FROM_HERE, base::Bind(&MuteDestination::QueryForMatchesOnUIThread, this, candidates, media::BindToCurrentLoop(results_callback))); } void QueryForMatchesOnUIThread(const std::set<SourceFrameRef>& candidates, const MatchesCallback& results_callback) { ... results_callback.Run(matches); }
Выводы
▌ Не нужно думать о том, на каком потоке нужно дергать коллбеки
▌ Скрываем информацию об исполняющем потоке
Инициализируем плеер void Player::Init() { OpenFile(); InitializeDecoder(); InitializeRenderer(); StartDecodeLoop(); StartDisplayLoop(); }
30
Читаем и показываем кадры // Producer thread void Player::DecodeLoop() { ... while (!end) { ... decoder->ReadFrame(&frame); queue->Push(frame); ... } }
// Consumer thread void Player::DisplayLoop() { while (!end) { ... queue->Pop(&frame); if (!frame) continue; end = frame->IsLastFrame(); ShowFrame(frame); ... } }
Инициализируем плеер
void Player::Init() { OpenFile(); base::Callback<void(Frame)> show_frame_cb = Bind(&ShowFrame, this); InitializeDecoder(BindToCurrentLoop(show_frame_cb)); InitializeRenderer(); StartDecodeLoop(); }
Читаем и отдаем кадры // Producer thread void Decoder::DecodeLoop() { ... while (!end) { ... decoder->ReadFrame(&frame); frame_ready_cb.Run(frame); ... } }
class Account { double balance; int id; void withdraw(double amount) { balance -= amount; } void deposit(double amount) { balance += amount; } void transfer(Account from, Account to, double amount) { sync(from); sync(to); from.withdraw(amount); to.deposit(amount); release(to); release(from); } }
Избегаем dedlock-и
Плюсы
▌ Данные привязаны к конкретным потокам, не требуются примитивы синхронизации
▌ Можно скрыть знание того, на каком потоке будет выполняться задача, а также поток выполнения коллбека
▌ Проверяем поток, на котором происходит выполнение
Минусы
▌ Код должен быть асинхронным
▌ Теряем возможность использования исключений
▌ В некоторых случаях использование мьютексов может быть быстрее
36
Полезные ссылки
▌ http://www.chromium.org/developers/coding-style/important-abstractions-and-data-structures
▌ http://www.chromium.org/developers/design-documents/threading
▌ https://en.wikipedia.org/wiki/Actor_model
37
Спасибо за внимание!
38