色哟哟视频在线观看-色哟哟视频在线-色哟哟欧美15最新在线-色哟哟免费在线观看-国产l精品国产亚洲区在线观看-国产l精品国产亚洲区久久

0
  • 聊天消息
  • 系統消息
  • 評論與回復
登錄后你可以
  • 下載海量資料
  • 學習在線課程
  • 觀看技術視頻
  • 寫文章/發帖/加入社區
會員中心
創作中心

完善資料讓更多小伙伴認識你,還能領取20積分哦,立即完善>

3天內不再提示

使用C++11新特性實現一個通用的線程池設計

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

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

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

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

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

C++編譯器會自動為我們插入釋放鎖的代碼,這樣我們就不用時刻擔心鎖有沒有正確釋放了。我個人的感覺是,從C++11標準開始,C++變得不那么可怕了,甚至很多時候覺得C++變得好用了。

這篇文章我們試著使用C++11標準的新特性來實現一個通用的線程池。首先,我們來看一下C++11標準中線程是如何創建的,先看代碼:

#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創建一個t1線程對象,傳入線程主函數,將x以引用的方式傳入線程主函數,接著調用join方法等待主函數threadFunc執行完成。上面x最終的結果將是20。

整個流程和使用pthread_create其實區別不大,只是C++11將線程的創建封裝成了一個類,使得我們可以用面向對象編程的方式來創建多線程。

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

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

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

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

為了說明這個問題,我們先來看一段Golang的代碼,暫時看不懂沒關系,后面我會解釋

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


  buildWorkers(poolSize, msg, notify)


  // 模擬任務
  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方法創建了10個協程,每個協程里面都有一個for循環,一開始每個for循環都阻塞在select語句中的case m := ←msg,如果之前沒有接觸過Go語言(這里的select你可以簡單和Linux中的select技術類比)。另外還有兩個通道,一個是msg和notify,msg用來傳遞參數,notify用來通知外面這個協程任務已經執行完了。

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

運行上面的代碼,得到的結果大概像下面這樣

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

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

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

注意箭頭的方向,所有協程都不斷的嘗試從channel中接收消息,拿到程序運行必要的參數,當msg中有數據時從case m := <-msg中蘇醒并執行具體的業務邏輯,我們知道,在Golang中channel是線程安全的,其內部有一把鎖,這把鎖就是mutex,下面是channel底層結構體

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

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

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

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

下面,我們就參照上面Golang的代碼,先把這個框架給搭出來,然后再在這個基礎之上去完善代碼,最后實現一個準生產的線程池。

我們先參照上面Golang的代碼,實現相似邏輯,代碼如下:

#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函數中,創建了一個隊列q1,這個隊列用來向線程池傳遞參數,接著創建了10個線程保存在了vector中,將q1以引用的形式傳入線程池主函數(注意:這里傳引用必須使用std::ref包裝一下),再接著調用jobDispatch模擬任務分配然后每個線程調用join等待結束。

接著我們來看線程池主函數threadFunc,這個函數接收一個隊列q作為參數,這里的q就是我們在創建線程池的時候傳進來的q1,然后是一個死循環,在這個循環里面我們不斷的判斷隊列是否為空,如果不為空就從隊列取出一個元素出來。最后,分配任務的函數jobDispatch向隊列q1里面push了1000個元素,來模擬1000個任務。

上面的代碼當然是有問題的,有興趣的可以把這段代碼拷貝下來把自己跑一下,你會發現雖然代碼能跑,但是結果完全看不懂。

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

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函數中的出隊列之前加了一把鎖。這把鎖是全局的,每個線程都要先拿到這把鎖之后才能從隊列里拿到數據,你可以把這段代碼替換之后再運行一下,這次的結果應該是正確的了。

可能你覺得奇怪,我們使用lock_guard創建了一個ltx對象,但是并沒有地方去釋放這把鎖,如果你有這樣的疑問應該是對C++11還不是很熟悉,在C++11標準中,因為有了RAII的緣故,一個對象被銷毀時一定會執行析構函數,就算是運行過程中對象產生異常析構函數也會執行,在C++中這叫棧展開。有了這個特性之后,lock_guard就不難理解了,其構造函數其實就是調用了mutex的lock方法,然后把這個mutex保存在成員變量中,當對象銷毀時析構函數中調用unlock。所以,有了這個機制之后,我們就不用到處寫unlock了,這也是我覺得C++更好用了的原因之一。

在C++中同樣遵循大括號作用域,在上面的代碼中,lock_guard是在if語句中的,當if語句執行完之后,ltx就被銷毀了,所以當循環進入到下一次的時候實際上鎖已經被釋放了。

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

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

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), 首先我們定義了一個全局的條件變量cond,然后在threadFunc中調用cond的wait方法等待通知。然后在jobDispatch中往隊列里面寫完數據之后調用notify_all方法通知所有等待的線程。這樣,當隊列中沒有數據的時候線程池中的線程就會進入睡眠,直到notify_all被調用。這里你可以想一下,上面notify_all還可以進一步優化嗎?

當然,上面還作了一個調整,就是將原來的lock_guard換了unique_lock,這個改動是必須的,我們知道cond調用wait的時候會嘗試釋放鎖,可lock_guard里面沒有釋放鎖的方法,而unique_lock是有unlock方法的。也就是說,unique_lock創建的ltx對象可以手動調用unlock方法釋放鎖。

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

當然,到這里還遠遠沒完呢,C++的看家本領是啥?對,是面向對象編程。上面的代碼很顯然沒有面向對象的味道。下面我們就使用面向對象的思想來實現一個線程池。這里直接給出代碼

#include 
#include 
#include 
#include 


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


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


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


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

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

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

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

下面我們就來詳細講一下線程各個方法的實現。首先是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;
}

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

初始化完了之后,調用start方法啟動線程池,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不為空說明已經初始化過了,直接就返回了。接著就創建線程放到m_threads這個vector中,線程的主函數是當前類的run方法。這樣,所有線程的主函數都跑起來了,接下來我們看一下線程主函數的代碼,如下:
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()) { // 是否所有任務都執行完成
                m_cond.notify_all();
            }
        }
    }
}

不出所料,run方法里其實就是一個死循環,這個循環上來就判斷是否結束了,如果已經結束就退出當前循環,run方法返回,當前線程結束。

如果沒有結束,就調用get方法從任務隊列里取一個任務出來執行,這里使用一個原子變量來判斷最后是不是所有任務都執行完成,這個原子變量不是必須的,你可以根據你自己的場景做相應的修改。取到任務之后,就會調用任務的m_func方法,還記得這個方法嗎?它定義在Task結構體中。最后會判斷是否所有任務都結束了,如果已經結束了會通知其它線程。

這里我們來看一下get方法是怎么獲取任務的,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;
}

上來首先加了一把鎖,如果任務隊列沒有任務可以執行,使用條件變量m_cond調用wait方法等待。

然后,如果此時線程已經被結束掉了,直接返回false,如果沒有結束,就從隊列中取出一個任務,賦值給傳進來的t。注意,這里使用的是參數傳值的方式。這樣就實現了任務的傳遞,當沒有任務的時候m_cond.wait會讓當前進程進入睡眠,等待通知。

接下來,我們看一下任務是如何被投遞到任務隊列中的,用來投遞任務的方法是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方法稍微有一點復雜,知識點非常密集,我們簡單過一下邏輯。首先,我們將exec方法聲明成了模板函數,有兩個參數,F表示任務最終執行的方法,A是一個可變參數,實際就是傳給函數f的參數,返回值只有在運行的時候才會知道,所以這里使用了自動類型推導,并且配合了decltype關鍵字,->future這句的意思是最終會返回一個future,這個future的類型是由decltype推導出f函數返回值的類型,這里有點繞,如果看不明白的話還是得去看一下future和decltype是怎么回事。

進入函數內部,我們一行一行講,首先是

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

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

首先,最外層make_shared是創建一個智能指針這沒什么可說的。這里的std::packaged_task會根據前面推導出的類型創建出一個future對象,后面的bind是將這個函數和后面的可變參數綁定起來。這樣在函數調用的時候就可以獲取到參數了。

接著是創建Task類型的智能指針,并將剛剛創建好的函數放到Task結構中的m_func中

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

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

exec方法是整個線程池中最復雜的部分了,涉及到很多C++的知識,后面有時間我會專門開幾篇文章單獨深入的去剖析這部分內容。

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

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();
}

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

最后我們實際用一下這個線程池,代碼如下:

#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;
}

可以看到,除了使用方法外,我們還可以使用一個類方法作為線程的主函數,當然,主函數是一個模板函數,你可以傳任意的類型,好,到這里我整個線程池就實現完了。

總結

這篇文章我們使用C++11新特性實現了一個通用的線程池,我們先是使用Golang寫了一個簡單的協程池,然后順著相同的思路通過隊列傳參的形式實現了一個初級版本,但還沒有結束,因為C++是支持面向對象編程的,所以我們又使用面向對象的方式實現了最終的版本。

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






審核編輯:劉清

  • Linux系統
    +關注

    關注

    4

    文章

    593

    瀏覽量

    27392
  • C語言
    +關注

    關注

    180

    文章

    7604

    瀏覽量

    136692
  • 線程池
    +關注

    關注

    0

    文章

    57

    瀏覽量

    6844
  • for循環
    +關注

    關注

    0

    文章

    61

    瀏覽量

    2502

原文標題:新特性深度探索:實現一個通用線程池

文章出處:【微信號:CPP開發者,微信公眾號:CPP開發者】歡迎添加關注!文章轉載請注明出處。

收藏 人收藏

    評論

    相關推薦

    C語言線程實現方案

    這是簡單小巧的C語言線程實現,在 Github 上有 1.1K 的 star,很適合用來學
    的頭像 發表于 01-29 16:43 ?1535次閱讀

    Java中的線程包括哪些

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

    線程是如何實現

    線程的概念是什么?線程是如何實現的?
    發表于 02-28 06:20

    《深入理解C++11C++11特性解析與應用的詳細電子教材免費下載

    國內首本全面深入解讀 C++11 新標準的專著,由 C++ 標準委員會代表和 IBM XL 編譯器中國開發團隊共同撰寫。不僅詳細闡述了 C++11 標準的設計原則,而且系統地講解了 C++11
    發表于 08-27 08:00 ?0次下載

    基于Nacos的簡單動態化線程實現

    本文以Nacos作為服務配置中心,以修改線程核心線程數、最大線程數為例,實現
    發表于 01-06 14:14 ?863次閱讀

    如何用C++實現線程呢?

    C++線程種多線程管理模型,把線程分成任務執行和線程
    發表于 06-08 14:53 ?1765次閱讀
    如何用<b class='flag-5'>C</b>++<b class='flag-5'>實現</b><b class='flag-5'>一</b><b class='flag-5'>個</b><b class='flag-5'>線程</b><b class='flag-5'>池</b>呢?

    細數線程的10

    JDK開發者提供了線程實現類,我們基于Executors組件,就可以快速創建線程
    的頭像 發表于 06-16 10:11 ?722次閱讀
    細數<b class='flag-5'>線程</b><b class='flag-5'>池</b>的10<b class='flag-5'>個</b>坑

    線程的兩思考

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

    Spring 的線程應用

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

    線程基本概念與原理

    、17、20等的新特性,簡化了多線程編程的實現。 提高性能與資源利用率 線程主要解決兩問題:
    的頭像 發表于 11-10 10:24 ?528次閱讀

    線程的基本概念

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

    如何用C++11實現自旋鎖

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

    基于C++11線程實現

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

    線程的創建方式有幾種

    線程種用于管理和調度線程的技術,能夠有效地提高系統的性能和資源利用率。它通過預先創建線程
    的頭像 發表于 12-04 16:52 ?856次閱讀

    什么是動態線程?動態線程的簡單實現思路

    因此,動態可監控線程種針對以上痛點開發的線程管理工具。主要可實現功能有:提供對 Sprin
    的頭像 發表于 02-28 10:42 ?639次閱讀
    主站蜘蛛池模板: 晚夜免费禁用十大亏亏| 国产免费人成在线视频视频| 在线视频久久只有精品第一日韩 | av老司机色爱区综合| 在线高清电影理论片4399| 亚洲久热无码中文字幕| 小泽玛丽av无码观看| 无颜之月全集免费观看| 我解开了岳的乳第一个女人| 肉多荤文高h羞耻校园| 揉抓捏打抽插射免费视频| 三级色视频| 十九岁韩国电影在线观看| 日本二区三区欧美亚洲国| 人人舔人人爱| 日韩精品无码视频一区二区蜜桃| 日本伦理电影聚| 色偷偷影院| 午夜福利院电影| 香蕉鱼视频观看在线视频下载| 亚洲AV无码久久流水呻蜜桃久色 | 蜜桃人妻无码AV天堂三区 | 纯肉合集(高H)| 鬼灭之刃花街篇免费樱花动漫| 国产产一区二区三区久久毛片国语| 高h浪荡文辣文神奇宝贝| 国产精品青青草原app大全| 国产免费人成在线看视频| 湖南张丽大战黑人hd视频| 久久久久久久电影| 蜜柚视频高清在线| 人人模人人干| 天天久久影视色香综合网| 亚洲国产成人综合| 一区二区三区国产亚洲网站| 51精品国产AV无码久久久| 9亚洲欧洲免费无码在线| 俄罗斯xxxxxbbbbb| 国精产品一区二区三区四区糖心| 极品色αv影院| 男女亲吻摸下面吃奶视频|