記事ディレクトリ
1. 生産者・消費者モデル
名前が示すように、サプライヤー、スーパーマーケット、顧客のようなものです。顧客のニーズはさまざまです。サプライヤーに直接買いに行くことはできません。サプライヤーには独自の規制があります。消費者のニーズに応えるために、サプライヤーはスーパーマーケットに商品を供給し、スーパーマーケットはそれを販売します。買いにスーパーへ。サプライヤーは生産者であり、顧客は消費者です。スーパーマーケットの存在により、生産者と消費者のペースは一致しない可能性があります。サプライヤーが商品を供給しなくても、スーパーマーケットには顧客に販売するための在庫がまだあります。
コンピュータに対応して、プロデューサーとコンシューマーの両方がスレッドであり、スーパーマーケットは特定のバッファーであり、バッファーは複数の構造を持つことができます。プロデューサは有用なデータを持っており、それはバッファを通じてコンシューマに提供されます。これは以前の通信によく似ていますが、通信ではありません。このモデルの前提は、バッファーが最初にすべてのスレッドによって認識される必要があるということです。つまり、このバッファーは複数のスレッドによって同時にアクセスされるパブリック領域であるため、マルチスレッドは共有リソースのセキュリティを保護し、スレッドを維持する必要があります。相互作用: 反発と同期の関係。それを維持するにはどうすればよいですか?
生産者-消費者モデルには、生産者-生産者、消費者-消費者、生産者-消費者の 3 つの関係が必要です。学生と学生の間には相互排他的な関係があり、あるプロデューサーがスペースにデータを挿入すると、他のプロデューサーはこのスペースにデータを挿入できません。生産者と消費者、顧客はスーパーに行き、欲しい商品はないか尋ねますが、なければ、しばらく待って再度尋ねるしかありませんが、スーパーがいつ入荷するかを顧客に伝えれば、顧客は同様に、スーパーマーケットがいつ、どの商品が在庫切れになるかをサプライヤーに通知しない場合、サプライヤーは供給するかどうかを決定できません。効率的に実行するには、モデル全体を同期する必要があります。いいえ、存在するのは同期関係のみです。顧客とサプライヤーは、商品の購入と供給時に異なるタイミングで同期を行う必要があります。これは相互に排他的な関係です。消費者と消費者の間には、二人の顧客が同じ商品を購入したい場合、競合が発生するため、相互排他的な関係になります。
モデルにはプロデューサーとコンシューマーという 2 つの役割があり、トランザクションの場所 (通常はバッファ) があります。
1. BlockingQueue に基づくプロデューサー コンシューマー モデル
BlockingQueue はブロッキング キューです。キューがいっぱいの場合、データを書き込むスレッドはデータを書き込むことができず、キューが空の場合は、前のパイプラインと同様に、データを受け取るスレッドはデータを受け取ることができません。BlockQueue.hpp ファイルを作成し、main.cc ファイルにはこの hpp ファイル (メイクファイル) が含まれます。
最初にフレームワークを構築する
//BlockQueue.hpp
#pragma once
#include <iostream>
#include <pthread.h>
template <class T>
class BlockQueue
{
};
//main.cc
#include "BlockQueue.hpp"
void* consumer(void* args)
{
}
void* productor(void* args)
{
}
int main()
{
//单生产和单消费
pthread_t c, p;
pthread_create(&c, nullptr, consumer, nullptr);
pthread_create(&p, nullptr, productor, nullptr);
pthread_join(c, nullptr);
pthread_join(p, nullptr);
return 0;
}
これらは 2 つの別個のスレッドですが、両方のスレッドが同じバッファを参照できるようにするにはどうすればよいでしょうか? BlockQueue クラスは hpp ファイルで定義されており、最初にそのテンプレート パラメーターを int にします。
void* consumer(void* args)
{
BlockQueue<int> *bq = static_cast<BlockQueue<int>*>(args);
}
void* productor(void* args)
{
BlockQueue<int> *bq = static_cast<BlockQueue<int>*>(args);
}
int main()
{
BlockQueue<int> *bq = new BlockQueue<int>();
//单生产和单消费
pthread_t c, p;
pthread_create(&c, nullptr, consumer, bq);
pthread_create(&p, nullptr, productor, bq);
pthread_join(c, nullptr);
pthread_join(p, nullptr);
return 0;
}
データに関しては、現実にはさまざまなデータが存在しますが、ここでは単純な乱数をデータとして偽造します。
void* consumer(void* args)
{
sleep(1);
BlockQueue<int> *bq = static_cast<BlockQueue<int>*>(args);
while(true)
{
int data = 0;
//1、从阻塞队列中获取数据
bq->pop(&data);
//2、结合某种业务逻辑,处理数据
cout << "consumer data" << data << endl;
}
}
void* productor(void* args)
{
BlockQueue<int> *bq = static_cast<BlockQueue<int>*>(args);
while(true)
{
sleep(1);
//1、先通过某种渠道获取数据
int data = rand() % 10 + 1;//也就是生成1-10
//2、将数据推送到阻塞队列 —— 生产过程
bq->push(data);
}
}
int main()
{
srand((uint64_t)time(nullptr) ^ getpipd());
BlockQueue<int> *bq = new BlockQueue<int>();
//单生产和单消费
pthread_t c, p;
pthread_create(&c, nullptr, consumer, bq);
pthread_create(&p, nullptr, productor, bq);
pthread_join(c, nullptr);
pthread_join(p, nullptr);
return 0;
}
次に、ブロッキング キュー クラスのフレームワークの作成を開始します。
const int gcap = 5;
template <class T>
class BlockQueue
{
public:
BlockQueue(const int cap = gcap):_cap(gcap)
{
pthread_mutex_init(&_mutex, nullptr);
pthread_cond_init(&_consumerCond, nullptr);
pthread_cond_init(&_productorCond, nullptr);
}
void push(const T &in)
{
}
void pop(T* out)
{
}
~BlockQueue()
{
pthread_mutex_destroy(&_mutex);
pthread_cond_destroy(&_consumerCond);
pthread_cond_destroy(&_productorCond);
}
private:
queue<T> _q;
int _cap;//容量上限
pthread_mutex_t _mutex;
pthread_cond_t _consumerCond;//如果缓冲区为空,消费者等待,这个变量就是是否为空的条件变量
pthread_cond_t _productorCond;//如果缓冲区为满,生产者等待,这个变量就是是否为满的条件变量
};
プッシュしてポップする
bool isFull() {
return _q.size() == _cap; }
bool isEmpty() {
return _q.empty(); }
void push(const T &in)
{
pthread_mutex_lock(&_mutex);
if(isFull())//1、只能在临界区内部判断临界资源是否就绪,注定了当前一定持有锁
{
//2、要让线程进行休眠等待,就不能持有锁。锁只有一个,锁如果放在了阻塞线程中,那么谁也申请不了锁,就死锁了
//3、wait接口就得需要传锁,来释放锁
pthread_cond_wait(&_productorCond, &_mutex);//休眠结束后,应当从哪里继续执行?
//4、从系统角度看,休眠就是把线程给切走了,当线程醒来时,应当从临界区内部继续执行,因为线程是在临界区被切走的
//5、被唤醒时,wait函数需要重新申请锁,申请成功了才会返回,然后线程继续执行余下的代码
}
//走到这里,说明没满,就可以生产
_q.push(in);
//加策略,策略决定什么时候去唤醒,这里就不加了,直接唤醒
pthread_cond_signal(&_consumerCond);//生产者知道它自己放入了数据,所以缓冲区一定不为空,所以唤醒消费者
pthread_mutex_unlock(&_mutex);
}
void pop(T* out)
{
pthread_mutex_lock(&_mutex);
if(ifEmpty())
{
pthread_cond_wait(&_consumerCond, &_mutex);
}
*out = _q.front();
_q.pop();
//加策略,策略决定什么时候去唤醒,这里就不加了,直接唤醒
pthread_cond_signal(&_productorCond);//消费者知道它刚拿走一个数据,那么那个位置一定是空,就可以唤醒生产者生产
pthread_mutex_unlock(&_mutex);
}
この時点で、基本的な生産者/消費者モデルが作成されました。現在、このモデルの改良が続けられています。
誤ってスレッドを起動してしまう可能性はありますか? プッシュでは、複数のプロデューサーが wait からウェイクアップしてからデータをプッシュすると、元の残りのスペースではこれらのスレッドがプッシュするのに十分ではないため、データがオーバーフローする可能性があります。または、 wait の直後にプロデューサーがコンシューマーによってウェイクアップされる場合があります。たとえば、次のような場合です。コンシューマーによって指定されたポリシーと条件が満たされない場合、またはコンシューマーがブロードキャストを使用してすべてのプロデューサーをウェイクアップすると、プロデューサーは誤ってウェイクアップされるか、疑似的にウェイクアップされ、引き続き逆方向に実行されてプッシュされ、問題が発生します。起きます。
したがって、生産する前に必ず条件を満たしていることを確認する必要があります。そうすれば、push の if 判定が while に置き換えられ、誤って呼び起こされても、プロデューサーはループを続けて判定し、再度待機することができます。同様に、ポップでも while ループが使用されます。
2. モデルの包括的な理解
以前にも書きましたが、このモデルは効率的で不均一ですが、それがどこに反映されているのでしょうか?作っても消費できない、消費しても生産できない、この二つはシリアルなので効率をどう反映させるか?プロデューサーがデータをキューに入れ、コンシューマーがキューからデータを取得すると単純に考える必要はありません。プロデューサーはデータ ソースを持ち、コンシューマーはデータの処理メソッドを持っています。プロデューサーがデータを置くと、消費者がデータを処理して取得することを妨げるものではありません。コンシューマーがデータを取得しても、プロデューサーが他の場所からデータを取得することには影響しないため、プロデューサーとコンシューマーは並行して実行でき、そこに効率が生まれます。ブロッキング キューには、整数文字列などを入れるだけでなく、オブジェクトやタスクなどを入れることもできます。コードを修正してみましょう。
新しい task.hpp ファイルを作成します
#pragma once
#include <iostream>
#include <string>
class Task
{
public:
Task()
{
}
Task(int x, int y, char op):_x(x), _y(y), _op(op), _result(0), _exitCode(0)
{
}
void operator()()
{
switch(_op)
{
case '+':
_result = _x + _y;
break;
case '-':
_result = _x - _y;
break;
case '*':
_result = _x * _y;
break;
case '/':
{
if(_y == 0)
_exitCode = -1;
else
_result = _x / _y;
}
break;
case '%':
{
if(_y == 0)
_exitCode = -2;
else
_result = _x % _y;
}
break;
default:
break;
}
}
std::string formatArg()
{
return std::to_string(_x) + _op + to_string(_y) + "=";
}
std::string formatRes()
{
return std::string(_result) + "(" + std::to_string(_exitCode) + ")";
}
~Task()
{
}
private:
int _x;
int _y;
char _op;
int _result;
int _exitCode;
}
main.cc内
void* consumer(void* args)
{
BlockQueue<Task> *bq = static_cast<BlockQueue<Task>*>(args);
while(true)
{
Task t;
//1、从阻塞队列中获取数据
bq->pop(&t);
t();
//2、结合某种业务逻辑,处理数据
cout << "consumer data" << t.formatArg() <<t.formatRes() << endl;
}
}
void* productor(void* args)
{
BlockQueue<Task> *bq = static_cast<BlockQueue<Task>*>(args);
string opers = "+-*/%";
while(true)
{
sleep(1);
//1、先通过某种渠道获取数据
int x = rand() % 20 + 1;//也就是生成1-10
int y = rand() % 10 + 1;
char op = opers[rand() % opers.size()];
//2、将数据推送到阻塞队列 —— 生产过程
Task t(x, y, op)
bq->push(t);
cout << "productor Task: " << t.formatArg() << "?" << endl;
}
}
すべてのコードを再度実行して効果を確認します。これは、ネットワーク、通信などの単純なタスクをデータとして送信するだけです。
3. 複数のプロデューサーと複数のコンシューマー
main.cc 内のみの場合
int main()
{
srand((uint64_t)time(nullptr) ^ getpipd());
//BlockQueue<int> *bq = new BlockQueue<int>();
BlockQueue<Task> *bq = new BlockQueue<Task>();
//单生产和单消费
pthread_t c, p;
pthread_create(&c, nullptr, consumer, bq);
pthread_create(&p, nullptr, productor, bq);
pthread_join(c, nullptr);
pthread_join(p, nullptr);
return 0;
}
c と p を 2 つの配列に置き換え、ループを作成して結合すると、より多くの生産とより多くの消費を達成できるでしょうか? 実際にそれは可能です。なぜ?
生産者と消費者の間の相互排他と同期関係を完了するために使用するロックは 1 つだけであり、Shengsheng と Feifei 間の相互排他関係を満たすことができ、より多くの生産、より多くの消費、および高効率を意味します。生産者はデータを取得し、消費者はデータを取得します。データを処理します。
なぜロックが 1 つだけなのでしょうか? 根本的な原因は、プロデューサーとコンシューマーが同じブロッキング キューにアクセスし、その 3 つが全体としてみなされることです。
2、信号量
セマフォはクリティカル リソースの数を表すカウンターです。セマフォは PV 操作を実行する必要があり、P は - 操作に相当し、V は ++ 操作に相当し、これら 2 つの操作はアトミックです。
クリティカルなリソースが 1 つしかない場合、セマフォを 1 に設定できます。アプリケーションが成功すると、セマフォは 1 から 0 に減らされ、クリティカル領域を出るとセマフォは 1 に戻ります。セマフォはバイナリ信号、数量、つまりミューテックスロックとも呼ばれます。
リソースを複数の小さなリソースに分割し、複数のスレッドが異なる小さなリソースに同時アクセスすることができる、このときのセマフォがマルチセマフォです。各スレッドは、対応するリソースにアクセスする際、まずセマフォの申請を行い、申請が成功すればそのスレッドはリソースの使用が許可されたことを意味し、成功しなかった場合はリソースが使用できなくなります。
セマフォはリソース予約メカニズムであり、使用されていないリソースがある場合、他のスレッドは終了しない限りそのリソースにアクセスできません。
セマフォはリソースカウンタなので、アプリケーションの成功はリソースが利用可能であることを示し、アプリケーションの失敗はリソースが利用できないことを示し、本質的には判断をセマフォに変換するアプリケーションの動作です。セマフォは、クリティカル セクションにアクセスしてロックを申請する前に実行されます。
1. POSIX セマフォ
POSIX と System V はどちらも、共有リソースへの競合のないアクセスを実現するための同期操作に使用されますが、POSIX はスレッドの同期にも使用できます。
2. リングキューに基づく生産および消費モデル
配列を使用して循環キューをシミュレートします。i %= N を使用します。配列の末尾に達したら、再度 i++ を実行します。i が N に等しい場合、%=N により i は 0 になり、再び配列の先頭に来ます。リングキューでは、head は先頭を指し、tail は末尾を指し、データを最初に配置し、次に tail++ を配置します。プロデューサーはデータを末尾にプッシュし、コンシューマーはデータを先頭にポップします。生産者はスペースを重視し、消費者はデータを重視します。
リングキューでは、別のエリアを訪問すれば生産と消費を同時に行うことができますが、生産者と消費者が同じエリアをいつ訪問するのでしょうか。これは実際には追いつき問題であり、最初のケースは最初にデータがない場合であり、2 番目のケースはキューがデータでいっぱいになり、末尾が再びキューの先頭に到達する場合です。頭。最初のケースでは、プロデューサーが最初に進む必要があり、2 番目のケースでは、コンシューマが最初に進む必要があります。これら 2 つのケースは、それぞれキューが空と満杯に対応しており、その他のケースでは、プロデューサーとコンシューマーが同時に実行できます。
私たちのコードでは、キューが空の場合と満杯の場合に対応する処理が存在することを保証する必要があり、コンシューマーがプロデューサーを超えることは許可されず、プロデューサーがコンシューマーによって囲まれることもできません。
このような状況をコンピュータ言語でどのように説明できるでしょうか? プロデューサのセマフォ sem_room を定義したいとします。初期値は N で、コンシューマのセマフォ sem_data は 0 です。どちらも最初にリソースを申請する必要があり、プロデューサはスペース セマフォを申請し、コンシューマはセマフォを申請します。データ セマフォに適用されます。プロデューサー セマフォは空ではないため、プロデューサーが最初に進むことが保証されています。プロデューサーは生産アクティビティを実行して次の場所を指し、スペース リソースを返さず、変更する必要はありません。スペース セマフォ、データ セマフォに 1 を追加するだけです。消費者はアプリケーションが成功したことを検出し、消費活動を実行します。消費者はデータを持ち去りますが、消費者はデータ セマフォを変更しません。スペース セマフォを -1 に設定し、次の場所に移動します。両方とも、リングキュー全体のルールがすべて満たされるように、関心のあるセマフォを申請し、相手のセマフォを解放します。
次にコードを書きます
Main.cc、RingQueue.hpp、および Makefile ファイルを作成します。2 つのメイン ファイルは、フレームワークを最初に作成するファイルです。
メイクファイル
ringqueue:Main.cc
g++ -o $@ $^ -std=c++11 -lpthread
.PHONY:clean
clean:
rm -f ringqueue
Main.cc
#include "RingQueue.cc"
using namespace std;
void* consumerRoutine(void* args)
{
RingQueue<int>* rq = static_cast<RingQueue<int>*>(args);
while(true)
{
int data = 0;
rq->pop(&data);
cout << "consumer done: " << datat << endl;
sleep(1);
}
}
void* productorRoutine(void* args)
{
RingQueue<int>* rq = static_cast<RingQueue<int>*>(args);
while(true)
{
int data = rand() % 10 + 1;
rq->push(data);
cout << "productor done: " << data << endl;
sleep(1);
}
}
int main()
{
srand(time(nullptr) ^ getpid());
RingQueue<int> *rq = new RingQueue<int>();
//单生产单消费
pthread_t c, p;
pthread_create(&c, nullptr, consumerRoutine, nullptr);
pthread_create(&p, nullptr, productorRoutine, nullptr);
pthread_join(c, nullptr);
pthread_join(p, nullptr);
return 0;
}
セマフォを申請するには、ヘッダー ファイル < semaphore.h > を使用する必要があります。全体的な考え方は、実際には以前の生産および消費モデルと同じです。
RingQueue.hpp ファイル
#pragma once
#include <iostream>
#include <vector>
#include <pthread.h>
#include <ctime>
#include <sys/types.h>
#include <unistd.h>
#include <semaphore.h>
static const int N = 5;
template<class T>
class RingQueue
{
private:
void P(sem_t &s) {
sem_wait(&s); }
void V(sme_t &s) {
sem_post(&s); }//发布信号量的接口
public:
RingQueue(int num = N): _ring(num), _cap(num)
{
sem_init(&_data_sem, 0, 0);
sem_init(&_space_sem, 0, num);
_c_step = _p_step = 0;
}
void push(const T& in)
{
//生产
P(_space_sem);//P操作,生产者需要看看空间信号量是否不为空,不空才可以继续
//不需要判断,一定有对应的空间资源给我
//因为信号量本身就是描述临界资源的,它可以在临界区外去申请,P成功就说明可以继续执行了
_ring[_p_step] = in;//_p_step是生产者的位置
++_p_step;
_p_step %= _cap;
//V操作
V(_data_sem);//一个数据放进去了,那么数据信号量就增加
}
void pop(T* out)
{
//消费
P(_data_sem);//P操作,消费者需要看看数据信号量是否不为空,不空才可以继续
*out = _ring[_c_step];//_c_step是消费者的位置
++_c_step;
_c_step %= _cap;
V(_space_sem);//一个数据被拿走,消费者往后走一步,空间信号量就减少
}
~RingQueue()
{
sem_destroy(&_data_sem);
sem_destroy(&_space_sem);
}
private:
std::vector<T> _ring;
int _cap;//环形队列大小
sem_t _data_sem;//只有消费者关心
sem_t _space_sem//只有生产者关心
int _c_step;//消费者位置
int _p_step;//生产者位置
}
プロデューサとコンシューマのどちらを先に実行するかは問題ではありませんが、hpp ファイルでは、コードを使用してプロデューサが最初に実行されることを決定しています。Main.cc ファイルの生成関数と消費関数では、どちらが速いかを制御するためにスリープが使用されます。実際には、2 つが同期しているため、遅い方の方がもう一方も遅くなります。
int を渡すことに加えて、リング キューはクラスも渡すことができます。上記のブロッキング キューに書かれた task.hpp を使用し、operator()() の最後に usleep(100000) を書いてタスク期間をシミュレートすることができます。ヘッダファイル <unistd.h> を使用するには、Main.cc を次のように変更します
#include "RingQueue.cc"
#include "task.hpp"
using namespace std;
const char* ops = "+-*/%";
void* consumerRoutine(void* args)
{
RingQueue<Task>* rq = static_cast<RingQueue<Task>*>(args);
while(true)
{
Task t;
rq->pop(&t);
t();
cout << "consumer done, 处理完成的任务: " << t.formatArg() <<t.formatRes() << endl;
}
}
void* productorRoutine(void* args)
{
RingQueue<Task>* rq = static_cast<RingQueue<Task>*>(args);
while(true)
{
int x = rand() % 100;
int y = rand() % 100;
char op = ops[(x + y) % strlen(ops)];
Task t(x, y, op);
rq->push(t);
cout << "productor done, 生产的任务是: " << t.formatArg() << "?" << endl;
}
}
生産と消費の 2 つのアクションは時間を消費する必要があるため、タスク処理タスクの Operator()() には usleep(100000) が設定されています。
3. 複数のプロデューサーと複数のコンシューマー
上記のコードは生産者と消費者の関係を維持していますが、複数生産、複数消費に変更すると、上記のコードだけでは生産と生産、消費と消費の関係を維持できなくなるため、ロックする必要があります。生産と生産の間、消費と消費の間にあるため、2 つのロックが必要です。もちろん、プロデューサーとコンシューマーも配列に置き換える必要があります。他のスレッドがセマフォを申請してリソースを割り当てることができるように、セマフォを申請した後にロックを実行する必要があります。スレッド自体がリソースを処理できる場合は、ロックを申請して操作を実行できます。セマフォ、次に他のスレッド スレッドはハングして待機することしかできないため、効率が低くなります。
#pragma once
#include <iostream>
#include <vector>
#include <pthread.h>
#include <ctime>
#include <sys/types.h>
#include <unistd.h>
#include <semaphore.h>
#include <string>
#include <cstring>
static const int N = 5;
template<class T>
class RingQueue
{
private:
void P(sem_t &s) {
sem_wait(&s); }
void V(sme_t &s) {
sem_post(&s); }//发布信号量的接口
void Lock(pthread_mutex_t& m) {
pthread_mutex_lock(&m); }
void Unlock(pthread_mutex_t& m) {
pthread_mutex_unlock(&m); }
public:
RingQueue(int num = N): _ring(num), _cap(num)
{
sem_init(&_data_sem, 0, 0);
sem_init(&_space_sem, 0, num);
_c_step = _p_step = 0;
pthread_mutex_init(&_c_mutex, nullptr);
pthread_mutex_init(&_p_mutex, nullptr);
}
//生产
void push(const T& in)
{
P(_space_sem);//P操作,生产者需要看看空间信号量是否不为空,不空才可以继续
Lock(_p_mutex);
//不需要判断,一定有对应的空间资源给我
//因为信号量本身就是描述临界资源的,它可以在临界区外去申请,P成功就说明可以继续执行了
_ring[_p_step] = in;//_p_step是生产者的位置
++_p_step;
_p_step %= _cap;
Unlock(_p_mutex);
//V操作
V(_data_sem);//一个数据放进去了,那么数据信号量就增加
}
//消费
void pop(T* out)
{
P(_data_sem);//P操作,消费者需要看看数据信号量是否不为空,不空才可以继续
Lock(_c_mutex);
*out = _ring[_c_step];//_c_step是消费者的位置
++_c_step;
_c_step %= _cap;
Unlock(_p_mutex);
V(_space_sem);//一个数据被拿走,消费者往后走一步,空间信号量就减少
}
~RingQueue()
{
sem_destroy(&_data_sem);
sem_destroy(&_space_sem);]
pthread_mutex_destroy(&_c_mutex);
pthread_mutex_destroy(&_p_mutex);
}
private:
std::vector<T> _ring;
int _cap;//环形队列大小
sem_t _data_sem;//只有消费者关心
sem_t _space_sem//只有生产者关心
int _c_step;//消费者位置
int _p_step;//生产者位置
pthread_mutex_t _c_mutex;//消费者之间的锁
pthread_mutex_t _p_mutex;//生产者之间的锁
}
3. マルチプロデューサー・マルチコンシューマーモデルの意義
モデルの存在はバッファへの出し入れではなく、データを入れる前にタスクを並行して構築することであり、これらの操作はロックされていないため、マルチスレッドはデータを取得した後にタスクを並行して処理できます。セマフォの存在により、クリティカル領域での判断を行わずにクリティカルなリソースの使用状況を知ることができます。ロックを追加するかどうかは、対応する重要なリソースが全体として使用されているかどうかに依存するため、複数生産および複数消費モデルでは、ロックを使用してリソース間の関係を制御する必要があります。
次の記事はスレッド プールについてです。
仕上げる。