記事ディレクトリ
1 スレッドプールの概念
スレッドの使用パターン。スレッドが多すぎると、スケジューリングのオーバーヘッドが生じ、キャッシュの局所性と全体的なパフォーマンスに影響します。スレッド プールは複数のスレッドを維持し、スーパーバイザが同時に実行できるタスクを割り当てるのを待ちます。これにより、存続期間の短いタスクを処理するときにスレッドを作成および破棄するコストが回避されます。スレッド プールは、コアを完全に利用することを保証するだけでなく、オーバースケジューリングも防ぎます。使用可能なスレッドの数は、同時に使用できるプロセッサ、プロセッサ コア、メモリ、ネットワーク ソケットなどの数によって異なります。
スレッド プールのアプリケーション シナリオ:
- 1️⃣タスクを完了するには多数のスレッドが必要ですが、タスクの完了までの時間は比較的短くなります。WEB サーバーが Web ページ要求などのタスクを完了する場合、スレッド プール テクノロジを使用するのが非常に適切です。1 つのタスクは小さく、タスクの数は膨大であるため、人気のある Web サイトのクリック数を想像することができます。ただし、Telnet 接続リクエストなどの長期にわたるタスクの場合、スレッド プールの利点は明らかではありません。Telnet セッション時間はスレッド作成時間よりもはるかに長いためです。
- 2️⃣サーバーが顧客の要求に迅速に応答する必要があるなど、厳しいパフォーマンス要件を持つアプリケーション。
- 3️⃣突然の大量のリクエストを受け入れますが、サーバーが大量のスレッドを生成しないようにします。突然大量の顧客リクエストが発生すると、スレッド プールがないと大量のスレッドが生成されます。ほとんどのオペレーティング システムでは、理論的にはスレッドの最大数は問題ありませんが、短期間に大量のスレッドを生成すると、メモリが限界に達し、エラーが発生する可能性があります。
スレッドプールの例:
-
- 一定数のスレッドプールを作成し、ループ内でタスクキューからタスクオブジェクトを取得します。
-
- タスクオブジェクトを取得したら、タスクオブジェクト内のタスクインターフェースを実行します。
2 スレッドプールの最初のバージョン
スレッド プールを作成する前に、スレッド プールのメンバー変数が何であるべきかを考えてみましょう。まず第一に、スレッドを保存するコンテナが必要なので、それを使用することもできますvector
。また、整数変数を使用してスレッド プール内のスレッド数を記録する必要があります。スレッドの安全性を確保するには、同期関係を維持するには、条件変数も必要です (ここでの同期関係とは、スレッド ライブラリ内のスレッドがタスクがない場合はスリープし、タスクがある場合はタスクを実行することを意味します)。さらに、タスクキューも必要です。ここでは、後の検証時に効果がより明確になるようにタスク クラスをカプセル化します。
タスク.hpp:
#pragma once
#include <iostream>
using namespace std;
class Task
{
public:
Task(int x=0, int y=0, char op='+')
: _x(x), _y(y), _op(op)
{
}
void run()
{
switch (_op)
{
case '+':
_res = _x + _y;
break;
case '-':
_res = _x - _y;
break;
case '*':
_res = _x * _y;
break;
case '/':
if(_y==0)
{
_exitCode=1;
return;
}
_res = _x / _y;
break;
case '%':
_res = _x % _y;
break;
}
}
void formatMsk()
{
cout<<"mask:"<<_x<<_op<<_y<<"==?"<<endl;
}
void formatRes()
{
cout<<"res:"<<_x<<_op<<_y<<"=="<<_res<<endl;
}
private:
int _x;
int _y;
char _op;
int _res = 0;
int _exitCode = 0;
};
それでは、最初のバージョンを実装してみましょう。
#pragma once
#include<iostream>
#include<vector>
#include<queue>
using namespace std;
const int N=5;
template<class T>
class threadPool
{
public:
threadPool(int sz=N)
:_sz(sz)
,_threads(sz)
{
pthread_mutex_init(&_mutex,nullptr);
pthread_cond_init(&_cond,nullptr);
}
static void* Routine(void* args)//用内存池的多线程执行任务
{
pthread_detach(pthread_self());//先让自己与主线程分离
threadPool<T> *ptp=static_cast<threadPool<T> *>(args);
while(true)
{
pthread_mutex_lock(&(ptp->_mutex));
while((ptp->_masks).empty())
{
pthread_cond_wait(&(ptp->_cond),&(ptp->_mutex));
}
T task=(ptp->_masks).front();
(ptp->_masks).pop();
pthread_mutex_unlock(&(ptp->_mutex));
task.run();//在临界区外执行任务
task.formatRes();
}
return nullptr;
}
void Start()
{
for(int i=0;i<_sz;++i)
{
pthread_create(&_threads[i],nullptr,Routine,this);
}
}
void PushTask(const T& task)
{
pthread_mutex_lock(&_mutex);
_masks.push(task);
pthread_mutex_unlock(&_mutex);
pthread_cond_signal(&_cond);//记得唤醒休眠的线程去执行任务
}
~threadPool()
{
pthread_mutex_destroy(&_mutex);
pthread_cond_destroy(&_cond);
}
vector<pthread_t> _threads;
queue<T> _masks;
int _sz;//线程池中线程的个数
pthread_mutex_t _mutex;
pthread_cond_t _cond;
};
ここでは特に注意が必要な点がいくつかあります。
- スレッドの作成に必要な関数ポインターがクラス内のメンバー関数と一致せず、クラス内のメンバー関数がポインターを持っているため、静的バージョンのルーチンを実装します
this
。 - スレッドを作成した後、スレッドをメイン スレッドから分離します。つまり、メイン スレッドは新しいスレッドのリソースのリサイクルを考慮しません。
- タスクを実行するときは、同時実行の効率を高めるために、クリティカル セクションの外でタスクを実行する必要があります。
- 使用の便宜のため、クラス内のすべてのメンバー変数は公開されていますが、これを行わないことをお勧めします。get は自分で作成できます。
その他の点については非常にシンプルなので誰でも簡単に理解できると思います。
テストプログラム:
const char *ops = "+-*/%";
int main()
{
threadPool<Task> *threads = new threadPool<Task>(30);
threads->Start();
srand((size_t)time(nullptr));
while (true)
{
int x = rand() % 30 + 1;
int y = rand() % 30 + 1;
char op = ops[rand() % strlen(ops)];
Task t(x, y, op);
threads->PushTask(t);
t.formatMsk();
sleep(1);
}
return 0;
}
結果を実行してみましょう:
3 スレッド プールの 2 番目のバージョン
実際、スレッド プールの 2 番目のバージョンの中心的な考え方は、基本的に最初のものと同じです。主な理由は、2 番目のバージョンでは、自分たちでシミュレートして実装したスレッド作成クラスを使用しているためです。以前に自分たちで実装しました (基本的にスレッド プールをライブラリにカプセル化します)。スレッド ライブラリ インターフェイスの Thread.hpp のコピー):
#pragma once
#include <iostream>
#include <functional>
using namespace std;
class threadProcess
{
public:
enum stu
{
NEW,
RUNNING,
EXIT
};
template<class T>
threadProcess(int num, T exe, void *args)
: _tid(0)
, _status(NEW)
,_exe(exe)
, _args(args)
{
char name[26];
snprintf(name, 26, "thread%d", num);
_name = name;
}
static void* runHelper(void *args)
{
threadProcess *ts = (threadProcess *)args;
(*ts)();
return nullptr;
}
void operator()() // 仿函数
{
if (_exe != nullptr)
_exe(_args);
}
void Run()
{
int n = pthread_create(&_tid, nullptr, runHelper, this);
if (n != 0)
exit(-1);
_status = RUNNING;
}
void Join()
{
int n = pthread_join(_tid, nullptr);
if (n != 0)
exit(-1);
_status = EXIT;
}
string _name;
pthread_t _tid;
stu _status;
function<void*(void*)> _exe;
void *_args;
};
このようにして、独自のスレッド ライブラリを使用して自分で実行できます。
#pragma once
#include"Thread.hpp"
#include<iostream>
#include<vector>
#include<queue>
using namespace std;
const int N=5;
template<class T>
class threadPool
{
public:
threadPool(int sz=N)
:_sz(sz)
{
pthread_mutex_init(&_mutex,nullptr);
pthread_cond_init(&_cond,nullptr);
}
static void* Routine(void* args)//用内存池的多线程执行任务
{
//pthread_detach(pthread_self());调用自己写的线程接口不用在分离了,析构时在join掉就好了
threadPool<T> *ptp=static_cast<threadPool<T> *>(args);
while(true)
{
pthread_mutex_lock(&(ptp->_mutex));
while((ptp->_masks).empty())
{
pthread_cond_wait(&(ptp->_cond),&(ptp->_mutex));
}
T task=(ptp->_masks).front();
(ptp->_masks).pop();
pthread_mutex_unlock(&(ptp->_mutex));
task.run();//在临界区外执行任务
task.formatRes();
}
return nullptr;
}
void Init()
{
for(int i=0;i<_sz;++i)
{
_threads.push_back(threadProcess(i+1,Routine,this));
}
}
void Start()
{
for(auto& e:_threads)
{
e.Run();
}
}
void PushTask(const T& task)
{
pthread_mutex_lock(&_mutex);
_masks.push(task);
pthread_mutex_unlock(&_mutex);
pthread_cond_signal(&_cond);//记得唤醒休眠的线程去执行任务
}
~threadPool()
{
for(auto& e:_threads)
{
e.Join();
}
pthread_mutex_destroy(&_mutex);
pthread_cond_destroy(&_cond);
}
void Check()
{
for(auto& e:_threads)
{
cout<<"name:"<<e._name<<" id"<<e._tid<<endl;
}
}
vector<threadProcess> _threads;
queue<T> _masks;
int _sz;//线程池中线程的个数
pthread_mutex_t _mutex;
pthread_cond_t _cond;
};
テストコード:
int main()
{
threadPool<Task> *threads = new threadPool<Task>(8);
threads->Init();
threads->Start();
srand((size_t)time(nullptr));
while (true)
{
int x = rand() % 30 + 1;
int y = rand() % 30 + 1;
char op = ops[rand() % strlen(ops)];
Task t(x, y, op);
threads->PushTask(t);
t.formatMsk();
sleep(1);
}
return 0;
}
操作結果:
4 スレッド プールの 3 番目のバージョン
このバージョンのスレッド プールでは、シングルトンパターン。必要なスレッド プールは 1 つだけであることがわかったので、それを使用して懒汉模式
シングルトンを作成します。
コード:
#pragma once
#include "Thread.hpp"
#include <iostream>
#include <vector>
#include <queue>
using namespace std;
const int N = 5;
template <class T>
class threadPool
{
public:
static threadPool<T>* GetInstance(int sz=N)
{
if(_sta_obj==nullptr)
{
pthread_mutex_lock(&_mutex);
if(_sta_obj==nullptr)
{
_sta_obj=new threadPool<T>(sz);
}
pthread_mutex_unlock(&_mutex);
}
}
static void *Routine(void *args) // 用内存池的多线程执行任务
{
// pthread_detach(pthread_self());调用自己写的线程接口不用在分离了,析构时在join掉就好了
threadPool<T> *ptp = static_cast<threadPool<T> *>(args);
while (true)
{
pthread_mutex_lock(&(ptp->_mutex));
while ((ptp->_masks).empty())
{
pthread_cond_wait(&(ptp->_cond), &(ptp->_mutex));
}
T task = (ptp->_masks).front();
(ptp->_masks).pop();
pthread_mutex_unlock(&(ptp->_mutex));
task.run(); // 在临界区外执行任务
task.formatRes();
}
return nullptr;
}
void Init()
{
for (int i = 0; i < _sz; ++i)
{
_threads.push_back(threadProcess(i + 1, Routine, this));
}
}
void Start()
{
for (auto &e : _threads)
{
e.Run();
}
}
void PushTask(const T &task)
{
pthread_mutex_lock(&_mutex);
_masks.push(task);
pthread_mutex_unlock(&_mutex);
pthread_cond_signal(&_cond); // 记得唤醒休眠的线程去执行任务
}
~threadPool()
{
for (auto &e : _threads)
{
e.Join();
}
pthread_mutex_destroy(&_mutex);
pthread_cond_destroy(&_cond);
}
void Check()
{
for (auto &e : _threads)
{
cout << "name:" << e._name << " id" << e._tid << endl;
}
}
vector<threadProcess> _threads;
queue<T> _masks;
int _sz; // 线程池中线程的个数
pthread_mutex_t _mutex;
pthread_cond_t _cond;
private:
threadPool(int sz = N)
: _sz(sz)
{
pthread_mutex_init(&_mutex, nullptr);
pthread_cond_init(&_cond, nullptr);
}
threadPool(const threadPool<T>& th)=delete;
threadPool<T>& operator=(const threadPool<T>& th)=delete;
static threadPool<T>* _sta_obj;
};
template<class T>
threadPool<T>* threadPool<T>::_sta_obj=nullptr;
注意点:
ロック時の効率化のために使用しています双重if条件判断
。
コンストラクターはプライベートになり、コピー構築とコピー代入は削除されることに注意してください。
5 STL のコンテナとスマート ポインタのスレッド セーフティの問題
STL のコンテナはスレッドセーフですか?
いいえ。その理由は、STL の本来の目的はパフォーマンスを最大化することですが、スレッドの安全性を確保するためにロックが必要になると、パフォーマンスに大きな影響を与えることになります。また、コンテナーごとに、ロックの方法によってパフォーマンスが異なる場合があります (たとえば、ハッシュ テーブル、ロック テーブル、ロック バケットなど)。したがって、STL はデフォルトではスレッド セーフではありません。マルチスレッド環境で使用する必要がある場合、呼び出し元は多くの場合、スレッド セーフを確保する必要があります。
スマート ポインターはスレッドセーフですか?
unique_ptr の場合、現在のコード ブロックのスコープ内でのみ有効であるため、スレッド セーフティの問題は発生しません。
shared_ptr の場合、複数のオブジェクトが参照カウント変数を共有する必要があるため、スレッド セーフティの問題が発生します。ただし、標準ライブラリでは実装時にこの問題が考慮され、アトミック オペレーション (CAS) メソッドに基づいて、shared_ptr が確実に実行できるようにしました。参照カウントを効率的かつアトミックに操作します。
その他の 6 つの一般的なロック
- 悲観的ロック: データをフェッチするたびに、データが他のスレッドによって変更されるのではないかと常に心配するため、データをフェッチする前にデータをロックします (読み取りロック、書き込みロック、行ロックなど)。データにアクセスするにはブロックされ、ハングします。
- オプティミスティック ロック: データがフェッチされるたびに、データは他のスレッドによって変更されないことが常にオプティミスティックになるため、データはロックされません。ただし、データを更新する前に、他のデータによって更新前のデータが変更されているかどうかが判断されます。バージョン番号メカニズムと CAS 操作という 2 つの主な方法があります。CAS 操作: データを更新する必要がある場合、現在のメモリ値が以前に取得した値と等しいかどうかを判断します。等しい場合は、新しい値で更新します。待機しない場合は失敗し、失敗した場合は再試行され、通常は回転プロセス、つまり常に再試行されます。
- スピンロック、フェアロック、アンフェアロック。
7 リーダーライターの問題 (理解)
読み取り/書き込みロック:
マルチスレッドの書き込み時に、非常に一般的な状況が発生します。つまり、一部の公開データは変更される可能性が低くなります。書き換えに比べて、読み取られる可能性がはるかに高くなります。一般に、読み取り処理には検索処理が伴うことが多く、時間がかかります。この種のコード セグメントをロックすると、プログラムの効率が大幅に低下します。では、読み取りが増えて書き込みが減少するこの状況に具体的に対処する方法はあるのでしょうか? はい、それは読み取り/書き込みロックです。
読み取り/書き込みロックの動作:
現在のロック状態 | 読み取りロック要求 | 書き込みロック要求 |
---|---|---|
ロックなし | できる | できる |
読み取りロック | できる | ブロック |
書き込みロック | ブロック | ブロック |
- 注: 書き込み排他、読み取り共有、読み取りロックの優先順位は高くなります。