用C++11實現(xiàn)一個簡單的線程池

0.為什么需要線程池滨巴?

當(dāng)我們需要完成一些持續(xù)時間短、發(fā)生頻率高的工作時,每次為他們開啟一個線程既顯得繁瑣又會造成不必要的開銷夭咬,所以為這一類工作寫一個簡單的線程池就很有必要了。

1.如何設(shè)計我們的線程池铆隘?

考慮這樣一個應(yīng)用場景卓舵,我們有一個設(shè)計軟件,當(dāng)我們需要下載一些模板用作接下來的設(shè)計工作膀钠。我們顯然不能將下載工作放在主線程去完成掏湾,如果你的用戶網(wǎng)絡(luò)速度飛快還好說,一旦他的網(wǎng)絡(luò)稍有波動肿嘲,你的設(shè)計軟件將會停止GUI界面的渲染融击,卡在那邊不懂,這顯然不是好的體驗雳窟。正確的做法是另外開啟一個線程去下載尊浪,一旦這個下載工作完成后,我們可以通知主線程去做一個提示,例如彈出一個對話框告訴用戶模板已經(jīng)可以用了拇涤。
如果這個設(shè)計軟件需要頻繁的做下載工作捣作,那么每次單開一個線程就比較麻煩。我們可以將下載任務(wù)打包好工育,一個一個的推送到隊列中虾宇,由線程池自動從隊列中獲取任務(wù),完成下載工作如绸。當(dāng)隊列為空時嘱朽,線程池里面的線程就進入等待狀態(tài),這也不會造成資源的占用怔接。

2.先完成隊列的設(shè)計

STL中提供了一個關(guān)于隊列的實現(xiàn)std::queue搪泳,但是std::queue并不是線程安全的,我們需要在此基礎(chǔ)上進行一些包裝扼脐,讓它在多線程的情況下也可以使用岸军。
我們的隊列需要擁有這幾個功能:
1.從隊列里彈出一個任務(wù)
2.向隊列里推送任務(wù)
3.了解隊列里的任務(wù)還有多少個
4.清除任務(wù)隊列
下面是一個簡單的線程安全隊列:

namespace multi_thread {
    template<typename Task>
    class thread_safe_queue {
    public:
        thread_safe_queue() :done_(false) {

        }

        void push(const Task& t) {
            std::lock_guard<std::mutex> lock(mtx_);
            queue_.push(t);
            ready_.notify_one();
        }

        void wait_and_pop(Task& t) {
            std::unique_lock<std::mutex> lock(mtx_);
            if (queue_.empty() && !done_)
                ready_.wait(lock);
            if (done_)
                return;
            t = queue_.front();
            queue_.pop();
        }

        bool empty()const {
            std::lock_guard<std::mutex> lock(mtx_);
            return queue_.empty();
        }

        void clear() {
            std::lock_guard<std::mutex> lock(mtx_);
            for (int i = 0; i < queue_.size(); ++i)
                queue_.pop();
        }

        void done() {
            done_ = true;
            ready_.notify_all();
        }
    private:
        std::queue<Task> queue_;
        mutable std::mutex mtx_;
        std::condition_variable ready_;
        std::atomic_bool done_;
    };
}

簡單講解一下:
我們的線程安全隊列thread_safe_queue遵循了STL的命名規(guī)范组力,對于任意容器猴誊,他們的判斷為空的函數(shù)簽名都是bool empty()const;,清除容器中的內(nèi)容的都為void clear()提佣。對于推入任務(wù)的函數(shù)肚吏,我們跟std::queue保持了一致方妖,而彈出函數(shù)的命名我們做了一些改變,與函數(shù)的操作更加切合罚攀,表示對std::queue::pop()的一種擴展党觅。

這里我們采用了std::mutex作為我們的互斥量,每次訪問隊列中的內(nèi)容時斋泄,都用std::lock_guard或者std::unique_lock加鎖杯瞻,保證在同一時段只有一個線程可以讀寫隊列。std::lock_guard是一個方便的RAII的體現(xiàn)炫掐,他會在析構(gòu)時自動調(diào)用unlock()魁莉,std::unique_lock與之的區(qū)別就是,你可以顯式調(diào)用unlock()函數(shù)完成解鎖募胃,它更加的靈活沛厨,當(dāng)然你可以用{}std::lock_guard完成相同的工作。我們將std::mutex_設(shè)計成mutable摔认,是因為我們需要在const member functionempty()中使用它逆皮。
push函數(shù)中,每次完成push后就調(diào)用std::conditional_variable::notify_one()來喚醒一個處于等待狀態(tài)的線程参袱,通知他隊列中已經(jīng)有任務(wù)了电谣,可以開始彈出了秽梅。在對應(yīng)了wait_and_pop函數(shù)中,同樣有一個std::conditional_variable::wait()剿牺,這個函數(shù)可以傳入一個或者兩個參數(shù)企垦,第一個參數(shù)表示要釋放的鎖,第二個函數(shù)是一個predicate晒来,不接受參數(shù)钞诡,返回值為bool。他會在調(diào)用wait時首先調(diào)用這個predicate湃崩,如果值是true荧降,那么就會重新獲得鎖并繼續(xù)向下執(zhí)行,如果值是false攒读,他會釋放鎖并且進入等待狀態(tài)朵诫,直到接收到前面所說的喚醒通知,他才會重新檢查predicate并做前述操作薄扁。這里我們沒有使用predicate剪返,僅使用了簡單的版本,他會在接收到喚醒通知時直接向下執(zhí)行邓梅。
再來看最后一個變量std::atomic_bool脱盲,它是標(biāo)準(zhǔn)庫提供的一個原子類型,在訪問時標(biāo)準(zhǔn)保證在同一時刻只有一個線程在讀寫他日缨。原子類型是不可拷貝且不可移動的钱反,所以我們不能對他進行類內(nèi)初始化,這會調(diào)用copy constructor殿遂,正確的方法是使用constructor initializer list進行初始化诈铛,當(dāng)然你也可以在構(gòu)造函數(shù)體內(nèi)使用賦值運算符給其賦上一個值乙各,但是請務(wù)必在這兩個中間選擇一個墨礁。回到代碼耳峦,這個變量的作用很簡單恩静,就是讓彈出的函數(shù)wait_and_pop直接結(jié)束,我們只有在需要析構(gòu)這個線程池時才會這么做蹲坷,具體做法在線程池中細說驶乾。

3.完成線程池的設(shè)計

在啟用線程池時,我們需要指定在線程池中存放的線程的數(shù)量循签,當(dāng)然我們也可以在后續(xù)為線程池添加新的線程级乐。
我們將使用STL提供的容器std::vector來存放我們的線程,這些線程做的工作都是一致的县匠,他們將在隊列中不斷地彈出任務(wù)并執(zhí)行它风科,如果隊列中沒有任務(wù)了撒轮,他們就將進入等待狀態(tài)。
結(jié)合上面的隊列實現(xiàn)贼穆,給出一個簡單的線程池實現(xiàn):

namespace multi_thread {
    template<typename Task>
    class thread_safe_queue {
    public:
        thread_safe_queue() :done_(false) {

        }

        void push(const Task& t) {
            std::lock_guard<std::mutex> lock(mtx_);
            queue_.push(t);
            ready_.notify_one();
        }

        void wait_and_pop(Task& t) {
            std::unique_lock<std::mutex> lock(mtx_);
            if (queue_.empty() && !done_)
                ready_.wait(lock);
            if (done_)
                return;
            t = queue_.front();
            queue_.pop();
        }

        bool empty()const {
            std::lock_guard<std::mutex> lock(mtx_);
            return queue_.empty();
        }

        void clear() {
            std::lock_guard<std::mutex> lock(mtx_);
            for (int i = 0; i < queue_.size(); ++i)
                queue_.pop();
        }

        void done() {
            done_ = true;
            ready_.notify_all();
        }
    private:
        std::queue<Task> queue_;
        mutable std::mutex mtx_;
        std::condition_variable ready_;
        std::atomic_bool done_;
    };

    class thread_pool {
    public:
        thread_pool(const int threadNum) :done_(false) {
            for (int i = 0; i < threadNum; ++i) {
                threads_.emplace_back(&thread_pool::workerThread, this);
            }
        }
        ~thread_pool() {
            done_.store(true);
            queue_.done();
            for (auto& thread : threads_) {
                if (thread.joinable())
                    thread.join();
            }
        }
        void submit(const std::function<void(void)>& t) {
            queue_.push(t);
        }

        bool isEmpty()const {
            return queue_.empty();
        }

        void clearTask() {
            queue_.clear();
        }
    private:
        void workerThread() {
            while (true) {
                std::function<void(void)> t;
                queue_.wait_and_pop(t);
                if (done_)
                    break;
                if (t)
                    t();
            }
        }
    private:
        std::vector<std::thread> threads_;
        thread_safe_queue<std::function<void(void)>> queue_;
        std::atomic_bool done_;
    };
}

隊列里存放的是std::function<void(void)>题山,是一個沒有參數(shù)和返回值的Callable。在C++中故痊,常用的Callable包括lambda表達式顶瞳、function object(也就是operator())、std::bindstd::function愕秫。我個人比較喜歡使用lambda和std::bind慨菱,他們都很簡單易用,在使用std::bind時豫领,如果你的函數(shù)是一個member function抡柿,別忘了把this作為第一個參數(shù)。
這個線程池非常簡單等恐,當(dāng)你調(diào)用構(gòu)造函數(shù)時洲劣,傳入一個你想要的線程數(shù)量,然后構(gòu)造函數(shù)體里就會開啟對應(yīng)數(shù)量的線程课蔬。std::thread接受一個Callable和它的參數(shù)作為構(gòu)造函數(shù)的參數(shù)囱稽,構(gòu)造函數(shù)完成后線程立即啟動,這里的workerThread是一個member function二跋,所以战惊,我們還得加上this作為參數(shù),跟std::bind一樣扎即。
workerThread函數(shù)里面循環(huán)地從隊列中獲取任務(wù)并執(zhí)行吞获,如果沒有任務(wù),他就會處于等待狀態(tài)谚鄙「骺剑可以看到這里同樣有一個std::atomic_bool的變量done_,它用于使線程結(jié)束闷营。我們會在線程池的析構(gòu)函數(shù)里將這個變量設(shè)為true烤黍,然后我們調(diào)用隊列的done函數(shù),讓隊列退出傻盟。最后我們逐個檢查線程池中的線程速蕊,如果他們還在運行,等待他們結(jié)束娘赴。一個std::thread在析構(gòu)前必須指定等待該線程結(jié)束還是分離該線程规哲,否則會發(fā)生錯誤。如果我們不做這些工作诽表,那么線程池在析構(gòu)時要么是其中的線程一直處于等待狀態(tài)唉锌,要么是一直處于循環(huán)狀態(tài)腥光,這會導(dǎo)致析構(gòu)函數(shù)停滯不前,也就是程序的卡死糊秆。

4.總結(jié)

這次實現(xiàn)的線程池只能說是實現(xiàn)了最基礎(chǔ)的功能武福,還缺少一些功能,例如任務(wù)返回值的缺失等痘番。本文實現(xiàn)的線程安全隊列也十分簡單捉片,還有很多可以優(yōu)化的地方。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末汞舱,一起剝皮案震驚了整個濱河市伍纫,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌昂芜,老刑警劉巖莹规,帶你破解...
    沈念sama閱讀 206,311評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異泌神,居然都是意外死亡良漱,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評論 2 382
  • 文/潘曉璐 我一進店門欢际,熙熙樓的掌柜王于貴愁眉苦臉地迎上來母市,“玉大人,你說我怎么就攤上這事损趋』季茫” “怎么了?”我有些...
    開封第一講書人閱讀 152,671評論 0 342
  • 文/不壞的土叔 我叫張陵浑槽,是天一觀的道長蒋失。 經(jīng)常有香客問我,道長桐玻,這世上最難降的妖魔是什么篙挽? 我笑而不...
    開封第一講書人閱讀 55,252評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮畸冲,結(jié)果婚禮上嫉髓,老公的妹妹穿的比我還像新娘观腊。我一直安慰自己邑闲,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 64,253評論 5 371
  • 文/花漫 我一把揭開白布梧油。 她就那樣靜靜地躺著苫耸,像睡著了一般。 火紅的嫁衣襯著肌膚如雪儡陨。 梳的紋絲不亂的頭發(fā)上褪子,一...
    開封第一講書人閱讀 49,031評論 1 285
  • 那天量淌,我揣著相機與錄音,去河邊找鬼嫌褪。 笑死呀枢,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的笼痛。 我是一名探鬼主播裙秋,決...
    沈念sama閱讀 38,340評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼缨伊!你這毒婦竟也來了摘刑?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,973評論 0 259
  • 序言:老撾萬榮一對情侶失蹤刻坊,失蹤者是張志新(化名)和其女友劉穎枷恕,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體谭胚,經(jīng)...
    沈念sama閱讀 43,466評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡徐块,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,937評論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了灾而。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片蛹锰。...
    茶點故事閱讀 38,039評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖绰疤,靈堂內(nèi)的尸體忽然破棺而出铜犬,到底是詐尸還是另有隱情,我是刑警寧澤轻庆,帶...
    沈念sama閱讀 33,701評論 4 323
  • 正文 年R本政府宣布癣猾,位于F島的核電站,受9級特大地震影響余爆,放射性物質(zhì)發(fā)生泄漏纷宇。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,254評論 3 307
  • 文/蒙蒙 一蛾方、第九天 我趴在偏房一處隱蔽的房頂上張望像捶。 院中可真熱鬧,春花似錦桩砰、人聲如沸拓春。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽硼莽。三九已至,卻和暖如春煮纵,著一層夾襖步出監(jiān)牢的瞬間懂鸵,已是汗流浹背偏螺。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留匆光,地道東北人套像。 一個月前我還...
    沈念sama閱讀 45,497評論 2 354
  • 正文 我出身青樓,卻偏偏與公主長得像终息,于是被迫代替她去往敵國和親凉夯。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,786評論 2 345

推薦閱讀更多精彩內(nèi)容