[Linux]スレッドの同期

[Linux]スレッドの同期

スレッドの同期

スレッド飢餓の問題

スレッド不足とは、1 つ以上のスレッドが必要なリソースや実行タイム スライスを取得できず、ブロックされたり、長時間待機したりして実行を継続できない状況を指します。

スレッド枯渇の問題を理解するための簡単な例を挙げます。複数のスレッドはロックによってスレッド相互排他制御を完了しますが、ロックを申請した最初のスレッドがロックを解放した後すぐにロックを申請したため、他のスレッドはロックを申請できなくなりました。ロック。

コンセプト

スレッドの同期:データのセキュリティを確保することを前提として、スレッドが特定の順序で重要なリソースにアクセスできるようにし、それによって枯渇の問題を効果的に回避することを同期と呼びます。

スレッドの同期を理解するには、前述のスレッド スターベーションの問題で示された例を使用してください。複数のスレッドも、ロックを通じて相互に排他的です。ロックを適用した最初のスレッドがロックを解放した後、次のスレッドが順番にロックを適用します。これにより、スレッド枯渇の問題に対する解決策が完成します。

スレッド同期制御 - 条件変数

条件変数は、Linux オペレーティング システムのネイティブ スレッド ライブラリで提供されるデータ型でありpthread_cond_t、条件変数を使用することでスレッドの同期制御を完了できます。

条件変数は内部的に循環キューを維持し、スレッドが条件変数に渡された後、条件変数はスレッドを独自の循環キュー構造を通じて順番に実行できます。

pthread_cond_init 関数

Linux オペレーティング システムには、pthread_cond_init条件変数を初期化するための関数が用意されています。

//pthread_cond_init所在的头文件和函数声明
#include <pthread.h>

int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);
  • cond パラメータ:初期化される条件変数。
  • attr パラメータ:条件変数に設定された属性。デフォルトでは null ポインタが渡されます。
  • グローバル条件変数は、pthread_cond_t cond = PTHREAD_COND_INITIALIZER初期化と同じ方法で初期化できます。

pthread_cond_destroy関数

Linux オペレーティング システムには、pthread_cond_destroyロックを破棄する機能が用意されています。

//pthread_mutex_destroy所在的头文件和函数声明
#include <pthread.h>

int pthread_cond_destroy(pthread_cond_t *cond);
  • pthread_cond_destroyローカル条件変数の破棄に使用される関数。
  • cond パラメータ:破棄する条件変数。

pthread_cond_wait 関数

//pthread_cond_wait所在的头文件和函数声明
#include <pthread.h>

int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
  • pthread_cond_waitこの関数は、条件変数の準備ができるまでスレッドを待機させるために使用されます。スレッドが条件変数の待機を開始すると、条件変数の準備ができた場合にのみスレッドは実行を継続できます。
  • cond パラメータ:使用される条件変数。
  • mutex パラメータ:使用するロック。
  • pthread_cond_waitこの関数は、最初に受信ロックを解放して他のスレッドがクリティカル セクションにアクセスできるようにし、次にブロックして条件変数の準備ができるまで待ちます。条件変数の準備ができたら、スレッドの安全性を確保するために再度受信ロックを適用します。

pthread_cond_signal 関数

//pthread_cond_signal所在的头文件和函数声明
#include <pthread.h>

int pthread_cond_signal(pthread_cond_t *cond);
  • pthread_cond_signalこの関数は、最初の順序で条件変数を待っているスレッドを起動するために使用されます。この関数は、条件変数を待っているスレッドに、条件変数の準備ができていることを伝え、スレッドは起動後も実行を継続します。
  • cond パラメータ:使用される条件変数。

pthread_cond_broadcast 関数

//pthread_cond_broadcast所在的头文件和函数声明
#include <pthread.h>

int pthread_cond_broadcast(pthread_cond_t *cond);
  • pthread_cond_broadcastこの関数は、条件変数を待っているすべてのスレッドを起動するために使用されます。この関数は、条件変数を待機しているすべてのスレッドに、条件変数の準備ができたことを通知し、スレッドが起動された後、順番に実行を続けます。
  • cond パラメータ:使用される条件変数。

条件変数に関する関数の使用例

条件変数によってスレッドが順番に実行できることを確認するには、確認するためのコードを記述します。具体的なコードは次のとおりです。

#include <iostream>
#include <unistd.h>
#include <pthread.h>

#define NUM 5

using namespace std;

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;//锁的初始化

pthread_cond_t cond = PTHREAD_COND_INITIALIZER;//条件变量的初始化

void *active(void *args)
{
    
    
    const char* tname = static_cast<const char*>(args);
    while(true)
    {
    
    
        pthread_mutex_lock(&mutex);
        pthread_cond_wait(&cond, &mutex);//等待函数内会自动释放锁
        cout << tname << " active" << endl;
        pthread_mutex_unlock(&mutex);
    }
    return nullptr;
}

int main()
{
    
    
    pthread_t tids[NUM];
    for (int i = 0; i < NUM; i++)//线程创建
    {
    
    
        char* name = new char[64];
        snprintf(name, 64, "thread-%d", i+1);
        pthread_create(tids+i, nullptr, active, name);
    }

    sleep(2);

    while(true)//唤醒线程
    {
    
    
        pthread_cond_signal(&cond);
        sleep(1);
    }

    for (int i = 0; i < NUM; i++)//线程回收
    {
    
    
        pthread_join(tids[i], nullptr);
    }
    return 0;
}

コードをコンパイルして実行し、結果を確認します。

条件

上記の検証コードでは、スレッドがロックされてクリティカル セクションに入った後、まず条件変数の準備が完了するのを待ち、スレッドの呼び出し順序に従って条件変数の循環キューで待機します。変数は循環キュー内のスレッドの順序に従って起動されるため、スレッドは特定の順序で実行されます。

生産者消費者モデル

生産者-消費者モデルを理解するために、スーパーマーケットを運送業者として形成された生産者-消費者モデルを実際の例で見てみましょう。

画像-20230930173651179

サプライヤーは生産者として商品を生産し、スーパーマーケットに納品し、顧客は消費者としてスーパーマーケットから商品を消費します。サプライヤーは生産者として商品を大量に生産してスーパーマーケットに販売し、顧客は消費者としてスーパーマーケットに商品を買いに行くことで、生産効率が向上するだけでなく、消費効率も向上します。供給者は生産者として商品を大量に生産してスーパーマーケットに届けることができ、顧客は消費者としてスーパーにある商品だけを気にすればよいため、たとえ消費者が当分消費しなくても、供給者は継続的に商品を生産し続けることができます。たとえ供給者が当分製品を生産しなくても、消費者は消費を続けることができるため、生産と消費のペースが不一致になり、生産と消費の忙しさが不均一になる可能性があり、生産と消費の両方が継続的に行われます。遅れていない。

スレッドの概念に対応:

  • スーパーマーケットは、データを何らかの構造に格納するバッファです。
  • サプライヤーはプロデューサー スレッドであり、ある種の構造化データを生成し、それをバッファーに送信します。
  • カスタマーはコンシューマ スレッドであり、バッファからデータをフェッチし、それに応じてデータを処理します。

バッファーは、生産者と消費者がお互いを気にする必要がないように取引の場として使用されるため、生産者/消費者モデルの利点は次のとおりです。

  • デカップリング

  • 同時実行性のサポート

  • 忙しさや怠さの偏りをサポート

プロデューサー/コンシューマー モデルは、プロセス間通信で使用されるパイプラインに似た、スレッド間の通信の一種であるとも言えます。このモデルのバッファーはスレッド通信の場所として機能し、プロデューサー スレッドとコンシューマー スレッドから参照される必要があります。したがって、バッファーは共有リソースです。共有リソースとして、スレッドの安全性の問題を回避するために保護する必要があります。生産者・消費者モデル。

生産者/消費者モデルの特徴の概要:

  • 3つの関係
    • プロデューサとプロデューサ: 相互に排他的な関係(バッファスペースは限られており、プロデューサによって送信された限られたデータのみを保存できます)
    • プロデューサーとコンシューマー: 同期関係(バッファーがいっぱいの場合はコンシューマーが優先的に消費し、バッファーが空の場合はプロデューサーが優先的に生成します。特定の同期関係で処理されない場合、プロデューサーとコンシューマーは、消費者は頻繁にアクセスする必要があり、不必要にバッファのステータスをクエリするため、非効率になります)と相互排他関係(消費者と生産者は同時にバッファにアクセスできません)
    • コンシューマとコンシューマ: 相互に排他的な関係(バッファ内のデータは制限されており、コンシューマはバッファから限られたデータしか取得できません)
  • 二つの役割
    • プロデューサー
    • 消費者
  • 取引場所
    • バッファ

BlockingQueue に基づくプロデューサー/コンシューマー モデル

マルチスレッド プログラミングでは、ブロッキング キュー (Blocking Queue) は、プロデューサーおよびコンシューマー モデルを実装するために一般的に使用されるデータ構造です。通常のキューとの違いは、キューが空の場合、キューに要素が配置されるまでキューから要素を取得する操作がブロックされ、キューがいっぱいの場合、キューに要素を格納する操作もブロックされることです。要素がキューから取り出されるまでブロックされます (上記の操作は別のスレッドに基づいており、ブロックしているキュー プロセスで操作する場合、スレッドはブロックされます)。

画像-20231001164158555

BlockingQueue に基づくプロデューサー/コンシューマー モデルと条件変数の使用を体験するコードを作成します。

blockqueue.hpp: ブロッキングキューファイルを実装する

#include <queue>
#include <pthread.h>

const int gcap = 5;

template <class T>
class blockqueue
{
    
    
private:
    bool isFull() {
    
     return _q.size() == _cap; }
    bool isEmpty() {
    
     return _q.empty(); }

public:
    blockqueue(int cap = gcap) : _cap(cap)
    {
    
    
        pthread_mutex_init(&_mutex, nullptr);
        pthread_cond_init(&_consumercond, nullptr);
        pthread_cond_init(&_productorcond, nullptr);
    }
    void push(const T &in)
    {
    
    
        pthread_mutex_lock(&_mutex);
        while (isFull()) // 队列已满,采用循环判断是为了避免多个生产者都被消费者唤醒后造成并发问题,比如队列只剩一个空间,但多个生产者线程被唤醒后进行数据操作
        {
    
    
            pthread_cond_wait(&_productorcond, &_mutex);
        }
        _q.push(in);
        pthread_cond_signal(&_consumercond);
        pthread_mutex_unlock(&_mutex);
    }
    void pop(T *out)
    {
    
    
        pthread_mutex_lock(&_mutex);
        while (isEmpty())
        {
    
    
            pthread_cond_wait(&_consumercond, &_mutex);
        }
        *out = _q.front();
        _q.pop();
        pthread_cond_signal(&_productorcond);
        pthread_mutex_unlock(&_mutex);
    }
    ~blockqueue()
    {
    
    
        pthread_mutex_destroy(&_mutex);
        pthread_cond_destroy(&_consumercond);
        pthread_cond_destroy(&_productorcond);
    }

private:
    std::queue<T> _q;
    int _cap;                      // 记录容量
    pthread_mutex_t _mutex;        // 控制线程互斥
    pthread_cond_t _consumercond;  // 控制消费者线程
    pthread_cond_t _productorcond; // 控制生产者线程
};

Task.hpp:プロデューサーとコンシューマーによって処理されるデータを実装するクラス。

#include <cstdlib>
#include <iostream>

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

    const std::string operatorArgs()//打印要处理的数据
    {
    
    
        return "(" + std::to_string(_x) + _op + std::to_string(_y) + ")" + "(" + std::to_string(_exitcode) + ")"; 
    }

    const std::string operatorRes()//打印数据处理结果
    {
    
    
        return  std::to_string(_x) + _op +  std::to_string(_y) + "=" + std::to_string(_result); 
    }

private:
    int _x;//左操作数
    int _y;//右操作数
    char _op;//操作符
    int _result;//算数结果
    int _exitcode;//退出码
};

main.cc:プロデューサー/コンシューマー モデルを実装するファイル。

#include "blockqueue.hpp"
#include "Task.hpp"
#include <iostream>
#include <unistd.h>
#include <pthread.h>

void *consumer(void *args)//消费者方法
{
    
    
    blockqueue<Task> *bq = static_cast<blockqueue<Task>*>(args);
    while(true)
    {
    
    
        Task t;
        bq->pop(&t);//从阻塞队列中取出数据
        t();//对获取的数据进行处理
        std::cout << "consumer: " << t.operatorRes() << std::endl;
    }
}

void *productor(void* args)//生产者方法
{
    
    
    blockqueue<Task> *bq = static_cast<blockqueue<Task>*>(args);
    std::string ops = "+-*/%";
    while(true)
    {
    
    
        int x = rand() % 20;//生成数据
        int y = rand() % 10;
        char op = ops[rand() % ops.size()];
        Task t(x, y, op);
        bq->push(t);//将数据交给阻塞队列
        std::cout << "productor: " << t.operatorArgs() << "?" << std::endl;
    }
}

int main()
{
    
       
    srand((uint32_t)time(nullptr) ^ getpid());//设置随机数
    blockqueue<Task>* q = new blockqueue<Task>;//创建阻塞队列
    pthread_t c[2];
    pthread_t p[3];
    pthread_create(&c[0], nullptr, consumer, q);
    pthread_create(&c[1], nullptr, consumer, q);
    pthread_create(&p[0], nullptr, productor, q);
    pthread_create(&p[1], nullptr, productor, q);
    pthread_create(&p[2], nullptr, productor, q);
    pthread_join(c[0], nullptr);
    pthread_join(c[1], nullptr);
    pthread_join(p[0], nullptr);
    pthread_join(p[1], nullptr);
    pthread_join(p[2], nullptr);
    return 0;
}

コードをコンパイルして実行し、結果を確認します。

画像-20231001171758937

上記のコードの全体的なロジックは、プロデューサー スレッドがデータを生成してブロッキング キューに渡し、コンシューマー スレッドがブロッキング キューからデータを取得して処理し、プロデューサーが算術式を生成し、コンシューマーが演算を実行するというものです。表現。キューが 1 つしかなく、すべてのスレッドが相互に排他的であるため、すべてのスレッドがロックを共有します。プロデューサーとコンシューマーの同期は、条件変数の待機とウェイクアップによって完了します。

説明:効率を向上させるプロデューサー/コンシューマー モデルの原理は、プロデューサーがデータを取得するときに、コンシューマーがバッファからデータを取得できることです。コンシューマーがデータを処理するとき、プロデューサーはデータをバッファに渡すことができ、1 つのコンシューマーがデータをバッファに渡すことができます。データを処理します。別のコンシューマがバッファからデータを取得できます。プロデューサーがデータを取得します。別のコンシューマがデータをバッファに渡すことができます。すべてのスレッドが同時に実行できます。データを待っているシリアル化されたコンシューマは存在しません。さあ、プロデューサーはデータを待ちます。消費者はデータの処理を終了します。

スレッド同期制御 – POSIX セマフォ

コンセプト

POSIX セマフォは、スレッド間の同期操作に使用され、共有リソースへの競合のないアクセスを実現します。

POSIX セマフォはsem_tLinux オペレーティング システムで提供されるデータ型であり、POSIX セマフォを使用することでスレッドの同期制御を完了できます。

セマフォの本質は、重要なリソースの数を記録するカウンターです。クリティカルなリソースにアクセスするには、スレッドは P 操作 (リソースのアプリケーション) を実行する必要があります。クリティカルなリソースを使用した後、スレッドは V 操作 (リソースの解放) を実行する必要があります。セマフォは、セマフォが実行する操作を通じてスレッドの同期を制御します。重要なリソースにアクセスするときにスレッドを適用する必要があります。PV 操作はアトミックです。

セマフォ制御スレッド同期のメカニズム: ロックを適用してから重要なリソースを判断することが、セマフォの適用に変換されます。セマフォ アプリケーションの操作は、ロック アプリケーションとクリティカル リソースの操作に先行します。複数のスレッドが同じリソースにアクセスする必要がある場合、セマフォを適用したリソースのみがロックとクリティカル リソースの操作を適用できます。他のスレッドはセマフォを待つことしかできません。スレッドが順番に実行され、ロックを適用できない、またはクリティカルなリソースがなければロックを適用できないという枯渇の問題が発生しないこと。

sem_init 関数

//sem_init函数所在的头文件和函数声明
#include <semaphore.h>

int sem_init(sem_t *sem, int pshared, unsigned int value);
  • sem_init関数はセマフォを初期化するために使用されます。
  • sem パラメータ:初期化されるセマフォ。
  • pshared パラメータ: pshared:0 はスレッド間での共有を意味し、0 以外の場合はプロセス間での共有を意味します。
  • value パラメータ:セマフォの初期値、つまりリソー​​スの数。

sem_wait 関数

//sem_wait函数所在的头文件和函数声明
#include <semaphore.h>

int sem_wait(sem_t *sem);
  • sem_waitこの関数は、セマフォの適用 (P 操作) を待機するために使用されます。
  • sem パラメータ:適用されるセマフォ。

sem_post関数

//sem_post函数所在的头文件和函数声明
#include <semaphore.h>

int sem_post(sem_t *sem);
  • sem_postこの関数は、セマフォを解放するために使用されます (V 操作)。
  • sem パラメータ:解放されるセマフォ。

sem_destroy関数

//sem_destroy函数所在的头文件和函数声明
#include <semaphore.h>

int sem_destroy(sem_t *sem);
  • sem_destroyセマフォの破棄に使用される関数。
  • sem パラメータ:破棄されるセマフォ。

リングキューに基づく生産および消費モデル

同期制御にセマフォを使用し、バッファとして循環キューを使用するプロデューサー/コンシューマー モデルには、次の特徴があります。

  • プロデューサーは空間リソースに関心があり、コンシューマーはデータ リソースに関心があります。
  • セマフォが 0 でない限り、リソースが使用可能であり、スレッドがバッファー リソースにアクセスできることを意味します。
  • 循環キューでは、同じ場所にアクセスしない限り、プロデューサーとコンシューマーは同時に動作できます。
  • リング キューが空またはいっぱいの場合、プロデューサーとコンシューマーは同じ領域にアクセスし、セマフォを使用して、最初に動作する操作可能なリソースを持つスレッドを制御します。

画像-20231002152006068

リング キューとセマフォの使用に基づいた生産モデルと消費モデルを体験するコードを作成します。

ringqueue.hpp:循環キュー ファイルを実装します。


#include <vector>
#include <semaphore.h>
#include <pthread.h>

using namespace std;

const int N = 5;

template<class T>
class ringqueue
{
    
    
private:
    void P(sem_t& sem) {
    
     sem_wait(&sem); }//P操作
    void V(sem_t& sem) {
    
     sem_post(&sem); }//V操作
    void Lock(pthread_mutex_t& mutex) {
    
     pthread_mutex_lock(&mutex); }//申请锁操作
    void UnLock(pthread_mutex_t& mutex) {
    
     pthread_mutex_unlock(&mutex); }//释放锁操作
public:
    ringqueue(int num = N):_cap(num), _ring(num), _c_step(0), _p_step(0)
    {
    
    
        sem_init(&_data_sem, 0, 0);
        sem_init(&_space_sem, 0, num);
        pthread_mutex_init(&_c_mutex, nullptr);
        pthread_mutex_init(&_p_mutex, nullptr);
    }

    void push(const T& in)
    {
    
    
        P(_space_sem);//申请空间信号量
        Lock(_p_mutex);//对生产者锁申请锁
        _ring[_c_step++] = in;
        _c_step %= _cap;
        UnLock(_p_mutex);//对生产者锁释放锁
        V(_data_sem);//释放数据信号量
    }

    void pop(T* out)
    {
    
    
        P(_data_sem);//申请数据信号量
        Lock(_c_mutex);//对消费者锁申请锁
        *out = _ring[_c_step++];
        _c_step %= _cap;
        UnLock(_c_mutex);//对消费者锁释放锁
        V(_space_sem);//释放空间信号量
    }

    ~ringqueue()
    {
    
    
        sem_destroy(&_data_sem);
        sem_destroy(&_space_sem);
        pthread_mutex_destroy(&_c_mutex);
        pthread_mutex_destroy(&_p_mutex);
    }
private:
    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;//生产者互斥控制锁
};

Task.hpp:プロデューサーとコンシューマーによって処理されるデータを実装するクラス。

#include <cstdlib>
#include <iostream>
#include <unistd.h>

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

    const std::string operatorArgs()//打印要处理的数据
    {
    
    
        return "(" + std::to_string(_x) + _op + std::to_string(_y) + ")" + "(" + std::to_string(_exitcode) + ")"; 
    }

    const std::string operatorRes()//打印数据处理结果
    {
    
    
        return  std::to_string(_x) + _op +  std::to_string(_y) + "=" + std::to_string(_result); 
    }

private:
    int _x;//左操作数
    int _y;//右操作数
    char _op;//操作符
    int _result;//算数结果
    int _exitcode;//退出码
};

main.cc:プロデューサー/コンシューマー モデルを実装するファイル。

#include "ringqueue.hpp"
#include "Task.hpp"
#include <iostream>
#include <cstring>

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.operatorRes() << 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.operatorArgs() << endl;
    }
}

int main()
{
    
    
    ringqueue<Task>* rq= new ringqueue<Task>();
    pthread_t c[3], p[2];

    for (int i = 0; i < 3; i++)
        pthread_create(c + i, nullptr, consumerRoutine, rq);
    for (int i = 0; i < 2; i++)
        pthread_create(p + i, nullptr, productorRoutine, rq);

    for (int i = 0; i < 3; i++)
        pthread_join(c[i], nullptr);
    for (int i = 0; i < 2; i++)
        pthread_join(p[i], nullptr);

    return 0;
}

コードをコンパイルして実行し、結果を確認します。

画像-20231002191054980

プロデューサー/コンシューマー モデルを実装するためにセマフォを使用することの重要性:より多くの生産と消費を行う場合、データをキューに入れる前にタスクを同時に構築します。データを取得した後は、これらの操作がロックされていないため、複数のスレッドでタスクを同時に処理できます。

おすすめ

転載: blog.csdn.net/csdn_myhome/article/details/133499769
おすすめ