在线观看www成人影院-在线观看www日本免费网站-在线观看www视频-在线观看操-欧美18在线-欧美1级

0
  • 聊天消息
  • 系統(tǒng)消息
  • 評論與回復(fù)
登錄后你可以
  • 下載海量資料
  • 學(xué)習(xí)在線課程
  • 觀看技術(shù)視頻
  • 寫文章/發(fā)帖/加入社區(qū)
會(huì)員中心
創(chuàng)作中心

完善資料讓更多小伙伴認(rèn)識你,還能領(lǐng)取20積分哦,立即完善>

3天內(nèi)不再提示

使用C++11新特性實(shí)現(xiàn)一個(gè)通用的線程池設(shè)計(jì)

CPP開發(fā)者 ? 來源:程序員班吉 ? 2023-12-22 13:58 ? 次閱讀

C++11標(biāo)準(zhǔn)之前,多線程編程只能使用pthread_xxx開頭的一組POSIX標(biāo)準(zhǔn)的接口。從C++11標(biāo)準(zhǔn)開始,多線程相關(guān)接口封裝在了C++的std命名空間里。

Linux中,即便我們用std命名空間中的接口,也需要使用-lpthread鏈接pthread庫,不難猜出,C++多線程的底層依然還是POSIX標(biāo)準(zhǔn)的接口。你可能會(huì)有疑問,既然底層還是使用pthread庫,為什么不直接用pthread,而要繞一大圈再封裝一層呢?

在我看來,除了統(tǒng)一C++編程接口之外,C++11標(biāo)準(zhǔn)更多的是在語言層面做了很多優(yōu)化,規(guī)避了原來C語言中的很多陷阱,比如C++11中的lock_guard、future、promise等技術(shù),將原來C語言中語法上容易犯錯(cuò)的地方進(jìn)行了簡化,簡單來說就是將原來依賴人的地方交給了編譯器(很多時(shí)候機(jī)器比人更可靠)。比如在C++11標(biāo)準(zhǔn)之前,我們使用mutex是像下面這樣的:

pthread_mutex_lock(mutex)
....
if (condition) {
  ...
} elser if {
  ...
} else {
  ...
}
...
pthread_mutex_unlock(mutex)
相信有mutex使用經(jīng)驗(yàn)的人都或多或少都在這上面踩過坑,比如少寫了一個(gè)unlock,中間異常退出沒有執(zhí)行到unlock等等各種各樣的情況導(dǎo)致的鎖沒有被正確釋放。而在C++11標(biāo)準(zhǔn)中,我們只需要使用lock_guard就可以了,比如:
lock_gruard locker(mutex)
...
if (condition) {
  ...
} elser if {
  ...
} else {
  ...
}
...

C++編譯器會(huì)自動(dòng)為我們插入釋放鎖的代碼,這樣我們就不用時(shí)刻擔(dān)心鎖有沒有正確釋放了。我個(gè)人的感覺是,從C++11標(biāo)準(zhǔn)開始,C++變得不那么可怕了,甚至很多時(shí)候覺得C++變得好用了。

這篇文章我們試著使用C++11標(biāo)準(zhǔn)的新特性來實(shí)現(xiàn)一個(gè)通用的線程池。首先,我們來看一下C++11標(biāo)準(zhǔn)中線程是如何創(chuàng)建的,先看代碼:

#include 
#include 


using namespace std;
void threadFunc(int &a) {
    a += 10;
    std::cout << "this is thread fun!" << std::endl;
}


int main() {
    int x = 10;
    std::thread t1(threadFunc, std::ref(x));
    t1.join();


    std::cout << "x=" << x << std::endl;
}

使用std::thread創(chuàng)建一個(gè)t1線程對象,傳入線程主函數(shù),將x以引用的方式傳入線程主函數(shù),接著調(diào)用join方法等待主函數(shù)threadFunc執(zhí)行完成。上面x最終的結(jié)果將是20。

整個(gè)流程和使用pthread_create其實(shí)區(qū)別不大,只是C++11將線程的創(chuàng)建封裝成了一個(gè)類,使得我們可以用面向?qū)ο缶幊痰姆绞絹韯?chuàng)建多線程。

我們可以思考一下,要實(shí)現(xiàn)一個(gè)線程池,應(yīng)該怎么做?這個(gè)問題我們可以倒過來想一下,線程池應(yīng)該怎么使用。比如,對于一個(gè)web服務(wù)器來講,為了能利用多核的優(yōu)勢,我們可以在主線程里接收到請求之后,將對這個(gè)請求的具體操作交給worker線程,比如像下面這樣:

04c8b604-a07e-11ee-8b88-92fbcf53809c.png

這個(gè)流程我們在C語言網(wǎng)絡(luò)編程那個(gè)系列文章中有非常詳細(xì)的說明,如果你get不到這個(gè)例子是什么意思,建議你去看一下那個(gè)系列的文章(C語言網(wǎng)絡(luò)編程系列合集)

主線程將請求交給Worker線程這個(gè)好理解,但是你有沒有想過,它怎么就能交給Worker線程呢?線程創(chuàng)建完之后,對應(yīng)的主函數(shù)就已經(jīng)運(yùn)行起來了,對應(yīng)accept出來的套接字要怎么傳遞給一個(gè)正在運(yùn)行的函數(shù)呢?

為了說明這個(gè)問題,我們先來看一段Golang的代碼,暫時(shí)看不懂沒關(guān)系,后面我會(huì)解釋

func TestWorker(t *testing.T) {
  msg := make(chan int, 1)
  notify := make(chan struct{}, 0)
  poolSize := 10


  buildWorkers(poolSize, msg, notify)


  // 模擬任務(wù)
  for i := 0; i < poolSize; i++ {
    msg <- i
  }


  for i := 0; i < poolSize; i++ {
    <-notify
  }
}


func buildWorkers(poolSize int, msg <-chan int, notify chan<- struct{}) {
  for i := 0; i < poolSize; i++ {
    go func(i int) {
      if ret := recover(); ret != nil {
        log.Printf("panic: %v", ret)
      }


      log.Println("worker-id:", i)


      for {
        select {
        case m := <-msg:
          fmt.Println("m:", m)
          notify <- struct{}{}
        }
      }
    }(i)
  }
}

buildWorkers方法創(chuàng)建了10個(gè)協(xié)程,每個(gè)協(xié)程里面都有一個(gè)for循環(huán),一開始每個(gè)for循環(huán)都阻塞在select語句中的case m := ←msg,如果之前沒有接觸過Go語言(這里的select你可以簡單和Linux中的select技術(shù)類比)。另外還有兩個(gè)通道,一個(gè)是msg和notify,msg用來傳遞參數(shù),notify用來通知外面這個(gè)協(xié)程任務(wù)已經(jīng)執(zhí)行完了。

在TestWorker方法中,我們模擬了10個(gè)任務(wù),這10個(gè)任務(wù)不斷的往msg通道中發(fā)送數(shù)據(jù),當(dāng)msg有數(shù)據(jù)之后,我們創(chuàng)建的那10個(gè)協(xié)程就會(huì)爭取從msg通道接收消息,只要接收到消息就說明這是執(zhí)行任務(wù)所必需的參數(shù)。執(zhí)行完成之后向notify發(fā)送消息。在TestWorker中我們同樣接收了10次,在沒有消息的時(shí)候就會(huì)阻塞在<-notify這一行,直到有協(xié)程執(zhí)行完成向notify通道發(fā)送消息,這里就從<-notify返回,進(jìn)入下一次循環(huán)。上面其實(shí)就是一個(gè)非常簡單的協(xié)程池了,當(dāng)然為了演示,代碼并不是很完整。

運(yùn)行上面的代碼,得到的結(jié)果大概像下面這樣

2023/10/07 21:37:44 worker-id: 9
2023/10/07 21:37:44 worker-id: 4
2023/10/07 21:37:44 worker-id: 8
2023/10/07 21:37:44 m: 2
2023/10/07 21:37:44 m: 0
2023/10/07 21:37:44 worker-id: 5
2023/10/07 21:37:44 m: 3
2023/10/07 21:37:44 worker-id: 2
2023/10/07 21:37:44 worker-id: 1
2023/10/07 21:37:44 m: 5
2023/10/07 21:37:44 m: 4
2023/10/07 21:37:44 worker-id: 7
2023/10/07 21:37:44 m: 6
2023/10/07 21:37:44 worker-id: 0
2023/10/07 21:37:44 m: 7
2023/10/07 21:37:44 m: 1
2023/10/07 21:37:44 worker-id: 6
2023/10/07 21:37:44 m: 8
2023/10/07 21:37:44 m: 9
2023/10/07 21:37:44 worker-id: 3

從上面的結(jié)果來看,協(xié)程運(yùn)行并不是順序執(zhí)行的,這和多線程是一樣的道理。上面Golang的代碼執(zhí)行的流程我畫了一張圖,如下:

04db9b2a-a07e-11ee-8b88-92fbcf53809c.png

注意箭頭的方向,所有協(xié)程都不斷的嘗試從channel中接收消息,拿到程序運(yùn)行必要的參數(shù),當(dāng)msg中有數(shù)據(jù)時(shí)從case m := <-msg中蘇醒并執(zhí)行具體的業(yè)務(wù)邏輯,我們知道,在Golang中channel是線程安全的,其內(nèi)部有一把鎖,這把鎖就是mutex,下面是channel底層結(jié)構(gòu)體

// src/runtime/chan.go:33
type hchan struct {
  ...
  lock mutex
}

channel除了能保證線程安全,還能保證順序性,也就是發(fā)送方最先發(fā)送的,在接收方一定也是最先收到的。這不就是一個(gè)加了鎖的隊(duì)列嗎?我們可以試著想一下在C++中是不是也可以實(shí)現(xiàn)類似的效果呢?不難想到,我們可以使用一個(gè)隊(duì)列在各個(gè)線程之間傳遞數(shù)據(jù),像下面這樣:

04e27f1c-a07e-11ee-8b88-92fbcf53809c.png

主線程accept出來的套接字,只管往隊(duì)列里面丟就可以了,我們創(chuàng)建的一堆worker線程,不斷的嘗試從隊(duì)列里面pop數(shù)據(jù)。這樣,我們就解決了線程之間的交互問題。

下面,我們就參照上面Golang的代碼,先把這個(gè)框架給搭出來,然后再在這個(gè)基礎(chǔ)之上去完善代碼,最后實(shí)現(xiàn)一個(gè)準(zhǔn)生產(chǎn)的線程池。

我們先參照上面Golang的代碼,實(shí)現(xiàn)相似邏輯,代碼如下:

#include 
#include 
#include 


void threadFunc(std::queue& q) {
    while (1) {
        if (!q.empty()) {
            int param = q.front();
            q.pop();
            std::cout << "param:" << param << std::endl;
        }
    }
}


void jobDispatch(std::queue& q) {
    for (int i = 0; i < 1000; i++) {
        q.push(i);
    }
}


int main() {
   std::queue q1;


   std::vector ths;
   for (int i = 0; i < 10; i++) {
       ths.emplace_back(threadFunc, std::ref(q1));
   }


   jobDispatch(q1);


   for (auto& th: ths) {
       th.join();
   }


   return 0;
}

上面的代碼盡可能的還原了Golang的邏輯,我們來分析一下這段代碼。在main函數(shù)中,創(chuàng)建了一個(gè)隊(duì)列q1,這個(gè)隊(duì)列用來向線程池傳遞參數(shù),接著創(chuàng)建了10個(gè)線程保存在了vector中,將q1以引用的形式傳入線程池主函數(shù)(注意:這里傳引用必須使用std::ref包裝一下),再接著調(diào)用jobDispatch模擬任務(wù)分配然后每個(gè)線程調(diào)用join等待結(jié)束。

接著我們來看線程池主函數(shù)threadFunc,這個(gè)函數(shù)接收一個(gè)隊(duì)列q作為參數(shù),這里的q就是我們在創(chuàng)建線程池的時(shí)候傳進(jìn)來的q1,然后是一個(gè)死循環(huán),在這個(gè)循環(huán)里面我們不斷的判斷隊(duì)列是否為空,如果不為空就從隊(duì)列取出一個(gè)元素出來。最后,分配任務(wù)的函數(shù)jobDispatch向隊(duì)列q1里面push了1000個(gè)元素,來模擬1000個(gè)任務(wù)。

上面的代碼當(dāng)然是有問題的,有興趣的可以把這段代碼拷貝下來把自己跑一下,你會(huì)發(fā)現(xiàn)雖然代碼能跑,但是結(jié)果完全看不懂。

首先,第一個(gè)問題就是queue不是線程安全的。所以,這個(gè)隊(duì)列得有一把鎖,比如:

std::mutex mtx;


void threadFunc(std::queue& q) {
    while (true) {
        if (!q.empty()) {
            std::lock_guard ltx(mtx);
            int param = q.front();
            q.pop();
            std::cout << "param:" << param << std::endl;
        }
    }
}

我們在threadFund函數(shù)中的出隊(duì)列之前加了一把鎖。這把鎖是全局的,每個(gè)線程都要先拿到這把鎖之后才能從隊(duì)列里拿到數(shù)據(jù),你可以把這段代碼替換之后再運(yùn)行一下,這次的結(jié)果應(yīng)該是正確的了。

可能你覺得奇怪,我們使用lock_guard創(chuàng)建了一個(gè)ltx對象,但是并沒有地方去釋放這把鎖,如果你有這樣的疑問應(yīng)該是對C++11還不是很熟悉,在C++11標(biāo)準(zhǔn)中,因?yàn)橛辛薘AII的緣故,一個(gè)對象被銷毀時(shí)一定會(huì)執(zhí)行析構(gòu)函數(shù),就算是運(yùn)行過程中對象產(chǎn)生異常析構(gòu)函數(shù)也會(huì)執(zhí)行,在C++中這叫棧展開。有了這個(gè)特性之后,lock_guard就不難理解了,其構(gòu)造函數(shù)其實(shí)就是調(diào)用了mutex的lock方法,然后把這個(gè)mutex保存在成員變量中,當(dāng)對象銷毀時(shí)析構(gòu)函數(shù)中調(diào)用unlock。所以,有了這個(gè)機(jī)制之后,我們就不用到處寫unlock了,這也是我覺得C++更好用了的原因之一。

在C++中同樣遵循大括號作用域,在上面的代碼中,lock_guard是在if語句中的,當(dāng)if語句執(zhí)行完之后,ltx就被銷毀了,所以當(dāng)循環(huán)進(jìn)入到下一次的時(shí)候?qū)嶋H上鎖已經(jīng)被釋放了。

這樣我們就解決了隊(duì)列的線程安全問題,但眼尖的你一定看出來其實(shí)還有一個(gè)問題,threadFunc函數(shù)中的死循環(huán)一直在空轉(zhuǎn),這顯然是有問題的。解決這個(gè)問題最容易想到的就是每次循環(huán)都sleep一下,但這顯然也是有問題的,對于一個(gè)有實(shí)時(shí)要求的系統(tǒng)是不能接受的。

所以,我們迫切需要一種機(jī)制,讓threadFunc沒事干的時(shí)候就停在那等通知,想想看什么技術(shù)可以實(shí)現(xiàn)?對,就是cond,在C++11中條件變量也被封裝在了std::命名空間中。下面我們就使用cond來改造一下,相關(guān)代碼如下:

std::mutex mtx;
std::condition_variable cond;   // v2


void threadFunc(std::queue& q) {
    while (true) {
        std::unique_lock ltx(mtx);       // v2
        cond.wait(ltx, [q]() { return !q.empty();}); // v2


        int param = q.front();
        q.pop();
        std::cout << "param:" << param << std::endl;
    }
}


void jobDispatch(std::queue& q) {
    for (int i = 0; i < 1000; i++) {
        q.push(i);
    }
    cond.notify_all();  // v2
}

修改后的代碼我在后面都加了注釋(v2), 首先我們定義了一個(gè)全局的條件變量cond,然后在threadFunc中調(diào)用cond的wait方法等待通知。然后在jobDispatch中往隊(duì)列里面寫完數(shù)據(jù)之后調(diào)用notify_all方法通知所有等待的線程。這樣,當(dāng)隊(duì)列中沒有數(shù)據(jù)的時(shí)候線程池中的線程就會(huì)進(jìn)入睡眠,直到notify_all被調(diào)用。這里你可以想一下,上面notify_all還可以進(jìn)一步優(yōu)化嗎?

當(dāng)然,上面還作了一個(gè)調(diào)整,就是將原來的lock_guard換了unique_lock,這個(gè)改動(dòng)是必須的,我們知道cond調(diào)用wait的時(shí)候會(huì)嘗試釋放鎖,可lock_guard里面沒有釋放鎖的方法,而unique_lock是有unlock方法的。也就是說,unique_lock創(chuàng)建的ltx對象可以手動(dòng)調(diào)用unlock方法釋放鎖。

好了,到這里其實(shí)我們已經(jīng)寫出一個(gè)簡單的線程池了,這個(gè)線程池通過一個(gè)隊(duì)列傳遞參數(shù),使用mutex解決線程安全問題,cond解決性能問題。看起來已經(jīng)和上面Golang的邏輯非常接近了。如果你使用Golang寫過代碼,并且上面C++的代碼你也嘗試寫出來了,你就會(huì)驚嘆于Golang簡單了。好了,這里不吹Golang了,我們繼續(xù)回到C++上來。

當(dāng)然,到這里還遠(yuǎn)遠(yuǎn)沒完呢,C++的看家本領(lǐng)是啥?對,是面向?qū)ο缶幊獭I厦娴拇a很顯然沒有面向?qū)ο蟮奈兜馈O旅嫖覀兙褪褂妹嫦驅(qū)ο蟮乃枷雭韺?shí)現(xiàn)一個(gè)線程池。這里直接給出代碼

#include 
#include 
#include 
#include 


class TPool {
public:
    TPool(): m_thread_size(1), m_terminate(false) {};
    ~TPool() { stop(); }
    // 線程池初始化
    bool init(size_t size);
    // 停止所有線程
    void stop();
    // 啟動(dòng)線程池的各個(gè)線程
    void start();
    // 任務(wù)執(zhí)行入口
    template 
    auto exec(F&& f, A&&... args)->std::future;
    // 等待所有線程執(zhí)行完成
    bool waitDone();


private:
    // 每個(gè)任務(wù)都是一個(gè)struct結(jié)構(gòu)體,方便未來擴(kuò)展
    struct Task {
        Task() {}
        std::function m_func;
    };
    // Task未來在隊(duì)列中以智能指針的形式傳遞
    typedef std::shared_ptr TaskFunc;


private:
    // 嘗試從任務(wù)隊(duì)列獲取一個(gè)任務(wù)
    bool get(TaskFunc &t);
    // 線程主函數(shù)
    bool run();


private:
    // 任務(wù)隊(duì)列,將Task直接放到隊(duì)列中
    std::queue m_tasks;
    // 線程池
    std::vector m_threads;
    // 鎖
    std::mutex m_mutex;
    // 用于線程之間通知的條件變量
    std::condition_variable m_cond;
    // 線程池大小
    size_t m_thread_size;
    // 標(biāo)記線程是否結(jié)束
    bool m_terminate;
    // 用來記錄狀態(tài)的原子變量
    std::atomic m_atomic{0};
};

我們定義了一個(gè)TPool類,這個(gè)類里面包含幾個(gè)部分,我們從下往上看。第一個(gè)部分是線程池管理相關(guān)的各種資源,每一個(gè)我都寫了注釋。第二部分是任務(wù)相關(guān)的操作,這部分不對外開放。第三部分是任務(wù)定義,使用struct聲明了一個(gè)Task,其中有一個(gè)空的構(gòu)造函數(shù),還聲明了一個(gè)m_func,這是最終task執(zhí)行的入口。最后一部分是線程池對外開放的各種接口。用戶使用線程的大致流程如下:

04e99b1c-a07e-11ee-8b88-92fbcf53809c.png

這里我將線程的初始化和線程的啟動(dòng)分成了兩步,是希望在使用的時(shí)候精確知道是哪一步出了問題,如果你覺得這樣太繁瑣,可以適當(dāng)減少步驟,比如將init和start方法進(jìn)行合并。

下面我們就來詳細(xì)講一下線程各個(gè)方法的實(shí)現(xiàn)。首先是init和start方法,init方法用來初始化線程,代碼如下:

bool init(size_t size) {
    unique_lock lock(m_mutex);
    if (!m_threads.empty()) {
        return false;
    }
    m_thread_size = size;
    return true;
}

傳入一個(gè)size,表示線程池的大小,上來就加鎖,這是為了防止在多線程的情景下執(zhí)行init,這個(gè)方法實(shí)際上只做了一件事,就是設(shè)置線程池的大小。

初始化完了之后,調(diào)用start方法啟動(dòng)線程池,start方法的代碼如下:

bool start() {
    unique_lock lock(m_mutex);
    if (!m_threads.empty()) {
        return false;
    }


    for (size_t i = 0; i < m_thread_size; i++) {
        m_threads.push_back(new thread(&TPool::run, this));
    }
    return true;
}
同樣,為了防止多線程語境,上來也是加了一把鎖,如果m_threads不為空說明已經(jīng)初始化過了,直接就返回了。接著就創(chuàng)建線程放到m_threads這個(gè)vector中,線程的主函數(shù)是當(dāng)前類的run方法。這樣,所有線程的主函數(shù)都跑起來了,接下來我們看一下線程主函數(shù)的代碼,如下:
void run() {
    while(!m_terminate) {
        TaskFunc task;
        bool ok = get(task);
        if (ok) {
            ++m_atomic;


            task->m_func();


            --m_atomic;


            unique_lock lock(m_mutex);
            if (m_atomic == 0 && m_tasks.empty()) { // 是否所有任務(wù)都執(zhí)行完成
                m_cond.notify_all();
            }
        }
    }
}

不出所料,run方法里其實(shí)就是一個(gè)死循環(huán),這個(gè)循環(huán)上來就判斷是否結(jié)束了,如果已經(jīng)結(jié)束就退出當(dāng)前循環(huán),run方法返回,當(dāng)前線程結(jié)束。

如果沒有結(jié)束,就調(diào)用get方法從任務(wù)隊(duì)列里取一個(gè)任務(wù)出來執(zhí)行,這里使用一個(gè)原子變量來判斷最后是不是所有任務(wù)都執(zhí)行完成,這個(gè)原子變量不是必須的,你可以根據(jù)你自己的場景做相應(yīng)的修改。取到任務(wù)之后,就會(huì)調(diào)用任務(wù)的m_func方法,還記得這個(gè)方法嗎?它定義在Task結(jié)構(gòu)體中。最后會(huì)判斷是否所有任務(wù)都結(jié)束了,如果已經(jīng)結(jié)束了會(huì)通知其它線程。

這里我們來看一下get方法是怎么獲取任務(wù)的,get方法的代碼如下:

bool get(TaskFunc &t) {
    unique_lock lock(m_mutex);
    if (m_tasks.empty()) {
        m_cond.wait(lock, [this]{return m_terminate || !m_tasks.empty();});
    }


    if (m_terminate)
        return false;


    t = std::move(m_tasks.front());
    m_tasks.pop();
    return true;
}

上來首先加了一把鎖,如果任務(wù)隊(duì)列沒有任務(wù)可以執(zhí)行,使用條件變量m_cond調(diào)用wait方法等待。

然后,如果此時(shí)線程已經(jīng)被結(jié)束掉了,直接返回false,如果沒有結(jié)束,就從隊(duì)列中取出一個(gè)任務(wù),賦值給傳進(jìn)來的t。注意,這里使用的是參數(shù)傳值的方式。這樣就實(shí)現(xiàn)了任務(wù)的傳遞,當(dāng)沒有任務(wù)的時(shí)候m_cond.wait會(huì)讓當(dāng)前進(jìn)程進(jìn)入睡眠,等待通知。

接下來,我們看一下任務(wù)是如何被投遞到任務(wù)隊(duì)列中的,用來投遞任務(wù)的方法是exec,代碼如下:

template 
auto exec(F&& f, A&&... args)->future {
    using retType = decltype(f(args...));
    auto task = make_shared>(bind(std::forward(f), std::forward(args)...));
    TaskFunc fPtr = make_shared();
    fPtr->m_func = [task](){
        (*task)();
    };


    unique_lock lock(m_mutex);
    m_tasks.push(fPtr);
    m_cond.notify_one();


    return task->get_future();
}

exec方法稍微有一點(diǎn)復(fù)雜,知識點(diǎn)非常密集,我們簡單過一下邏輯。首先,我們將exec方法聲明成了模板函數(shù),有兩個(gè)參數(shù),F(xiàn)表示任務(wù)最終執(zhí)行的方法,A是一個(gè)可變參數(shù),實(shí)際就是傳給函數(shù)f的參數(shù),返回值只有在運(yùn)行的時(shí)候才會(huì)知道,所以這里使用了自動(dòng)類型推導(dǎo),并且配合了decltype關(guān)鍵字,->future這句的意思是最終會(huì)返回一個(gè)future,這個(gè)future的類型是由decltype推導(dǎo)出f函數(shù)返回值的類型,這里有點(diǎn)繞,如果看不明白的話還是得去看一下future和decltype是怎么回事。

進(jìn)入函數(shù)內(nèi)部,我們一行一行講,首先是

using retType = decltype(f(args...));
decltype用于查詢表達(dá)的類型,這里的語義表達(dá)的就是f(args…)這個(gè)表達(dá)式最終返回的類型。接著,下一行是創(chuàng)建一個(gè)task,這個(gè)task是一個(gè)智能指針

autotask=make_shared>(bind(std::forward(f),std::forward(args)...));

首先,最外層make_shared是創(chuàng)建一個(gè)智能指針這沒什么可說的。這里的std::packaged_task會(huì)根據(jù)前面推導(dǎo)出的類型創(chuàng)建出一個(gè)future對象,后面的bind是將這個(gè)函數(shù)和后面的可變參數(shù)綁定起來。這樣在函數(shù)調(diào)用的時(shí)候就可以獲取到參數(shù)了。

接著是創(chuàng)建Task類型的智能指針,并將剛剛創(chuàng)建好的函數(shù)放到Task結(jié)構(gòu)中的m_func中

TaskFunc fPtr = make_shared();
fPtr->m_func = [task](){
    (*task)();
};

上面用了一個(gè)Lambda表達(dá)式創(chuàng)建一個(gè)函數(shù),并將這個(gè)函數(shù)賦值給了m_func,最終任務(wù)執(zhí)行的其實(shí)就是這個(gè)Lambda表達(dá)式函數(shù),在這個(gè)函數(shù)中才最終調(diào)用傳進(jìn)來的方法。此時(shí),fPtr實(shí)際上就是一個(gè)Task對象,我們在類中重命名成了TaskFunc。接著將這個(gè)Task放到隊(duì)列中,注意要加鎖。最后將future對象返回出去。這意味著我們調(diào)用exec方法之后可以得到一個(gè)future對象。

exec方法是整個(gè)線程池中最復(fù)雜的部分了,涉及到很多C++的知識,后面有時(shí)間我會(huì)專門開幾篇文章單獨(dú)深入的去剖析這部分內(nèi)容。

最后,我們來看一下其它的幾個(gè)方法,首先是線程的停止,如下:

void stop() {
    {
        unique_lock lock(m_mutex);
        m_terminate = true;
        m_cond.notify_all();
    }


    for (auto & m_thread : m_threads) {
        if (m_thread->joinable()) {
            m_thread->join();
        }
        delete m_thread;
        m_thread = nullptr;
    }


    unique_lock lock(m_mutex);
    m_threads.clear();
}

這里我們使用了一對大括號將部分代碼包起來了,這種用法其實(shí)是為了讓鎖更早的釋放,unique_lock出了大括號就會(huì)被銷毀,從而調(diào)用析構(gòu)函數(shù)進(jìn)而釋放鎖。接著是等待各個(gè)線程結(jié)束,其實(shí)就是將m_terminate置為true,run里面的死循環(huán)跳出循環(huán),線程主函數(shù)返回。然后是清除各種資源。

最后我們實(shí)際用一下這個(gè)線程池,代碼如下:

#include "thread-pool.hpp"
#include 


using namespace std;


void threadFunc(int a) {
   cout << "a=" << a << endl;
}


class A {
public:
    A() = default;
    int run(int a, int b) {
        return a + b;
    }
};


int main() {
    TPool p1;
    p1.init(10);
    p1.start();
    p1.exec(threadFunc, 100);
    p1.exec(threadFunc, 200);


    A a1;
    auto fu1 = p1.exec(std::bind(&A::run, &a1, std::_1, std::_2), 10, 20);
    int ret = fu1.get();
    std::cout << "res:" << ret << std::endl;


    p1.waitDone();
    return 0;
}

可以看到,除了使用方法外,我們還可以使用一個(gè)類方法作為線程的主函數(shù),當(dāng)然,主函數(shù)是一個(gè)模板函數(shù),你可以傳任意的類型,好,到這里我整個(gè)線程池就實(shí)現(xiàn)完了。

總結(jié)

這篇文章我們使用C++11新特性實(shí)現(xiàn)了一個(gè)通用的線程池,我們先是使用Golang寫了一個(gè)簡單的協(xié)程池,然后順著相同的思路通過隊(duì)列傳參的形式實(shí)現(xiàn)了一個(gè)初級版本,但還沒有結(jié)束,因?yàn)镃++是支持面向?qū)ο缶幊痰模晕覀冇质褂妹嫦驅(qū)ο蟮姆绞綄?shí)現(xiàn)了最終的版本。

當(dāng)然,上面只是線程池實(shí)現(xiàn)的其中一種方式。并且很多C++相關(guān)的新特性也沒有提到,比如thread_local,這部分內(nèi)容還是需要你自己去探索了。






審核編輯:劉清

  • Linux系統(tǒng)
    +關(guān)注

    關(guān)注

    4

    文章

    593

    瀏覽量

    27397
  • C語言
    +關(guān)注

    關(guān)注

    180

    文章

    7604

    瀏覽量

    136813
  • 線程池
    +關(guān)注

    關(guān)注

    0

    文章

    57

    瀏覽量

    6846
  • for循環(huán)
    +關(guān)注

    關(guān)注

    0

    文章

    61

    瀏覽量

    2503

原文標(biāo)題:新特性深度探索:實(shí)現(xiàn)一個(gè)通用線程池

文章出處:【微信號:CPP開發(fā)者,微信公眾號:CPP開發(fā)者】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。

收藏 人收藏

    評論

    相關(guān)推薦

    C語言線程實(shí)現(xiàn)方案

    這是個(gè)簡單小巧的C語言線程實(shí)現(xiàn),在 Github 上有 1.1K 的 star,很適合用來學(xué)
    的頭像 發(fā)表于 01-29 16:43 ?1540次閱讀

    Java中的線程包括哪些

    java.util.concurrent 包來實(shí)現(xiàn)的,最主要的就是 ThreadPoolExecutor 類。 Executor: 代表線程的接口,有
    的頭像 發(fā)表于 10-11 15:33 ?817次閱讀
    Java中的<b class='flag-5'>線程</b><b class='flag-5'>池</b>包括哪些

    線程是如何實(shí)現(xiàn)

    線程的概念是什么?線程是如何實(shí)現(xiàn)的?
    發(fā)表于 02-28 06:20

    《深入理解C++11C++11特性解析與應(yīng)用的詳細(xì)電子教材免費(fèi)下載

    國內(nèi)首本全面深入解讀 C++11 新標(biāo)準(zhǔn)的專著,由 C++ 標(biāo)準(zhǔn)委員會(huì)代表和 IBM XL 編譯器中國開發(fā)團(tuán)隊(duì)共同撰寫。不僅詳細(xì)闡述了 C++11 標(biāo)準(zhǔn)的設(shè)計(jì)原則,而且系統(tǒng)地講解了 C++11
    發(fā)表于 08-27 08:00 ?0次下載

    基于Nacos的簡單動(dòng)態(tài)化線程實(shí)現(xiàn)

    本文以Nacos作為服務(wù)配置中心,以修改線程核心線程數(shù)、最大線程數(shù)為例,實(shí)現(xiàn)
    發(fā)表于 01-06 14:14 ?863次閱讀

    如何用C++實(shí)現(xiàn)個(gè)線程呢?

    C++線程種多線程管理模型,把線程分成任務(wù)執(zhí)行和線程
    發(fā)表于 06-08 14:53 ?1779次閱讀
    如何用<b class='flag-5'>C</b>++<b class='flag-5'>實(shí)現(xiàn)</b><b class='flag-5'>一</b><b class='flag-5'>個(gè)</b><b class='flag-5'>線程</b><b class='flag-5'>池</b>呢?

    細(xì)數(shù)線程的10個(gè)

    JDK開發(fā)者提供了線程實(shí)現(xiàn)類,我們基于Executors組件,就可以快速創(chuàng)建個(gè)線程
    的頭像 發(fā)表于 06-16 10:11 ?728次閱讀
    細(xì)數(shù)<b class='flag-5'>線程</b><b class='flag-5'>池</b>的10<b class='flag-5'>個(gè)</b>坑

    線程的兩個(gè)思考

    今天還是說一下線程的兩個(gè)思考。 池子 我們常用的線程, JDK的ThreadPoolExecutor. CompletableFutur
    的頭像 發(fā)表于 09-30 11:21 ?3106次閱讀
    <b class='flag-5'>線程</b><b class='flag-5'>池</b>的兩<b class='flag-5'>個(gè)</b>思考

    Spring 的線程應(yīng)用

    我們在日常開發(fā)中,經(jīng)常跟多線程打交道,Spring 為我們提供了個(gè)線程方便我們開發(fā),它就是 ThreadPoolTaskExecutor
    的頭像 發(fā)表于 10-13 10:47 ?622次閱讀
    Spring 的<b class='flag-5'>線程</b><b class='flag-5'>池</b>應(yīng)用

    線程基本概念與原理

    、17、20等的新特性,簡化了多線程編程的實(shí)現(xiàn)。 提高性能與資源利用率 線程主要解決兩個(gè)問題:
    的頭像 發(fā)表于 11-10 10:24 ?537次閱讀

    線程的基本概念

    線程的基本概念 不管線程是什么東西!但是我們必須知道線程被搞出來的目的就是:提高程序執(zhí)行效
    的頭像 發(fā)表于 11-10 16:37 ?526次閱讀
    <b class='flag-5'>線程</b><b class='flag-5'>池</b>的基本概念

    如何用C++11實(shí)現(xiàn)自旋鎖

    下面我會(huì)分析下自旋鎖,并代碼實(shí)現(xiàn)自旋鎖和互斥鎖的性能對比,以及利用C++11實(shí)現(xiàn)自旋鎖。 :自旋鎖(spin lock) 自旋鎖是
    的頭像 發(fā)表于 11-11 16:48 ?1440次閱讀
    如何用<b class='flag-5'>C++11</b><b class='flag-5'>實(shí)現(xiàn)</b>自旋鎖

    基于C++11線程實(shí)現(xiàn)

    C++11 加入了線程庫,從此告別了標(biāo)準(zhǔn)庫不支持并發(fā)的歷史。然而 c++ 對于多線程的支持還是比較低級,稍微高級點(diǎn)的用法都需要自己去
    的頭像 發(fā)表于 11-13 15:29 ?765次閱讀

    線程的創(chuàng)建方式有幾種

    線程種用于管理和調(diào)度線程的技術(shù),能夠有效地提高系統(tǒng)的性能和資源利用率。它通過預(yù)先創(chuàng)建線程
    的頭像 發(fā)表于 12-04 16:52 ?867次閱讀

    什么是動(dòng)態(tài)線程?動(dòng)態(tài)線程的簡單實(shí)現(xiàn)思路

    因此,動(dòng)態(tài)可監(jiān)控線程種針對以上痛點(diǎn)開發(fā)的線程管理工具。主要可實(shí)現(xiàn)功能有:提供對 Sprin
    的頭像 發(fā)表于 02-28 10:42 ?645次閱讀
    主站蜘蛛池模板: 草久视频在线观看| 免费看黄资源大全高清| 日本久久综合视频| 国产一卡二卡3卡4卡四卡在线视频| 亚洲第一在线| 日不卡在线| 插插插天天| 日本加勒比一区| 中出丰满大乳中文字幕| 欧美影院一区二区| 午夜精品久久久久久91| xxxx日本老师hd| 色婷婷六月丁香七月婷婷| 最新大黄网站免费| h网站国产| 人与禽性视频77777| 天天躁夜夜躁狠狠躁2018a| 亚洲美女视频一区| 欧美两性网| 就操| 在线播放一区二区精品产| 国内露脸夫妇交换精品| 成人深夜视频| 四虎影库在线播放| 午夜精品久久久久久毛片| 视频在线一区二区| 九色精品在线| www.av日韩| 日韩不卡毛片| 久久系列| 天天噜噜色| 狠狠做深爱婷婷综合一区| 福利视频入口| 色综合激情网| 天天草b| 有码视频在线观看| 操美女网址| 男女视频在线播放| 国产精品美女免费视频大全| 国产在线h| 免费的黄色片|