CEF线程之multi_threaded_message_loop参数

发布时间:2023年12月28日

JS调用C++方法,OnQuery消息传递线程过程详解

之前的文章已经提到过JS调用C++方法的方式,我在开发的过程中碰到一个问题需要验证:
从JS发起的过程,到C++,也就是Browser进程这边,是不是都是同一个线程处理?

首先先琢磨了一下这个消息传递过程:
SimpleHandler::OnProcessMessageReceived
调用
CefMessageRouterBrowserSide的OnProcessMessageReceived:

bool SimpleHandler::OnProcessMessageReceived(
    CefRefPtr<CefBrowser> browser,
    CefRefPtr<CefFrame> frame,
    CefProcessId source_process,
    CefRefPtr<CefProcessMessage> message) {
    CEF_REQUIRE_UI_THREAD();
    std::cout << "my_browser" << std::endl;
    if (message_router_->OnProcessMessageReceived(browser, frame, source_process,
        message)) {

        return true;
    }
}

接下来就是看一下OnProcessMessageReceived这个函数干了啥:
在CEF LIB代码的cef_message_router.cc文件中,这个文件定义了CefMessageRouterBrowserSide和CefMessageRouterRenderSide两个类:
基本差不多,我代码里是先用到了CefMessageRouterBrowserSide。

bool OnProcessMessageReceived(CefRefPtr<CefBrowser> browser,
                                CefRefPtr<CefFrame> frame,
                                CefProcessId source_process,
                                CefRefPtr<CefProcessMessage> message) override {
    CEF_REQUIRE_UI_THREAD();

    const std::string& message_name = message->GetName();
    if (message_name == query_message_name_) {
      cmru::RendererMessage content = cmru::ParseRendererMessage(message);
      const int context_id = content.context_id;
      const int request_id = content.request_id;
      const bool persistent = content.is_persistent;

      if (handler_set_.empty()) {
        // No handlers so cancel the query.
        CancelUnhandledQuery(browser, frame, context_id, request_id);
        return true;
      }

      const int browser_id = browser->GetIdentifier();
      const int64_t query_id = query_id_generator_.GetNextId();

      CefRefPtr<CallbackImpl> callback =
          new CallbackImpl(this, browser_id, query_id, persistent,
                           config_.message_size_threshold, query_message_name_);

      // Make a copy of the handler list in case the user adds or removes a
      // handler while we're iterating.
      const HandlerSet handlers = handler_set_;

      Handler* handler = std::visit(
          [&](const auto& arg) -> CefMessageRouterBrowserSide::Handler* {
            for (auto handler : handlers) {
              bool handled = handler->OnQuery(browser, frame, query_id, arg,
                                              persistent, callback.get());
              if (handled) {
                return handler;
              }
            }
            return nullptr;
          },
          content.payload);

      // If the query isn't handled nothing should be keeping a reference to
      // the callback.
      DCHECK(handler != nullptr || callback->HasOneRef());

      if (handler) {
        // Persist the query information until the callback executes.
        // It's safe to do this here because the callback will execute
        // asynchronously.
        QueryInfo* info =
            new QueryInfo{browser,    frame,    context_id, request_id,
                          persistent, callback, handler};
        browser_query_info_map_.Add(browser_id, query_id, info);
      } else {
        // Invalidate the callback.
        callback->Detach();

        // No one chose to handle the query so cancel it.
        CancelUnhandledQuery(browser, frame, context_id, request_id);
      }

      return true;
    } else if (message_name == cancel_message_name_) {
      CefRefPtr<CefListValue> args = message->GetArgumentList();
      DCHECK_EQ(args->GetSize(), 2U);

      const int browser_id = browser->GetIdentifier();
      const int context_id = args->GetInt(0);
      const int request_id = args->GetInt(1);

      CancelPendingRequest(browser_id, context_id, request_id);
      return true;
    }

    return false;
  }

在消息传递那一篇说到的两个默认方法名OnQuery的就在这里设置:

  explicit CefMessageRouterBrowserSideImpl(const CefMessageRouterConfig& config)
      : config_(config),
        query_message_name_(config.js_query_function.ToString() +
                            kMessageSuffix),
        cancel_message_name_(config.js_cancel_function.ToString() +
                             kMessageSuffix) {}

在上面的代码中,OnProcessMessageReceived主要就是调用了Handle的Onquery方法。
而这个Handle类的定义为(在cef_message_route.h文件中):

  class Handler {
   public:
    using Callback = CefMessageRouterBrowserSide::Callback;

    ///
    /// Executed when a new query is received. |query_id| uniquely identifies
    /// the query for the life span of the router. Return true to handle the
    /// query or false to propagate the query to other registered handlers, if
    /// any. If no handlers return true from this method then the query will be
    /// automatically canceled with an error code of -1 delivered to the
    /// JavaScript onFailure callback. If this method returns true then a
    /// Callback method must be executed either in this method or asynchronously
    /// to complete the query.
    ///
    virtual bool OnQuery(CefRefPtr<CefBrowser> browser,
                         CefRefPtr<CefFrame> frame,
                         int64_t query_id,
                         const CefString& request,
                         bool persistent,
                         CefRefPtr<Callback> callback) {
      return false;
    }

所以,我们的MessageHandle类就是继承了这个Handle接口(之前的文章提到过):

class MessageHandler : public CefMessageRouterBrowserSide::Handler{...}

bool MessageHandler::OnQuery(CefRefPtr<CefBrowser> browser,
    CefRefPtr<CefFrame> frame,
    int64 query_id,
    const CefString& request,
    bool persistent,
    CefRefPtr<Callback> callback)
{
    // record frame
    currentFrame = frame;

    // Only handle messages from the test URL.
    const std::string& url = frame->GetURL();

    const char kTestMessageName[] = "demoTest";
    
    const std::string& message_name = request;
    if (message_name == kTestMessageName)
    {
        dosomething();
    }
}

这样就实现了JS消息的调用。

也就是说第一个问题:
从JS发起的消息,到render进程,再到Browser进程,再到MessageHandle处理,都是一个串行的过程。而且JS多次发起,也都是同一个线程执行。

第二个问题就出来了,因为我执行程序的时候,multi_threaded_message_loop设置的是false,
。如果multi_threaded_message_loop设置为true,是不是可以实现多线程?

CefSettings.multi_threaded_message_loop参数

在CEF的wiki上有这么一段来描述multi_threaded_message_loop参数。
TID_UI thread is the main thread in the browser process. This thread will be the same as the main application thread if CefInitialize() is called with a CefSettings.multi_threaded_message_loop value of false.

Set CefSettings.multi_threaded_message_loop = true (Windows and Linux only). This will cause CEF to run the browser UI thread on a separate thread from the main application thread. With this approach neither CefDoMessageLoopWork() nor CefRunMessageLoop() need to be called. CefInitialize() and CefShutdown() should still be called on the main application thread. You will need to provide your own mechanism for communicating with the main application thread (see for example the message window usage in cefclient_win.cpp). You can test this mode in cefclient on Windows or Linux by running with the “–multi-threaded-message-loop” command-line flag.

这一段的意思就是说multi_threaded_message_loop为false的时候,就不需要创建一个专门的UI线程来作为主线程,当前进程的主线程就可以作为UI线程来使用。

这里有一个关键点在于,进程自己创建的主线程是参与了windows程序自己的消息循环的,由操作系统来传递和维护这个消息循环。

如果multi_threaded_message_loop这个参数设置为true的话,那么进程就会创建一个新的UI线程作为Browser管理线程(上一篇有提到过)。那么Browser之间的消息如何与主线程之间连接上,这就需要自己指定。

上一篇的初始化过程是CEF框架中最简单的例子CEFSimple中的,在另一个例子CEFClient中就有如何使用multi_threaded_message_loop参数的方法(cefcilent_win.cc):

  // Create the main context object.
  scoped_ptr<MainContextImpl> context(new MainContextImpl(command_line, true));

  CefSettings settings;

  // Create the main message loop object.
  scoped_ptr<MainMessageLoop> message_loop;
  if (settings.multi_threaded_message_loop)
    message_loop.reset(new MainMessageLoopMultithreadedWin);
  else if (settings.external_message_pump)
    message_loop = MainMessageLoopExternalPump::Create();
  else
    message_loop.reset(new MainMessageLoopStd);

  // Initialize CEF.
  context->Initialize(main_args, settings, app, sandbox_info);

  // Register scheme handlers.
  test_runner::RegisterSchemeHandlers();

  RootWindowConfig window_config;
  window_config.always_on_top = command_line->HasSwitch(switches::kAlwaysOnTop);
  window_config.with_controls =
      !command_line->HasSwitch(switches::kHideControls);
  window_config.with_osr = settings.windowless_rendering_enabled ? true : false;

  // Create the first window.
  context->GetRootWindowManager()->CreateRootWindow(window_config);

  // Run the message loop. This will block until Quit() is called by the
  // RootWindowManager after all windows have been destroyed.
  int result = message_loop->Run();

  // Shut down CEF.
  context->Shutdown();

  // Release objects in reverse order of creation.
  message_loop.reset();
  context.reset();

这段代码复杂一些,但其实基本逻辑还是和上一篇提到的CEFSimple的代码逻辑是一样的。

  • cefclient把MainContextImpl弄出来做了一些设定,而cefsimple中使用的是默认值。
  • 另外一个关键点就是cefsimple中使用了chromium中最基本的消息循环,RunLoop.run()来启动线程中的循环。而cefclient中是对这个做了一些包装,然后再启动这个RunLoop.run()来启动循环,这些包装就是如何与主线程进行沟通的方法。
  • 有两个参数来确定具体使用什么样的循环方式:multi_threaded_message_loop与external_message_pump。

multi_threaded_message_loop

这种方式上面已经提到了,就是把UI线程与进程的主线程分开。在cefclient里使用的是MainMessageLoopMultithreadedWin类(在CEF LIB的main_message_loop_multithreaded_win.cc)。

这个类是继承的MainMessageLoop, 这个是一个接口类。
class MainMessageLoopMultithreadedWin : public MainMessageLoop {…}

MainMessageLoopMultithreadedWin定义的Run方法为:

int MainMessageLoopMultithreadedWin::Run() {
  DCHECK(RunsTasksOnCurrentThread());

  HINSTANCE hInstance = ::GetModuleHandle(nullptr);

  {
    base::AutoLock lock_scope(lock_);

    // Create the hidden window for message processing.
    message_hwnd_ = CreateMessageWindow(hInstance);
    CHECK(message_hwnd_);

    // Store a pointer to |this| in the window's user data.
    SetUserDataPtr(message_hwnd_, this);

    // Execute any tasks that are currently queued.
    while (!queued_tasks_.empty()) {
      PostTaskInternal(queued_tasks_.front());
      queued_tasks_.pop();
    }
  }

  HACCEL hAccelTable =
      LoadAccelerators(hInstance, MAKEINTRESOURCE(IDR_MAINFRAME));

  MSG msg;

  // Run the application message loop.
  while (GetMessage(&msg, nullptr, 0, 0)) {
    // Allow processing of dialog messages.
    if (dialog_hwnd_ && IsDialogMessage(dialog_hwnd_, &msg)) {
      continue;
    }

    if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg)) {
      TranslateMessage(&msg);
      DispatchMessage(&msg);
    }
  }

  {
    base::AutoLock lock_scope(lock_);

    // Destroy the message window.
    DestroyWindow(message_hwnd_);
    message_hwnd_ = nullptr;
  }

  return static_cast<int>(msg.wParam);
}

搞过WIN32程序的朋友一下就可以看出,下面的这段代码就是从windows的消息队列中拿取消息,接入windows的消息循环,把UI线程和主线程关联起来,相当于把这个线程也并入到操作系统的消息队列中去。

while (GetMessage(&msg, nullptr, 0, 0)) {
    // Allow processing of dialog messages.
    if (dialog_hwnd_ && IsDialogMessage(dialog_hwnd_, &msg)) {
      continue;
    }

    if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg)) {
      TranslateMessage(&msg);
      DispatchMessage(&msg);
    }
  }

external_message_pump

这个参数是一个不太常用的参数,我大致了解了一下,没有细琢磨。我自己的理解是,这个参数可以由自己来写逻辑,控制chromium中的task什么时候进行分发和处理。

还是看代码吧。

使用的是一个叫MainMessageLoopExternalPump的类,有些版本还区分了MainMessageLoopExternalPumpWin/MainMessageLoopExternalPumpLinux/MainMessageLoopExternalPumpMac作为继承细分。不重要,我们以MainMessageLoopExternalPumpWin和MainMessageLoopExternalPump为主。

MainMessageLoopExternalPumpWin的Run函数:

int MainMessageLoopExternalPumpWin::Run() {
  // Run the message loop.
  MSG msg;
  while (GetMessage(&msg, nullptr, 0, 0)) {
    TranslateMessage(&msg);
    DispatchMessage(&msg);
  }

  KillTimer();

  // We need to run the message pump until it is idle. However we don't have
  // that information here so we run the message loop "for a while".
  for (int i = 0; i < 10; ++i) {
    // Do some work.
    CefDoMessageLoopWork();

    // Sleep to allow the CEF proc to do work.
    Sleep(50);
  }

  return 0;
}

同样,还是集成使用了windows的消息队列,然后在没有消息的时候调用的CefDoMessageLoopWork函数十次。

CefDoMessageLoopWork函数:

void CefDoMessageLoopWork() {
  // Verify that the context is in a valid state.
  if (!CONTEXT_STATE_VALID()) {
    DCHECK(false) << "context not valid";
    return;
  }

  // Must always be called on the same thread as Initialize.
  if (!g_context->OnInitThread()) {
    DCHECK(false) << "called on invalid thread";
    return;
  }

  base::RunLoop run_loop;
  run_loop.RunUntilIdle();
}

官方文档上的描述为:Call CefDoMessageLoopWork() on a regular basis instead of calling CefRunMessageLoop(). Each call to CefDoMessageLoopWork() will perform a single iteration of the CEF message loop. Caution should be used with this approach. Calling the method too infrequently will starve the CEF message loop and negatively impact browser performance. Calling the method too frequently will negatively impact CPU usage. See CefBrowserProcessHandler::OnScheduleMessagePumpWork for advanced usage details. You can test this mode in cefclient by running with the “–external-message-pump” command-line flag.

结合描述和代码来看,我觉得就是得空将chromium的消息队列全部清空一次(RunUntilIdle)。具体内容还有待进一步研究。

MainMessageLoopStd

还有最基本的一类,CEF也做了一下封装,其实就是普通的RunLoop类的一种封装:

MainMessageLoopStd::MainMessageLoopStd() {}

int MainMessageLoopStd::Run() {
  CefRunMessageLoop();
  return 0;
}

void MainMessageLoopStd::Quit() {
  CefQuitMessageLoop();
}

void MainMessageLoopStd::PostTask(CefRefPtr<CefTask> task) {
  CefPostTask(TID_UI, task);
}

bool MainMessageLoopStd::RunsTasksOnCurrentThread() const {
  return CefCurrentlyOn(TID_UI);
}

实验

我在cefclient中做了一个实验,在bing_testing.cc中的OnQuery函数中增加了输出当前线程号的代码:

std::cout << "Thread ID is: " << GetCurrentThreadId() << std::endl;

结果为:

也就是还是同一线程处理。

文章来源:https://blog.csdn.net/pcgamer/article/details/135269012
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。