目錄
一、線程池
1、線程池
2、線程池代碼
3、線程池的應用場景
二、單例模式的線程安全問題
1、線程池的單例模式
2、線程安全問題
三、其他鎖
一、線程池
1、線程池
線程池是一種線程使用模式。線程池里面可以維護一些線程。
為什么要有線程池?
因為在我們使用線程去處理各種任務的時候,尤其是一些執(zhí)行時間短的任務,我們必須要先對線程進行創(chuàng)建然后再進行任務處理,最后再銷毀線程,效率是比較低的。而且有的時候線程過多會帶來調(diào)度開銷,進而影響緩存局部性和整體性能。
于是,我們可以通過線程池預先創(chuàng)建出一批線程,線程池維護著這些線程,線程等待著監(jiān)督管理者分配可并發(fā)執(zhí)行的任務。這避免了在處理短時間任務時創(chuàng)建與銷毀線程的代價。
線程池不僅能夠保證內(nèi)核的充分利用,還能防止過分調(diào)度。
2、線程池代碼
我們先對線程進行封裝:Thread.hpp
#pragma once
#include <iostream>
#include <string>
#include <cstdio>
#include <pthread.h>
using namespace std;
typedef void *(*fun_t)(void *);
class ThreadData
{
public:
void *arg_;
string name_;
};
class Thread
{
public:
Thread(int num, fun_t callback, void *arg)
: func_(callback)
{
char buffer[64];
snprintf(buffer, sizeof(buffer), "Thread-%d", num);
name_ = buffer;
tdata_.name_ = name_;
tdata_.arg_ = arg;
}
void start()
{
pthread_create(&tid_, nullptr, func_, (void *)&tdata_);
}
void join()
{
pthread_join(tid_, nullptr);
}
string &name()
{
return name_;
}
~Thread()
{
}
private:
pthread_t tid_;
string name_;
fun_t func_;
ThreadData tdata_;
};
線程池代碼:threadPool.hpp:
#pragma once
#include <vector>
#include <queue>
#include "thread.hpp"
#define THREAD_NUM 3
template <class T>
class ThreadPool
{
public:
bool Empty()
{
return task_queue_.empty();
}
pthread_mutex_t *getmutex()
{
return &lock;
}
void wait()
{
pthread_cond_wait(&cond, &lock);
}
T gettask()
{
T t = task_queue_.front();
task_queue_.pop();
return t;
}
public:
ThreadPool(int num = THREAD_NUM) : num_(num)
{
for (int i = 0; i < num_; i++)
{
threads_.push_back(new Thread(i, routine, this));
}
pthread_mutex_init(&lock, nullptr);
pthread_cond_init(&cond, nullptr);
}
static void *routine(void *arg)
{
ThreadData *td = (ThreadData *)arg;
ThreadPool<T> *tp = (ThreadPool<T> *)td->arg_;
while (true)
{
T task;
{
pthread_mutex_lock(tp->getmutex());
while (tp->Empty())
tp->wait();
task = tp->gettask();
pthread_mutex_unlock(tp->getmutex());
}
cout << "x+y=" << task() << " " << pthread_self() << endl;
}
}
void run()
{
for (auto &iter : threads_)
{
iter->start();
}
}
void PushTask(const T &task)
{
pthread_mutex_lock(&lock);
task_queue_.push(task);
pthread_mutex_unlock(&lock);
pthread_cond_signal(&cond);
}
~ThreadPool()
{
for (auto &iter : threads_)
{
iter->join();
delete iter;
}
pthread_mutex_destroy(&lock);
pthread_cond_destroy(&cond);
}
private:
vector<Thread *> threads_;
int num_;
queue<T> task_queue_;
pthread_mutex_t lock;
pthread_cond_t cond;
};
任務:task.hpp:
#pragma once
#include <iostream>
#include <queue>
#include <pthread.h>
#include <unistd.h>
class task
{
public:
task()
{
}
task(int x, int y)
: x_(x), y_(y)
{
}
int operator()()
{
return x_ + y_;
}
private:
int x_;
int y_;
};
?測試代碼:test.cc:
#include "threadPool.hpp"
#include "task.hpp"
#include <iostream>
#include <ctime>
int main()
{
srand((unsigned int)time(nullptr) ^ getpid() ^ 12232);
ThreadPool<task> *tp = new ThreadPool<task>();
tp->run();
while (true)
{
int x = rand() % 100 + 1;
sleep(1);
int y = rand() % 100 + 1;
task t(x, y);
tp->PushTask(t);
cout << x << "+" << y << "=?" << endl;
}
return 0;
}
運行結(jié)果:?
3、線程池的應用場景
1、需要大量的線程來完成任務,且完成任務的時間比較短。?
2、對性能要求苛刻的應用,比如要求服務器迅速響應客戶請求。
3、接受突發(fā)性的大量請求,但不至于使服務器因此產(chǎn)生大量線程的應用。突發(fā)性大量客戶請求,在沒有線程池情況下,將產(chǎn)生大量線程,雖然理論上大部分操作系統(tǒng)線程數(shù)目最大值不是問題,短時間內(nèi)產(chǎn)生大量線程可能使內(nèi)存到達極限,出現(xiàn)錯誤。
二、單例模式的線程安全問題
1、線程池的單例模式
首先,我們要做的第一件事就是把構(gòu)造函數(shù)私有,再把拷貝構(gòu)造和賦值運算符重載函數(shù)delete:
private:
ThreadPool(int num = THREAD_NUM) : num_(num)
{
for (int i = 0; i < num_; i++)
{
threads_.push_back(new Thread(i, routine, this));
}
pthread_mutex_init(&lock, nullptr);
pthread_cond_init(&cond, nullptr);
}
ThreadPool(const TreadPool &other) = delete;
ThreadPool operator=(const TreadPool &other) = delete;
接下來就要在類中定義一個成員變量:靜態(tài)指針,方便獲取單例對象,并在類外初始化:
//線程池中的成員變量
private:
vector<Thread *> threads_;
int num_;
queue<T> task_queue_;
pthread_mutex_t lock;
pthread_cond_t cond;
static ThreadPool<T> *tp;
//在類外初始化
?template <class T>
ThreadPool<T> *ThreadPool<T>::tp = nullptr;
最后我們寫一個函數(shù)可以獲取單例對象,在設(shè)置獲取單例對象的函數(shù)的時候,注意要設(shè)置成靜態(tài)成員函數(shù),因為在獲取對象前根本沒有對象,無法調(diào)用非靜態(tài)成員函數(shù)(無this指針):?
static ThreadPool<T> *getThreadPool()
{
if (tp == nullptr)
{
tp = new ThreadPool<T>();
}
return tp;
}
2、線程安全問題
上面的線程池的單例模式,看起來沒有什么問題。可是當我們有多個線程去調(diào)用 getThreadPool函數(shù),去創(chuàng)建線程池的時候,可能會有多個線程同時進入判斷,判斷出線程池指針為空,然后創(chuàng)建線程池對象。這樣就會創(chuàng)建出多個線程池對象,這就不符合我們單例模式的要求了,所以我們必須讓在同一時刻只有一個線程能夠進入判斷,我們就要用到鎖了。
定義一個靜態(tài)鎖,并初始化:
private:
vector<Thread *> threads_;
int num_;
queue<T> task_queue_;
pthread_mutex_t lock;
pthread_cond_t cond;
static ThreadPool<T> *tp;
static pthread_mutex_t lock;
// 類外初始化
?template <class T>
pthread_mutex_t ThreadPool<T>::lock = PTHREAD_MUTEX_INITIALIZER;
對 getThreadPool函數(shù)進行加鎖:
static ThreadPool<T> *getThreadPool()
{
if (tp == nullptr)
{
pthread_mutex_lock(&lock);
if (tp == nullptr)
{
tp = new ThreadPool<T>();
}
pthread_mutex_unlock(&lock);
}
return tp;
}
對于上面的代碼:我們?yōu)槭裁匆讷@取鎖之前還要再加一個判斷指針為空的條件呢?
當已經(jīng)有一個線程創(chuàng)建出來了線程池的單例模式后,在這之后的所有其他線程即使申請到鎖,緊著著下一步就是去釋放鎖,它不會進入第二個 if 條件里面。其實這樣是效率低下的,因為線程會頻繁申請鎖,然后就釋放鎖。所以我們在最外層再加一個if判斷,就可以阻止后來的線程不用去申請鎖創(chuàng)建線程池了,直接返回已經(jīng)創(chuàng)建出來的線程池。
三、其他鎖
1、悲觀鎖:在每次取數(shù)據(jù)時,總是擔心數(shù)據(jù)會被其他線程修改,所以會在取數(shù)據(jù)前先加鎖(讀鎖,寫鎖,行鎖等),當其他線程想要訪問數(shù)據(jù)時,被阻塞掛起。
2、樂觀鎖:每次取數(shù)據(jù)時候,總是樂觀的認為數(shù)據(jù)不會被其他線程修改,因此不上鎖。但是在更新數(shù)據(jù)前,會判斷其他數(shù)據(jù)在更新前有沒有對數(shù)據(jù)進行修改。主要采用兩種方式:版本號機制和CAS操作。
~ CAS操作:當需要更新數(shù)據(jù)時,判斷當前內(nèi)存值和之前取得的值是否相等。如果相等則用新值更新。若不等則失敗,失敗則重試,一般是一個自旋的過程,即不斷重試。
3、自旋鎖:說到自旋鎖,我們不得不說一說我們之前所用到的鎖,我們之前所用的鎖都是互斥鎖,當線程沒有競爭到互斥鎖時,它會阻塞等待,只有等鎖被釋放了后,才能去重新申請鎖。而對于自旋鎖,當線程沒有競爭到自旋鎖的時候,線程會不斷地循環(huán)檢測去申請自旋鎖,直到拿到鎖。文章來源:http://www.zghlxwxcb.cn/news/detail-855008.html
一般來說,如果臨界區(qū)的代碼執(zhí)行時間比較長的話,我們是使用互斥鎖而不是自旋鎖的,這樣線程不會因為頻繁地檢測去申請鎖而占用CPU資源。如果臨界區(qū)的代碼執(zhí)行時間較短的話,我們一般就最好使用自旋鎖,而不是互斥鎖,因為互斥鎖申請失敗,是要阻塞等待,是需要發(fā)生上下文切換的,如果臨界區(qū)執(zhí)行的時間比較短,那可能上下文切換的時間會比臨界區(qū)代碼執(zhí)行的時間還要長。文章來源地址http://www.zghlxwxcb.cn/news/detail-855008.html
到了這里,關(guān)于Linux之 線程池 | 單例模式的線程安全問題 | 其他鎖的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!