To conclude, the V8 engine initializes a default Platform that creates a pool of threads, one of which is used to handle delayed tasks like setTimeout
In addition, there are some diagrams, including inheritance tree, key attribute attribution, pure logical workflow, the code of wood interested in the graph can be X off.

When V8 initializes the DefaultPlatform object, it does three things. It generates the blank DefaultPlatform, gets the thread pool size, and then starts the thread.

Before writing, I spent 10 minutes to learn the thread of C++ under MAC, and had a preliminary understanding of API. To give a simple example, the general process is as follows.

// Stack_size set in V8 source code does not work in test demo
const int stack_size = 1 * 1024 * 512;
int tmp = 0;

// The task parameter of the thread is derived from the fourth parameter at creation time
void* add(void* number){
  tmp = tmp + *(int*)number;
  printf("tmp: %i\n", tmp);
  return nullptr;
};

int main(int argc, const char * argv[]) {
  // Create a thread object
  pthread_t pt;
  // Create thread properties
  pthread_attr_t attr;
  memset(&attr, 0.sizeof(attr));
  pthread_attr_init(&attr);
  // Set the size of the property
  pthread_attr_setstacksize(&attr, stack_size);
  // Function arguments
  int num = 5;
  int* ptr = #
  // Generate a thread
  // The argument list references each variable
  int ret = pthread_create(&pt, &attr, add, ptr);
  if(ret ! =0) printf("cannot create thread");
  return 0;
}Copy the code
In a few steps, a thread can be created to process the task, and the output after startup will not bother to take a screenshot, but simply print a 5.

With the above example, you can take a slow look at the multi-threading startup process during V8 initialization, starting with the getting started method.

/ / 3
void DefaultPlatform::EnsureBackgroundTaskRunnerInitialized() {
  // Initialize the DefaultPlatform property with a lock
  base::MutexGuard guard(&lock_);
  if(! worker_threads_task_runner_) { worker_threads_task_runner_ =/ / 3 to 2
        std::make_shared<DefaultWorkerThreadsTaskRunner>(
            thread_pool_size_, time_function_for_testing_
                                   ? time_function_for_testing_
                                  / / 3 to 1: DefaultTimeFunction); }}/ / 3 to 1
double DefaultTimeFunction(a) {
  return base::TimeTicks::HighResolutionNow().ToInternalValue() /
         static_cast<double>(base::Time::kMicrosecondsPerSecond);
}Copy the code
Worker_threads_task_runner in if is a private property of DefaultPlatform, and since the default value is NULL when initialized, a definition is assigned. The first argument is the thread pool size obtained in step 2, and the second argument is a counting method, which by default references the previous Time module and returns a hardware timestamp, as I wrote earlier.

See next DefaultWorkerThreadsTaskRunner class constructors, accept two parameters.

/ / 3 to 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 to 3
    thread_pool_.push_back(base::make_unique<WorkerThread>(this)); }}Copy the code
Initialize three attributes with two arguments and add threads to the pool according to size. The thread_pool_ attribute is managed by a vector. Push_back is a push_back array.

Add WorkerThread class is a private inner class in DefaultWorkerThreadsTaskRunner, inheritance in Thread, simple to manage threads. This is a pointer to the current object. Let’s take a look at the constructor of the thread class.

/ / 3 to 3
DefaultWorkerThreadsTaskRunner::WorkerThread::WorkerThread(DefaultWorkerThreadsTaskRunner* runner)
    // Call the superclass constructor here
    : Thread(Options("V8 DefaultWorkerThreadsTaskRunner WorkerThread")),
    // This initializes the current class attribute
      runner_(runner) {
  / / 3-4
  Start();
}Copy the code
The superclass constructor is called and the property is initialized. Runner is the object itself. The Options class is an inner class of Thread. There is a constructor that accepts a string. The Thread constructor only accepts Options, so the code looks like this.

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);
  // ...
}Copy the code
The Thread is given a name, and the Options name is also given to the 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) {
  // The length here is limited to 16
  strncpy(name_, name, sizeof(name_));
  name_[sizeof(name_) - 1] = '\ 0';
}Copy the code
The PRCTL method in Linux limits the length of the string, so the name can only hold 16 bits at most. The last bit of the string must be reserved for the terminator
Only the first 15 “V8 DefaultWorkerThreadsTaskRunner WorkerThread” as the name of the Thread preserved, namely “V8 Defaultworke”, very dramatic to cut off the r…

After initialization, the thread is started by calling the Start method, which does not require subclass implementation, but the base class is already defined, leaving the key code as follows.

/ / 3-4
void Thread::Start() {
  int result;
  // Thread object
  pthread_attr_t attr;
  memset(&attr, 0.sizeof(attr));
  // Initializes the thread object
  result = pthread_attr_init(&attr);
  size_t stack_size = stack_size_;
  if (stack_size == 0) {
    stack_size = 1 * 1024 * 1024;
  }
  if (stack_size > 0) {
    // Set thread object properties
    result = pthread_attr_setstacksize(&attr, stack_size);
  }
  {
    // Create a new thread
    / / 3-5
    result = pthread_create(&data_->thread_, &attr, ThreadEntry, this);
  }
  // Destroy the thread object
  result = pthread_attr_destroy(&attr);
}Copy the code
Taking a look at the demo at the beginning of this article, you can see that after removing validity checks and macros, the initialization and startup threads are essentially the same in V8.

To summarize, V8 initializes a DefaultPlatform class, calculates the available thread pool size, and generates several threads to get into the thread pool. Each thread’s task is that ThreadEntry.



This method is troublesome.

// 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;
}Copy the code
Since the thread task’s parameter definition and return value are void*, we do a strong cast directly. A thread lock is then added because these threads do not need to perform this task at the same time when they are initialized. The first method executed is simply a name for the thread, but the content is not.

The argument passed to the SetThreadName method is the truncated string. Look at this method.

/ / 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*);
  // Read the dynamic link library
  *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;
  // Process name from the method read
  dynamic_pthread_setname_np(name);
}Copy the code
It uses a very mysterious API called 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.
It basically reads a dynamically linked library based on the handle, the name is that string, and returns its address in memory, so this debugging is all machine code, you can’t read it, and finally returns a function.

Just know that this is a function, and I don’t really want to know how to set the thread name.

The method name of the second step is the task of the running thread, and the call chain is long, shuttling back and forth between several classes, calling the methods of their respective properties.

/ / 3 to 7
void NotifyStartedAndRun(a) {
  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()) {
    // Each task implements its own run functiontask->Run(); }}/ / 3-9
std: :unique_ptr<Task> DefaultWorkerThreadsTaskRunner::GetNext() {
  / / 3-10
  return queue_.GetNext();
}Copy the code
I don’t know, this place is really troublesome, very round, you can see the inheritance diagram at the top. In short, is the last call DefaultWorkerThreadsTaskRunner class a type for DelayedTaskQueue class GetNext method, the return type is a Task class, V8 simply defines a base class, All tasks at real time need to inherit this class and implement its Run method for the thread to execute.

Finally, GetNext’s logic can actually refer to libuv’s logic, the mechanism is much the same, the source code of the method is as follows.

/ / 3-10
std: :unique_ptr<Task> DelayedTaskQueue::GetNext() {
  base::MutexGuard guard(&lock_);
  for (;;) {
    /** * You can refer to the first two steps of libuv event polling * 1, remove tasks that have expired from the DelayQueue * 2, place all tasks that have expired in the task_queue_ queue * * * * * * * * * * * * * * * * * * * * * * * * * * *
    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;
    }
    Delay_task_queue_queue_pending task * This will calculate the last trigger time of the delayed task in the current queue and wait for the corresponding time to trigger again * 2 The thread will sleep and wait to wake up
    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_); }}}Copy the code
Ah… So much for the V8.