淺析libuv源碼-node事件輪詢解析(3)

来源:https://www.cnblogs.com/QH-Jimmy/archive/2019/05/10/10846097.html
-Advertisement-
Play Games

好像博客有觀眾,那每一篇都畫個圖吧! 本節簡圖如下。 上一篇其實啥也沒講,不過node本身就是這麼複雜,走流程就要走全套。就像曾經看webpack源碼,讀了300行代碼最後就為了取package.json裡面的main屬性,導致我直接棄坑了,垃圾源碼看完對腦子沒一點好處。回頭看了我之前那篇博客,同步 ...


  好像博客有觀眾,那每一篇都畫個圖吧!

  本節簡圖如下。

 

  上一篇其實啥也沒講,不過node本身就是這麼複雜,走流程就要走全套。就像曾經看webpack源碼,讀了300行代碼最後就為了取package.json裡面的main屬性,導致我直接棄坑了,垃圾源碼看完對腦子沒一點好處。回頭看了我之前那篇博客,同步那塊講的還像回事,非同步就慘不忍睹了。不過講道理,非同步中涉及鎖、底層操作系統API(iocp)的部分我到現在也不太懂,畢竟沒有實際的多線程開發經驗,只是純粹的技術愛好者。

  這一篇再次進入libuv內部,從uv_fs_stat開始,操作系統以windows為準,方法源碼如下。

// 參數分別為事件輪詢對象loop、管理事件處理的對象req、路徑path、事件回調cb
int uv_fs_stat(uv_loop_t* loop, uv_fs_t* req, const char* path, uv_fs_cb cb) {
  int err;

  INIT(UV_FS_STAT);
  err = fs__capture_path(req, path, NULL, cb != NULL);
  if (err) {
    return uv_translate_sys_error(err);
  }

  POST;
}

  其實Unix版本的代碼更簡潔,直接就是

int uv_fs_stat(uv_loop_t* loop, uv_fs_t* req, const char* path, uv_fs_cb cb) {
  INIT(STAT);
  PATH;
  POST;
}

  問題不大,都是三步。

  前面兩步在那篇都有介紹,這裡就不重覆了。大概就是根據操作類型初始化req對象,然後處理一下路徑,分配合理的空間給path字元串這些。

  重點還是放在POST巨集。

#define POST                                                                  \
  do {                                                                        \
    if (cb != NULL) {                                                         \
      uv__req_register(loop, req);                                            \
      // word_req是一個類型為uv__work的結構體
      // UV__WORK_FAST_IO是I/O操作類型
      // uv__fs_work是一個函數
      // uv__fs_done也是一個函數
      uv__work_submit(loop,                                                   \
                      &req->work_req,                                         \
                      UV__WORK_FAST_IO,                                       \
                      uv__fs_work,                                            \
                      uv__fs_done);                                           \
      return 0;                                                               \
    } else {                                                                  \
      uv__fs_work(&req->work_req);                                            \
      return req->result;                                                     \
    }                                                                         \
  }                                                                           \
  while (0)

  由於只關註非同步操作,所以看if分支。參數已經在註釋中給出,還需要註意的一個點是方法名,register、submit,即註冊、提交。意思是,非同步操作中,在這裡也不是執行I/O的地點,實際上還有更深入的地方,繼續往後面看。

  uv__req_register這個就不看了,簡單講是把loop的active_handle++,每一輪輪詢結束後會檢測當前loop是否還有活躍的handle需要處理,有就會繼續跑,判斷標準就是active_handle數量是否大於0。

  直接看下一步uv__work_submit。

// uv__word結構體
struct uv__work {
  void (*work)(struct uv__work *w);
  void (*done)(struct uv__work *w, int status);
  struct uv_loop_s* loop;
  void* wq[2];
};

// 參數參考上面 init_once是一個方法
void uv__work_submit(uv_loop_t* loop,
                     struct uv__work* w,
                     enum uv__work_kind kind,
                     void (*work)(struct uv__work* w),
                     void (*done)(struct uv__work* w, int status)) {
  uv_once(&once, init_once);
  w->loop = loop;
  w->work = work;
  w->done = done;
  post(&w->wq, kind);
}

  又是兩部曲,第一個uv_once如其名,這個方法只會執行一次,然後將loop對象和兩個方法掛在前面req的uv__work結構體上,最後調用post。

  uv_once這個方法有點意思,本身跟stat操作本身毫無關係,只是對所有I/O操作做一個準備工作,所有的I/O操作都會預先調一下這個方法。windows、Unix系統的處理方式完全不同,這裡貼一貼代碼,Unix不想看也看不懂,搞搞windows系統的。

void uv_once(uv_once_t* guard, void (*callback)(void)) {
  // 調用過方法此處ran為1 直接返回
  if (guard->ran) {
    return;
  }

  uv__once_inner(guard, callback);
}

static void uv__once_inner(uv_once_t* guard, void (*callback)(void)) {
  DWORD result;
  HANDLE existing_event, created_event;

  // 創建或打開命名或未命名的事件對象
  created_event = CreateEvent(NULL, 1, 0, NULL);
  if (created_event == 0) {
    uv_fatal_error(GetLastError(), "CreateEvent");
  }
  // 對&guard->event與NULL做原子比較 如果相等則將created_event賦予&guard->event
  // 返回第一個參數的初始值
  existing_event = InterlockedCompareExchangePointer(&guard->event,
                                                     created_event,
                                                     NULL);

  // 如果第一個參數初始值為NULL 說明該線程搶到了方法第一次執行權利
  if (existing_event == NULL) {
    /* We won the race */
    callback();

    result = SetEvent(created_event);
    assert(result);
    guard->ran = 1;
  } else {
    // ...
  }
}

  分塊來解釋一下上面的函數吧。

  • libuv這裡直接跟操作系統通信,在windows上需要藉助其自身的event模塊來輔助非同步操作。
  • 提前劇透一下,所有的I/O操作是調用獨立線程進行處理,所以這個uv_once會被多次調用,而多線程搶調用的時候有兩種情況;第一種最簡單,第一名已經跑完所有流程,將ran設置為1,其餘線程直接被擋在了uv_once那裡直接返回了。第二種就較為複雜,兩個線程同時接到了這個任務,然後都跑進了uv_once_inner中去了,如何保證參數callback只會被調用一次?這裡用上了windows內置的原子指針比較方法InterlockedCompareExchangePointer。何謂原子比較?這是只有在多線程才會出現的概念,原子性保證了每次讀取變數的值都是根據最新信息計算出來的,避免了多線程經常出現的競態問題,詳細文獻可以參考wiki。
  • 只有第一個搶到了調用權利的線程才會進入if分支,之後調用callback方法,並設置event,那個SetEvent也是windowsAPI,有興趣自己研究去。

  最後,所有的代碼流向都為了執行callback,參數表明這是一個函數指針,無返回值無參數,叫init_once。

static void init_once(void) {
#ifndef _WIN32
  // 用32位系統的去買新電腦
  // 略...
#endif
  init_threads();
}

  有意思咯,線程來了。

  先表明,libuv中有一個非常關鍵的數據結構:隊列,在src/queue.h。很多地方(比如之前講輪詢的某一階段取對應的callback時)我雖然說的是鏈表,但實際上用的是這個,由於鏈表是隊列的超集,而且比較容易理解,總的來說也不算錯。說這麼多,其實是初始化線程池會用到很多queue的巨集,我不想講,後面會單獨開一篇說。

  下麵上代碼。

static void init_threads(void) {
  unsigned int i;
  const char* val;
  uv_sem_t sem;

  // 線程池預設大小為4
  nthreads = ARRAY_SIZE(default_threads);
  // 可以通過環境變數UV_THREADPOOL_SIZE來手動設置
  val = getenv("UV_THREADPOOL_SIZE");
  // 如果設成0會變成1 大於上限會變成128
  if (val != NULL)
    nthreads = atoi(val);
  if (nthreads == 0)
    nthreads = 1;
  if (nthreads > MAX_THREADPOOL_SIZE)
    nthreads = MAX_THREADPOOL_SIZE;

  threads = default_threads;
  // 分配空間 靜態變數threads負責管理線程
  if (nthreads > ARRAY_SIZE(default_threads)) {
    threads = uv__malloc(nthreads * sizeof(threads[0]));
    if (threads == NULL) {
      nthreads = ARRAY_SIZE(default_threads);
      threads = default_threads;
    }
  }

  // 這裡是鎖和QUEUE相關...

  // 這裡給線程設置任務 喚醒後直接執行worker方法
  for (i = 0; i < nthreads; i++)
    if (uv_thread_create(threads + i, worker, &sem))
      abort();

  // 無關代碼...
}

  除去一些不關心的代碼,剩下的就是判斷是否有手動設置線程池數量,然後初始化分配空間,最後迴圈給每一個線程分配任務。

  這個worker可以先簡單看一下,大部分內容都是QUEUE相關,詳細內容全部寫在註釋裡面。

static void worker(void* arg) {
  // ...

  // 這個是給代碼塊加鎖 很多地方都有
  uv_mutex_lock(&mutex);
  for (;;) {
    // ..。

    // 從隊列取出一個節點
    q = QUEUE_HEAD(&wq);
    // 表示沒有更多要處理的信息 直接退出絕不能繼續走下麵的
    // 退出前還會兩個操作 1.喚醒另一個線程再次處理這個方法(可能下一瞬間來活了) 2.去掉鎖
    if (q == &exit_message) {
      uv_cond_signal(&cond);
      uv_mutex_unlock(&mutex);
      break;
    }

    // 從隊列中移除這個節點
    QUEUE_REMOVE(q);
    QUEUE_INIT(q);

    is_slow_work = 0;
    // node過來的都是快速通道 不會走這裡
    if (q == &run_slow_work_message) {
      //...
    }

    // 由於已經從隊列中移除了對應節點 這裡可以把鎖去掉了
    uv_mutex_unlock(&mutex);

    // 從節點取出對應的任務 執行work也就是實際的I/O操作(比如fs.stat...) 參考上面的uv__work_submit方法
    w = QUEUE_DATA(q, struct uv__work, wq);
    w->work(w);

    // 這裡也需要加鎖 執行完節點任務後需要將結果添加到word_queue的隊列中
    uv_mutex_lock(&w->loop->wq_mutex);
    w->work = NULL;
    QUEUE_INSERT_TAIL(&w->loop->wq, &w->wq);
    uv_async_send(&w->loop->wq_async);
    uv_mutex_unlock(&w->loop->wq_mutex);

    // 由於是for(;;) 這裡加鎖純粹是為了下一次提前準備迴圈
    uv_mutex_lock(&mutex);
    if (is_slow_work) {
      /* `slow_io_work_running` is protected by `mutex`. */
      slow_io_work_running--;
    }
  }
}

  註意是靜態方法,所以也需要處理多線程問題。註釋我寫的非常詳細了,可以慢慢看,不懂C++也大概能明白流程。

  還以為這一篇能搞完,沒想到這個流程有點長,先這樣吧。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 給背景圖片設置一個預設背景顏色,有以下兩種方法: 轉載 http://www.itxst.com/detail/jnjvv2qu.html ...
  • 3D 在2d的基礎上添加 z 軸的變化 3D 位移:在2d的基礎上添加 translateZ(),或者使用translate3d() translateZ():以方框中心為原點,變大 3D 縮放:在2d的基礎上添加 scaleZ(),或者使用scale3d() scaleZ()和 scale3d() ...
  • 顏色和插值 電腦中的顏色,常用的標準有RGB和HSL。 RGB:色彩模式是通過對紅(Red)、綠(Green)、藍(Blue)三個顏色通道相互疊加來得到額各式各樣的顏色。三個通道的值得範圍都是0~255,因此總共能表示16777216(255*255*255)種,即一千六百多萬種顏色。幾乎包括了人 ...
  • 引言 如果說vue是前端工程化使用較多的骨架,那麼JavaScript就是我們的前端的細胞。MVVM模式讓我們體驗到前端開發的便攜,無需再過多的考慮DOM的操作。而vue的漸進式開發(逐步引用組件,按需引入),也讓許多新手前端開發人員逐步繞過對jQuery的學習。jQuery需要記憶的內容頗多,這也 ...
  • 目前信息化產業發展勢頭很好,互聯網就成為了很多普通人想要涉及的行業,因為相比於傳統行業,互聯網行業漲薪幅度大,機會也多,所以就會大批的人想要轉行來學習web前端開發。目前來講市場上需要的web前端人員非常多,而且按照現在的勢頭,以後會需要更多的web前端開發人員,理由是以後每個人公司都會有自己的網站 ...
  • 一、css語法: css由兩大部分組成:選擇符和聲明,聲明由屬性和屬性值兩部分組成; 選擇符{屬性:屬性值;屬性:屬性值;} 註: a) 屬性和屬性值之間用冒號連接; b)每條聲明結束要加分號; 二、css選擇器: 1.id選擇器: 語法: <標記 id="id名"></標記> #id名{屬性:屬性 ...
  • CSS即層疊樣式表,在創建時有以下幾種樣式: 1.內聯樣式(行內樣式、行間樣式): <標記 style=“屬性:屬性值;”></標記> 2.內部樣式(嵌入式樣式): <style type="text/css"> < 一般放置在head部分 選擇器{ 屬性:屬性值; 屬性:屬性值; } </styl ...
  • 引言:JavaScript最早被設計出來就是為了實現對註冊表單的驗證,本文簡單的介紹了JavaScript的表單操作 1.獲取表單對象的方法 1.通過表單ID獲取:document.getElementById("formId"); 2.通過表單的索引獲取:document.forms[index] ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...