c++併發編程實戰-第4章 併發操作的同步

来源:https://www.cnblogs.com/BroccoliFighter/archive/2023/09/21/17720580.html
-Advertisement-
Play Games

等待事件或等待其他條件 坐車案例 想象一種情況:假設晚上坐車外出,如何才能確保不坐過站又能使自己最輕鬆? 方法一:不睡覺,時刻關註自己的位置 1 #include <iostream> 2 #include <thread> 3 #include <mutex> 4 using namespace ...


等待事件或等待其他條件

坐車案例

想象一種情況:假設晚上坐車外出,如何才能確保不坐過站又能使自己最輕鬆?

方法一:不睡覺,時刻關註自己的位置

 1 #include <iostream>
 2 #include <thread>
 3 #include <mutex>
 4 using namespace std;
 5 
 6 mutex _mtx;
 7 bool bFlag = false;
 8 void wait_for_flag()
 9 {
10     auto startTime = chrono::steady_clock::now();
11     while (1)
12     {
13         unique_lock<mutex> lock(_mtx);
14         if (bFlag)
15         {
16             auto endTime = chrono::steady_clock::now();
17             double dCount = chrono::duration<double, std::milli>(endTime - startTime).count();
18             cout << "wait_for_flag consume : " << dCount << endl;
19             return;
20         }
21     }
22 }
23 
24 void set_flag()
25 {
26     auto startTime = chrono::steady_clock::now();
27     unique_lock<mutex> lock(_mtx);
28     for (int i = 0; i < 5; i++)
29     {
30         lock.unlock();
31         //do something comsume 1000ms
32         this_thread::sleep_for(chrono::milliseconds(1000));
33         lock.lock();
34     }
35 
36     bFlag = true;
37     auto endTime = chrono::steady_clock::now();
38     double dCount = chrono::duration<double, std::milli>(endTime - startTime).count();
39     cout << "set_flag consume : " << dCount << endl;
40 }
41 
42 int main()
43 {
44     thread th1(wait_for_flag);
45     thread th2(set_flag);
46     th1.join();
47     th2.join();
48     return 0;
49 }

這種方式存在雙重浪費:

  • 線程 th1(wait_for_flag)須不斷查驗標誌,浪費原本有用的處理時間,這部分計算資源原本可以留給其他線程使用。
  • 線程 th1(wait_for_flag)每次迴圈都需要給互斥上鎖,導致其他線程無法加鎖。如果 th2 此時完成操作,則需要等待 th1 釋放互斥才能操作。

程式輸出如下:

set_flag consume : 5045.39
wait_for_flag consume : 5045.97

兩個線程執行時間相近,但查看任務管理器,發現Debug程式CPU占用率始終保持10%。

方法二:通過設定多個鬧鐘,每隔一段時間叫醒自己

 1 void wait_for_flag()
 2 {
 3     auto startTime = chrono::steady_clock::now();
 4     unique_lock<mutex> lock(_mtx);
 5     while (!bFlag)
 6     {
 7         lock.unlock();
 8         //設置 500ms 的鬧鐘
 9         this_thread::sleep_for(chrono::milliseconds(500));    
10         lock.lock();
11     }
12 
13     auto endTime = chrono::steady_clock::now();
14     double dCount = chrono::duration<double, std::milli>(endTime - startTime).count();
15     cout << "wait_for_flag consume : " << dCount << endl;
16 }

上面代碼中引用了 this_thread::sleep_for()函數,如果暫時不滿足條件,就讓線程休眠。這確有改進,因為線程休眠,所以處理時間不再被浪費(不用熬夜)。但是,還是存在缺陷,休眠間隔時間難以確定。如果設置太短,會導致頻繁檢驗,如果設置太長,又可能導致過度休眠(到站還沒響)。如果線程 th2 完成了任務,線程 th1 卻沒有被及時喚醒,就會導致延遲。

上面的代碼將休眠時間設置為500ms,CPU占用率始終為0%,但兩個線程的運行時間相差過大。運行結果如下:

set_flag consume : 5061.66
wait_for_flag consume : 5570.77

方法三:讓列車員叫醒你(使用 c++提供的同步機制)

若數據存在先後處理關係,線程甲需要等待線程乙完成處理後才能開始操作,那麼線程甲則需等待線程乙完成並且觸發事件,其中最基本的方式是條件變數。

 1 mutex _mtx;
 2 bool bFlag = false;
 3 condition_variable _cond;    //條件變數
 4 void wait_for_flag()
 5 {
 6     auto startTime = chrono::steady_clock::now();
 7     unique_lock<mutex> lock(_mtx);
 8     _cond.wait(lock, []() {return bFlag; });    //等待
 9 
10     auto endTime = chrono::steady_clock::now();
11     double dCount = chrono::duration<double, std::milli>(endTime - startTime).count();
12     cout << "wait_for_flag consume : " << dCount << endl;
13 }
14 
15 void set_flag()
16 {
17     auto startTime = chrono::steady_clock::now();
18     unique_lock<mutex> lock(_mtx);
19     for (int i = 0; i < 5; i++)
20     {
21         lock.unlock();
22         //do something comsume 1000ms
23         this_thread::sleep_for(chrono::milliseconds(1000));
24         lock.lock();
25     }
26 
27     bFlag = true;
28     _cond.notify_one();    //通知
29     auto endTime = chrono::steady_clock::now();
30     double dCount = chrono::duration<double, std::milli>(endTime - startTime).count();
31     cout << "set_flag consume : " << dCount << endl;
32 }

引用條件變數後,兩線程執行時間相差不大,程式輸出如下:

set_flag consume : 5015.84
wait_for_flag consume : 5016.75

註:上述案例的測試結果可能不盡相同,理解意思即可。

條件變數

C++標準庫提供了條件變數的兩種實現:

  • std::condition_variable,只能和std::mutex一起使用。(推薦)
  • std::condition_variable_any,只要某一類型符合成為互斥的最低標準,就能與其一起使用。

二者都在標準庫的頭文件<condition_variable>內聲明。

std::condition_variable

構造函數

condition_variable();
~condition_variable();

condition_variable(const condition_variable&) = delete;
condition_variable& operator=(const condition_variable&) = delete;

不支持拷貝、也不支持移動

通知

void notify_one();      //喚醒一個等待者
void notify_all();      //喚醒所有等待者

wait()函數

void wait(unique_lock<mutex>& _Lck);
void wait(unique_lock<mutex>& _Lck, _Predicate _Pred);

參數:

  • _Lck:獨占鎖,需要多次調用加鎖、解鎖操作。
  • _Pred:一個返回bool類型的可調用對象,用於檢查條件是否成立。

含義:

使當前線程進入休眠狀態,等待其他線程調用notify_one()函數或notify_all()函數喚醒。

該函數執行過程如下:

  • 當程式流程執行到wait時,如果指定了_Pred參數,wait會先執行_Pred如果_Pred返回true,wait函數執行完畢,返回,執行後續代碼;如果_Pred返回false,先將_Lck解鎖並阻塞當前線程,等待其他線程喚醒。如果沒有指定_Pred參數,等價於返回false的情況。
  • 後續,其他線程調用notify_one()或notify_all()函數喚醒當前線程。當前線程被喚醒,先將_Lck上鎖,如果指定_Pred參數,則先進行檢查,根據返回值決定是否阻塞。如果沒有指定_Pred參數,wait函數執行完畢,返回,執行後續代碼

wait_for()函數

template <class _Rep, class _Period>
cv_status wait_for(
            unique_lock<mutex>& _Lck,
            const chrono::duration<_Rep, _Period>& _Rel_time);

template <class _Rep, class _Period, class _Predicate>
bool wait_for(
        unique_lock<mutex>& _Lck,
        const chrono::duration<_Rep, _Period>& _Rel_time,
        _Predicate _Pred);

參數:

  • _Lck:獨占鎖
  • _Rel_time:等待所消耗的最大時間
  • _Pred:一個返回bool類型的可調用對象,用於檢查條件是否成立。
返回值:
  • cv_status:如果在最大時間時間內被喚醒,則wait_for()函數返回cv_status::no_timeout,否則wait_for()函數返回cv_status::timeout。
  • bool:返回_Pred的返回值。
含義:

使當前線程進入休眠狀態,等待其他線程調用notify_one()函數或notify_all()函數喚醒。如果等待時常超過_Rel_time,wait函數將返回。

wait_until()函數

template <class _Clock, class _Duration>
cv_status wait_until(
            unique_lock<mutex>&_Lck,
            const chrono::time_point<_Clock, _Duration>& _Abs_time);

template <class _Clock, class _Duration, class _Predicate>
bool wait_until(
        unique_lock<mutex>&_Lck,
        const chrono::time_point<_Clock, _Duration>&_Abs_time,
        _Predicate _Pred);

參數:

  • _Lck:獨占鎖
  • _Abs_time:指定停止等待的時間點
  • _Pred:一個返回bool類型的可調用對象,用於檢查條件是否成立。

返回值:

  • cv_status:如果在指定的時間點之前被喚醒,則wait_until()函數返回cv_status::no_timeout,否則wait_until()函數返回cv_status::timeout。
  • bool:返回_Pred的返回值。

含義:

使當前線程進入休眠狀態,等待其他線程調用notify_one()函數或notify_all()函數喚醒。如果等待時常超過指定的_Abs_time時間點,wait函數將返回。

std::condition_variable_any

std::condition_variable_any與std::condition_variable類似,這裡只簡單列出成員函數,具體含義可以參考上面的std::condition_variable。

構造函數

condition_variable_any();
~condition_variable_any();

condition_variable_any(const condition_variable_any&) = delete;
condition_variable_any& operator=(const condition_variable_any&) = delete;

不支持拷貝、也不支持移動

通知

void notify_one();
void notify_all();

wait()函數

template <class _Lock>
void wait(_Lock& _Lck);

template <class _Lock, class _Predicate>
void wait(_Lock& _Lck, _Predicate _Pred);

wait_for()函數

template <class _Lock, class _Rep, class _Period>
cv_status wait_for(
            _Lock & _Lck,
            const chrono::duration<_Rep, _Period>& _Rel_time);

template <class _Lock, class _Rep, class _Period, class _Predicate>
bool wait_for(
        _Lock & _Lck,
        const chrono::duration<_Rep, _Period>&_Rel_time,
        _Predicate _Pred);

wait_until()函數

template <class _Lock, class _Clock, class _Duration>
cv_status wait_until(_Lock & _Lck,
            const chrono::time_point<_Clock, _Duration>&_Abs_time)

template <class _Lock, class _Clock, class _Duration, class _Predicate>
bool wait_until(_Lock & _Lck,
        const chrono::time_point<_Clock, _Duration>&_Abs_time,
        _Predicate _Pred)

虛假喚醒

當線程從休眠狀態中被喚醒,卻發現等待條件未滿足時,因而無事,這種情況被稱為虛假喚醒。發生虛假喚醒最常見的情況是,多個線程爭搶同一個條件,例如:

 1 mutex _mtx;
 2 condition_variable        _cond;
 3 queue<int> _dataQueue;
 4 
 5 void data_preparation_thread()
 6 {
 7     while (true)
 8     {
 9         int _data = rand();
10         {
11             std::lock_guard<mutex> lock(_mtx);
12             _dataQueue.push(_data);
13         }
14         _cond.notify_all();
15         this_thread::sleep_for(chrono::milliseconds(1000));
16     }
17 }
18 
19 void data_processing_thread()
20 {
21     while (true)
22     {
23         std::unique_lock<mutex> lock(_mtx);
24         _cond.wait(lock, []()
25             {
26                 bool bEmpty = _dataQueue.empty();
27                 if (bEmpty)
28                     cout << this_thread::get_id() << " be spurious waken up\n";
29 
30                 return !bEmpty;
31             });
32         int _data = _dataQueue.front();
33         _dataQueue.pop();
34         lock.unlock();
35 
36         cout << "threadID : " << this_thread::get_id() << " data = " << _data << endl;
37     }
38 }
39 
40 int main()
41 {
42     srand(time(NULL));
43 
44     thread th1(data_processing_thread);
45     thread th2(data_processing_thread);
46     thread th3(data_preparation_thread);
47     th1.join();
48     th2.join();
49     th3.join();
50     return 0;
51 }

兩個線程競爭隊列中的一條數據,總有一個是被虛假喚醒的。

喚醒丟失

 1 void wait_for_flag()
 2 {
 3     unique_lock<mutex> lock(_mtx);
 4     _cond.wait(lock);    //等待
 5 }
 6 
 7 void set_flag()
 8 {
 9     unique_lock<mutex> lock(_mtx);
10     bFlag = true;
11     _cond.notify_one();    //通知
12 }
13 
14 int main()
15 {
16     thread th1(set_flag);
17     thread th2(wait_for_flag);
18     th1.join();
19     th2.join();
20     return 0;
21 }

先執行set_flag()函數,設置數據,然後通知等待線程,此時wait_for_flag()函數還未進入等待狀態,導致通知信號丟失,後續又無新的通知信號,導致線程一直處於阻塞狀態。

使用 future 等待一次性事件發生

本節介紹std::future類,該類一般用於處理一次性事件,可以獲取該事件的返回值。

我們可以在某個線程啟動一個目標事件,該目標事件由新的線程去執行,並獲取一個std::future對象。之後,該線程執行自己餘下的任務,等到未來某一時刻,獲取目標事件中執行的結果。

C++標準程式庫有兩種future,分別由兩個類模板實現,其聲明都位於標準庫的頭文件<future>內:

  • std::future:獨占future。
  • std::shared_future:共用future。

它們的設計參照了std::unique_ptr和std::shared_ptr。同一目標事件僅僅允許關聯唯一一個std::future實例,但可以關聯多個std::shared_future實例。大致含義是:目標事件中的返回結果,如果想被其他多個線程訪問,則應該使用std::shared_future,否則使用std::future。

std::future

std::future類提供了訪問非同步操作執行結果的機制。通過std::async、std::packaged_task或std::promise創建的非同步操作,這些函數會返回一個std::future對象,該對象中保存了非同步操作的執行結果。但是,該類的結果並不是共用的,即,該結果只能訪問一次。

構造函數

future();
~future();
future(future&& _Other);
future& operator=(future&& _Right);

future(const future&) = delete;
future& operator=(const future&) = delete;

僅支持移動語義,不支持拷貝。

valid()函數

bool valid() const;

檢測當前結果是否就緒。

get()函數

template <class _Ty>
_Ty get();

阻塞,等待future擁有合法結果並返回該結果。結果返回後,釋放共用狀態,後續調用valid()函數將返回false。若調用該函數前valid()為false,則行為未定義。

wait()函數

void wait() const;

阻塞直至結果變得可用,該函數執行後,valid() == true。

wait_for()函數

template <class _Rep, class _Per>
future_status wait_for(const chrono::duration<_Rep, _Per>& _Rel_time);

等待結果,如果在指定的超時間隔後仍然無法得到結果,則返回。

future_status有如下取值:

  • future_status::ready:共用狀態就緒。
  • future_status::timeout:共用狀態在經過指定的等待時間內仍未就緒。
  • future_status::deferred:共用狀態持有的函數正在延遲運行,結果將在顯式請求時計算。

wait_until()函數

template <class _Clock, class _Dur>
future_status wait_until(const chrono::time_point<_Clock, _Dur>& _Abs_time);

等待結果,如果在已經到達指定的時間點時仍然無法得到結果,則返回。

share()函數

template <class _Ty>
shared_future<_Ty> share();

將本對象移動到std::shared_future對象。

std::shared_future

類似std::future類,同樣提供了訪問非同步操作執行結果的機制,與std::future類不同的是,該類的結果可以被訪問多次(可以連續多次調用get()函數)。

構造函數

shared_future();      //構造函數
~shared_future();     //析構函數

//支持拷貝
shared_future(const shared_future& _Other);
shared_future& operator=(const shared_future& _Right);

//支持移動
shared_future(future<_Ty>&& _Other);
shared_future& operator=(shared_future&& _Right);

支持拷貝,可以複製多個std::shared_future對象指向同一非同步結果。每個線程通過自身的std::shared_future對象副本訪問共用的非同步結果,這一操作是安全的。

valid()函數

bool valid() const;

檢測當前結果是否可用。

get()函數

template <class _Ty>
const _Ty& get() const;

阻塞,等待shared_future 擁有合法結果並獲取它。若調用此函數前valid()為false,則行為未定義。

等待

void wait() const;

template <class _Rep, class _Per>
future_status wait_for(
    const chrono::duration<_Rep, _Per>&_Rel_time);

template <class _Clock, class _Dur>
future_status wait_until(
    const chrono::time_point<_Clock, _Dur>& _Abs_time);

和std::future相似。

案例-構造std::shared_future對象

 1 std::promise<int> pro;
 2 std::future<int> _fu = pro.get_future();
 3 std::shared_future<int> _sfu = std::move(_fu);        //顯示
 4 
 5 std::promise<int> pro;
 6 std::shared_future<int> _sfu = pro.get_future();        //隱式
 7 
 8 std::promise<int> pro;
 9 std::future<int> _fu = pro.get_future();
10 std::shared_future<int> _sfu = _fu.share();        //share函數

std::async()函數-從後臺任務返回值

std::async()函數用於構建後臺任務,並允許調用者在未來某一時刻獲取該任務的返回值。

函數定義

template <class _Fty, class... _ArgTypes>
std::future<...> async(_Fty&& _Fnarg, _ArgTypes&&... _Args);

template <class _Fty, class... _ArgTypes>
std::future<...> async(launch _Policy, _Fty&& _Fnarg, _ArgTypes&&... _Args);
參數:
  • _Policy:該函數執行方式,有如下幾種取值:
    • std::launch::async:非同步執行。該函數執行後,會啟動新線程執行可調用對象。
    • std::launch::deferred:惰性執行。該函數執行後,並不會啟動線程,而是等到後續需要獲取結果時,由獲取值的線程直接執行可調用對象。如果沒有調用get()或者wait(),可調用對象不會執行。
    • std::launch::async | std::launch::deferred:由系統自行決定採用其中一個。
  • _Fnarg:可調用對象。
  • _Args:傳遞給可調用對象的參數包。

返回值:

std::future對象,通過該對象可用獲取後臺任務的返回值。

參數傳遞流程

//省略Res_Data類
int Entry(Res_Data data)
{
    cout << "-----------";
    return 5;
}

int main()
{
    Res_Data _data;
    auto _fu = std::async(Entry, _data);
    cout << _fu.get() << endl;
    return 0;
}

輸出如下:

008FF9E3  Constractor
008FF6E8  Copy Constractor
008FF33C  Move Constractor
00CE0854  Move Constractor
008FF33C  Destractor
008FF6E8  Destractor
009FDE74  Move Constractor
-----------
009FDE74  Destractor
00CE0854  Destractor
5
008FF9E3  Destractor

結論:一次拷貝,3次移動。

案例

int ThreadEntry()
{
    cout << "son threadId : " << std::this_thread::get_id() << " start to do something!" << endl;
    std::this_thread::sleep_for(std::chrono::milliseconds(5000));
    cout << "son threadId : " << std::this_thread::get_id() << " end doing something!" << endl;
    return 5;
}

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

-Advertisement-
Play Games
更多相關文章
  • 本文是由最近做的一個項目有感而發,因為之前做了一些技術棧的統一,為了用ant Design的pro-table,PC統一使用react,但是我們有一些老的項目是vue的,本次新頁面較多,老頁面的改動較少,除此之外老項目想換菜單,因此我們想藉助本次機會用react開發,經過了幾番思考,發現本次很適合用... ...
  • web前端JavaScript交互 點擊事件 意義: JavaScript中的點擊事件是指當用戶在頁面上點擊某個元素時觸發的事件。這個事件可以用於執行各種操作,如改變元素的樣式、修改頁面內容等。這是Web應用程式中最常用 的交互方式之一,允許用戶與網頁進行交互,提高用戶體驗。 案例: 隨機點名器 知 ...
  • import React, { useEffect, useState } from 'react'; hook 是react 16.8的新增特性 ,他可以讓你不在編寫class的情況下shiystate以及react的特性 Hooks的出現,首先解決了以下問題: 告別了令人疑惑的生命周期 告別類組 ...
  • 不同行業基本都會有自己獨特的業務,甚至同行的不同企業之間的業務邏輯也會相差千里,只有最大程度抽象出通用性、標準性和普適性的系統才能夠成為平臺系統,平臺系統開發的成本和難度可想而知。 個人深度參與或獨立設計開發過的公共服務型平臺系統,主要包括基礎數據平臺、支付平臺、財務平臺、結算平臺、配送平臺、CRM ...
  • records 使用原生sql,可以操作大多數的關係型資料庫 PART_1 - records引入的包(部分) 1. from sys import stdout 說明:標準輸出流 具體請參考:(https://pythonjishu.com/python-sys-stdout/)[https:// ...
  • Java基礎語法 JAVA--黑馬程式員 筆記 一、Java入門 1、JAVA 是一種很火的電腦語言。 2、JAVA 代碼編寫分三步: 編寫程式源碼,即編寫一個java文件 編譯:翻譯文件, javac是JDK提供的編譯工具,對java文件編譯後會產生一個class文件,class文件即交給電腦 ...
  • 內置數據類型 在編程中,數據類型是一個重要的概念。 變數可以存儲不同類型的數據,不同類型可以執行不同的操作。 Python預設內置了以下這些數據類型,分為以下幾類: 文本類型:str 數值類型:int、float、complex 序列類型:list、tuple、range 映射類型:dict 集合類 ...
  • 目錄前言介紹照片:後續: 前言 V~~~V。 介紹 進程間通訊(Inter-Process Communication,IPC)是操作系統中的一個重要概念,用於不同進程之間的數據傳輸和交互。有多種方式可以實現進程間通訊,以下是其中一些常見的方式: 管道(Pipe):管道是一種單向通信方式,通常用於具 ...
一周排行
    -Advertisement-
    Play Games
  • 示例項目結構 在 Visual Studio 中創建一個 WinForms 應用程式後,項目結構如下所示: MyWinFormsApp/ │ ├───Properties/ │ └───Settings.settings │ ├───bin/ │ ├───Debug/ │ └───Release/ ...
  • [STAThread] 特性用於需要與 COM 組件交互的應用程式,尤其是依賴單線程模型(如 Windows Forms 應用程式)的組件。在 STA 模式下,線程擁有自己的消息迴圈,這對於處理用戶界面和某些 COM 組件是必要的。 [STAThread] static void Main(stri ...
  • 在WinForm中使用全局異常捕獲處理 在WinForm應用程式中,全局異常捕獲是確保程式穩定性的關鍵。通過在Program類的Main方法中設置全局異常處理,可以有效地捕獲並處理未預見的異常,從而避免程式崩潰。 註冊全局異常事件 [STAThread] static void Main() { / ...
  • 前言 給大家推薦一款開源的 Winform 控制項庫,可以幫助我們開發更加美觀、漂亮的 WinForm 界面。 項目介紹 SunnyUI.NET 是一個基於 .NET Framework 4.0+、.NET 6、.NET 7 和 .NET 8 的 WinForm 開源控制項庫,同時也提供了工具類庫、擴展 ...
  • 說明 該文章是屬於OverallAuth2.0系列文章,每周更新一篇該系列文章(從0到1完成系統開發)。 該系統文章,我會儘量說的非常詳細,做到不管新手、老手都能看懂。 說明:OverallAuth2.0 是一個簡單、易懂、功能強大的許可權+可視化流程管理系統。 有興趣的朋友,請關註我吧(*^▽^*) ...
  • 一、下載安裝 1.下載git 必須先下載並安裝git,再TortoiseGit下載安裝 git安裝參考教程:https://blog.csdn.net/mukes/article/details/115693833 2.TortoiseGit下載與安裝 TortoiseGit,Git客戶端,32/6 ...
  • 前言 在項目開發過程中,理解數據結構和演算法如同掌握蓋房子的秘訣。演算法不僅能幫助我們編寫高效、優質的代碼,還能解決項目中遇到的各種難題。 給大家推薦一個支持C#的開源免費、新手友好的數據結構與演算法入門教程:Hello演算法。 項目介紹 《Hello Algo》是一本開源免費、新手友好的數據結構與演算法入門 ...
  • 1.生成單個Proto.bat內容 @rem Copyright 2016, Google Inc. @rem All rights reserved. @rem @rem Redistribution and use in source and binary forms, with or with ...
  • 一:背景 1. 講故事 前段時間有位朋友找到我,說他的窗體程式在客戶這邊出現了卡死,讓我幫忙看下怎麼回事?dump也生成了,既然有dump了那就上 windbg 分析吧。 二:WinDbg 分析 1. 為什麼會卡死 窗體程式的卡死,入口門檻很低,後續往下分析就不一定了,不管怎麼說先用 !clrsta ...
  • 前言 人工智慧時代,人臉識別技術已成為安全驗證、身份識別和用戶交互的關鍵工具。 給大家推薦一款.NET 開源提供了強大的人臉識別 API,工具不僅易於集成,還具備高效處理能力。 本文將介紹一款如何利用這些API,為我們的項目添加智能識別的亮點。 項目介紹 GitHub 上擁有 1.2k 星標的 C# ...