深入V8引擎-預設Platform之mac篇(2)

書生小龍發表於2019-06-04

  先說結論,V8引擎在預設Platform中初始化的這個執行緒是用於處理類似於setTimeout的延時任務。

  另外附一些圖,包括繼承樹、關鍵屬性歸屬、純邏輯工作流程,對程式碼木得興趣的看完圖可以X掉了。

  上一篇講了V8初始化預設Platform物件時會做三件事,其中生成空白DefaultPlatform、獲取執行緒池大小已經講過了,剩下執行緒啟動相關的內容。

  寫之前花了10幾分鐘學了下mac下C++的執行緒,對API有一個初步瞭解,給一個簡單的例子,大概流程如下。

// V8原始碼中設定的stack_size 在測試demo中不好使
const int stack_size = 1 * 1024 * 512;
int tmp = 0;

// 執行緒的任務 引數來源於建立時的第四個引數
void* add(void* number){
  tmp = tmp + *(int*)number;
  printf("tmp: %i\n", tmp);
  return nullptr;
};

int main(int argc, const char * argv[]) {
  // 建立執行緒物件
  pthread_t pt;
  // 建立執行緒屬性
  pthread_attr_t attr;
  memset(&attr, 0, sizeof(attr));
  pthread_attr_init(&attr);
  // 設定屬性的size
  pthread_attr_setstacksize(&attr, stack_size);
  // 函式引數
  int num = 5;
  int* ptr = #
  // 生成一個執行緒
  // 引數列表參照各個變數
  int ret = pthread_create(&pt, &attr, add, ptr);
  if(ret != 0) printf("cannot create thread");
  return 0;
}

  通過幾個步驟,就可以建立一條執行緒來處理任務,啟動後的輸出就懶得截圖了,反正就是列印一個5。

  有了上面的例子,可以慢慢來看V8初始化時多執行緒的啟動過程,首先是入門方法。

// 3
void DefaultPlatform::EnsureBackgroundTaskRunnerInitialized() {
  // 這裡初始化DefaultPlatform的屬性 需要加鎖
  base::MutexGuard guard(&lock_);
  if (!worker_threads_task_runner_) {
    worker_threads_task_runner_ =
        // 3-2
        std::make_shared<DefaultWorkerThreadsTaskRunner>(
            thread_pool_size_, time_function_for_testing_
                                   ? time_function_for_testing_
                                  // 3-1
                                   : DefaultTimeFunction);
  }
}

// 3-1
double DefaultTimeFunction() {
  return base::TimeTicks::HighResolutionNow().ToInternalValue() /
         static_cast<double>(base::Time::kMicrosecondsPerSecond);
}

  if中的worker_threads_task_runner是DefaultPlatform的私有屬性,由於初始化時預設值為NULL,這裡做一個定義賦值。第一個引數是在第二步獲取的執行緒池大小,第二個引數是一個計數方法,預設引用之前Time模組裡的東西,返回硬體時間戳,具體實現可以看我之前寫的。

  接下來看DefaultWorkerThreadsTaskRunner類的建構函式,接受2個引數。

// 3-2
// queue_ => DelayedTaskQueue::DelayedTaskQueue(TimeFunction time_function) : time_function_(time_function) {}
DefaultWorkerThreadsTaskRunner::DefaultWorkerThreadsTaskRunner(
    uint32_t thread_pool_size, TimeFunction time_function)
    : queue_(time_function),
      time_function_(time_function),
      thread_pool_size_(thread_pool_size) {
  for (uint32_t i = 0; i < thread_pool_size; ++i) {
    // 3-3
    thread_pool_.push_back(base::make_unique<WorkerThread>(this));
  }
}

  用2個引數初始化了3個屬性,並且根據size往執行緒池中新增執行緒,thread_pool_這個屬性用vector在管理,push_back相當於JS的push,當成陣列來理解就行了。

  新增的WorkerThread類是在DefaultWorkerThreadsTaskRunner裡面的一個私有內部類,繼承於Thread,單純的用來管理執行緒。C++的this比較簡單,沒有JS那麼多概念,就是一個指向當前物件的指標,來看一下執行緒類的建構函式。

// 3-3
DefaultWorkerThreadsTaskRunner::WorkerThread::WorkerThread(DefaultWorkerThreadsTaskRunner* runner)
    // 這裡呼叫父類建構函式
    : Thread(Options("V8 DefaultWorkerThreadsTaskRunner WorkerThread")),
    // 這裡初始化當前類屬性
      runner_(runner) {
  // 3-4
  Start();
}

  這裡同時呼叫了父類建構函式並初始化本身的屬性,runner就是上面那個物件本身。這個建構函式長得比較奇怪,其中Options類是Thread的內部類,有一個接受一個型別為字串的建構函式,而Thread的建構函式只接受Options型別,所以會這樣,程式碼如下。

class Thread {
 public:
  // Opaque data type for thread-local storage keys.
  using LocalStorageKey = int32_t;

  class Options {
   public:
    Options() : name_("v8:<unknown>"), stack_size_(0) {}
    explicit Options(const char* name, int stack_size = 0)
        : name_(name), stack_size_(stack_size) {}
    // ...
  };

  // Create new thread.
  explicit Thread(const Options& options);
  // ...
}

  可以簡單理解這裡給執行緒取了一個名字,在給Options命名的同時,其實也給Thread命名了,如下。

Thread::Thread(const Options& options)
    : data_(new PlatformData),
      stack_size_(options.stack_size()),
      start_semaphore_(nullptr) {
  if (stack_size_ > 0 && static_cast<size_t>(stack_size_) < PTHREAD_STACK_MIN) {
    stack_size_ = PTHREAD_STACK_MIN;
  }
  set_name(options.name());
}

class Thread {
  // The thread name length is limited to 16 based on Linux's implementation of
  // prctl().
  static const int kMaxThreadNameLength = 16;
  char name_[kMaxThreadNameLength];
}

void Thread::set_name(const char* name) {
  // 這裡的長度被限制在16以內
  strncpy(name_, name, sizeof(name_));
  name_[sizeof(name_) - 1] = '\0';
}

  看註釋說,由於Linux的prctl方法限制了長度,所以這裡的name也最多隻能儲存16位,而且C++的字串的最後一位還要留給結束符,所以理論上傳入Options的超長字串

"V8 DefaultWorkerThreadsTaskRunner WorkerThread"只有前15位作為Thread的name儲存下來了,也就是"V8 Defaultworke",非常戲劇性的把r給砍掉了。。。

  初始化完成後,會呼叫Start方法啟動執行緒,這個方法並不需要子類實現,而是基類已經定義好了,保留關鍵程式碼如下。

// 3-4
void Thread::Start() {
  int result;
  // 執行緒物件
  pthread_attr_t attr;
  memset(&attr, 0, sizeof(attr));
  // 初始化執行緒物件
  result = pthread_attr_init(&attr);
  size_t stack_size = stack_size_;
  if (stack_size == 0) {
    stack_size = 1 * 1024 * 1024;
  }
  if (stack_size > 0) {
    // 設定執行緒物件屬性
    result = pthread_attr_setstacksize(&attr, stack_size);
  }
  {
    // 建立一個新執行緒
    // 3-5
    result = pthread_create(&data_->thread_, &attr, ThreadEntry, this);
  }
  // 摧毀執行緒物件
  result = pthread_attr_destroy(&attr);
}

  參照一下文章開始的demo,可以看出去掉了合法性檢測和巨集之後,在初始化和啟動執行緒基本上V8的形式是一樣的。

  簡單總結一下,V8初始化了一個DefaultPlatform類,計算了一下可用執行緒池大小,生成了幾條執行緒弄進執行緒池,而每條執行緒的任務就是那個ThreadEntry,這篇全部寫完算了。

 

  這個方法賊麻煩。

// 3-5
static void* ThreadEntry(void* arg) {
  Thread* thread = reinterpret_cast<Thread*>(arg);
  // We take the lock here to make sure that pthread_create finished first since
  // we don't know which thread will run first (the original thread or the new
  // one).
  { MutexGuard lock_guard(&thread->data()->thread_creation_mutex_); }
  // 3-6
  SetThreadName(thread->name());
  // 3-7
  thread->NotifyStartedAndRun();
  return nullptr;
}

  由於執行緒任務的引數定義與返回值都是void*,這裡直接做一個強轉。隨後會加一個執行緒鎖,因為這幾個執行緒在初始化的時候並不需要同時執行這個任務。執行的第一個方法雖然從名字來看只是簡單的給執行緒設定名字,但是內容卻不簡單。  

  傳入SetThreadName方法的引數是之前那個被截斷的字串,看一下這個方法。

// 3-6
static void SetThreadName(const char* name) {
  // pthread_setname_np is only available in 10.6 or later, so test
  // for it at runtime.
  int (*dynamic_pthread_setname_np)(const char*);
  // 讀取動態連結庫
  *reinterpret_cast<void**>(&dynamic_pthread_setname_np) =
    dlsym(RTLD_DEFAULT, "pthread_setname_np");
  if (dynamic_pthread_setname_np == nullptr) return;

  // Mac OS X does not expose the length limit of the name, so hardcode it.
  static const int kMaxNameLength = 63;
  // 從讀取到的方法處理name
  dynamic_pthread_setname_np(name);
}

  裡面用了一個很玄的api的叫dlsym,官方解釋如下。

The function dlsym() takes a "handle" of a dynamic library returned by dlopen() and the null-terminated symbol name, returning the address where that symbol is loaded into memory.

  大概就是根據控制程式碼讀取一個動態連結庫,名字就是那個字串,返回其在記憶體中的地址,所以這塊的除錯全是機器碼,根本看不懂,最後返回的一個函式。

  知道這是個函式就行了,至於怎麼設定執行緒名字我也不太想知道。

  第二步的方法名就是執行執行緒的任務,呼叫鏈比較長,會來回在幾個類之間穿梭,呼叫各自屬性的方法。

// 3-7
void NotifyStartedAndRun() {
  if (start_semaphore_) start_semaphore_->Signal();
  // 3-8
  Run();
}

// 3-8
void DefaultWorkerThreadsTaskRunner::WorkerThread::Run() {
  runner_->single_worker_thread_id_.store(base::OS::GetCurrentThreadId(), std::memory_order_relaxed);
  // 3-9
  while (std::unique_ptr<Task> task = runner_->GetNext()) {
    // 每一個task會實現自己的run函式
    task->Run();
  }
}

// 3-9
std::unique_ptr<Task> DefaultWorkerThreadsTaskRunner::GetNext() {
  // 3-10
  return queue_.GetNext();
}

  不理清楚,這個地方真的很麻煩,繞得很,可以看頂部的繼承圖。總之,最後呼叫的是DefaultWorkerThreadsTaskRunner類上一個型別為DelayedTaskQueue類的GetNext方法,返回型別是Task類,V8只是簡單定義了一個基類,實際執行時的所以task都需要繼承這個類並實現其Run方法以便執行緒執行。

  最後的最後,GetNext的邏輯其實可以參考libuv的邏輯,機制都大同小異,方法的原始碼如下。

// 3-10
std::unique_ptr<Task> DelayedTaskQueue::GetNext() {
  base::MutexGuard guard(&lock_);
  for (;;) {
    /**
     * 這一片內容完全可以參考libuv事件輪詢的前兩步
     * 1、從DelayQueue佇列中依次取出超過指定時間的task
     * 2、將所有超時的task放到task_queue_佇列中
     * 3、從task_queue_中將task依次取出並返回
     * 4、外部會呼叫task的Run方法並重復呼叫該函式
    */
    double now = MonotonicallyIncreasingTime();
    std::unique_ptr<Task> task = PopTaskFromDelayedQueue(now);
    while (task) {
      task_queue_.push(std::move(task));
      task = PopTaskFromDelayedQueue(now);
    }
    if (!task_queue_.empty()) {
      std::unique_ptr<Task> result = std::move(task_queue_.front());
      task_queue_.pop();
      return result;
    }

    if (terminated_) {
      queues_condition_var_.NotifyAll();
      return nullptr;
    }
    /**
     * 1、當task_queue_佇列沒有task需要處理 但是delay_task_queue_有待處理task
     * 這裡會計算當前佇列中延遲task中最近的觸發時間 等待對應的時間再次觸發
     * 2、當兩個佇列都沒有需要的事件
     * 執行緒會直接休眠等待喚醒
    */
    if (task_queue_.empty() && !delayed_task_queue_.empty()) {
      double wait_in_seconds = delayed_task_queue_.begin()->first - now;
      base::TimeDelta wait_delta = base::TimeDelta::FromMicroseconds(base::TimeConstants::kMicrosecondsPerSecond * wait_in_seconds);

      bool notified = queues_condition_var_.WaitFor(&lock_, wait_delta);
      USE(notified);
    } else {
      queues_condition_var_.Wait(&lock_);
    }
  }
}

  哎……V8引擎不過如此。

相關文章