スレッド同期を実現する方法---マルチスレッドサーバープログラミングに関する注意事項を読む

4つの設計原則

1.同期の必要性を減らすために、可能な限り共有オブジェクトを使用します。オブジェクトを他のスレッドに公開できない場合は、公開しないでください。公開する場合は、不変オブジェクトのみを検討してください。機能しない場合は、変更するオブジェクトを公開できます。公開しない場合は、動作する場合は、公開されたオブジェクトを変更し、同期手段を使用してオブジェクトを保護できます。

2. 2つ目は、TaskQueue、Producer-Consumer Queue、CountDownLatchなどの高度な並行プログラミングコンポーネントの使用です。

3.最後の手段として同期プリミティブを使用する必要がある場合は、ミューテックスと条件変数のみを使用し、読み取り/書き込みロックを慎重に使用し、セマフォは使用しないでください。

4.ロックフリーコードを自分で記述したり、カーネルレベルの同期プリミティブを使用したりしないでください。

 

2.1ミューテックス

ミューテックスは最も使用されている同期プリミティブです。ミューテックスを単独で使用する主な目的は、共有データを使用することです。私の個人的な原則は次のとおりです。

RAIIの手法を使用して、これら4つの操作を作成、破棄、ロック、およびロック解除します。忘れないでください

非再帰的ミューテックスを使用するだけです。

ロックおよびロック解除関数は手動で呼び出されることはなく、すべてがスタック上のGuardオブジェクトの構築関数およびデストラクタ関数に渡されます。Guardのライフサイクルは、クリティカルゾーンとまったく同じです。このようにして、同じスコープ内で自動的に追加およびロック解除することができます。

Guardオブジェクトを作成するたびに、さまざまなロックによって引き起こされるデッドロックを防ぐために、途中で保持されているロックを検討してください。

二次的な原則は次のとおりです。

非再帰的ミューテックスを使用しないでください

1)ロックとロック解除は同じスレッド内にある必要があります。スレッドaは、スレッドbがロックしたミューテックスのロックを解除できません。

2)ロックを解除することを忘れないでください

3)繰り返しロックを解除しないでください

4)必要に応じて、PTHREAD_MUTEX_ERRORCHECKを使用してトラブルシューティングを行います

2.1.1非再帰的ミューテックスのみを使用する

非再帰的ミューテックスに固執することについての私の個人的な考えについて話してください

ミューテックスは、再帰的と非再帰的の2つのタイプに分けられます。これはposixの名前です。他の名前は、再入可能と非再入可能です。2つの違いは、同じスレッドがリエントラントロックを繰り返しロックできるが、非再帰ロックをロックできないことです。

推奨される非再帰的ミューテックスは、パフォーマンスのためではなく、設計意図を反映するためのものです。再帰と非再帰の間のパフォーマンスのギャップは実際には大きくありません。カウンターが1つ少なく、前者はわずかに高速であり、同じスレッド
で非再帰ロック複数回使用するとデッドロックが発生するためです。これは、次のことを可能にする利点です。欠点を早期に発見します。

再帰ロックの方が使いやすいことは間違いありません。同じスレッドで自分をロックすることを考える必要はありません。

再帰ロックは、その利便性のために、いくつかの問題を隠す可能性があります。ロックを取得すればオブジェクトを変更できると思いますが、外部コードはすでにロックを取得しており、同じオブジェクトを変更しています。

再帰的および非再帰的ロックがどのように使用されるかを見てみましょう。

最初にミューテックスをカプセル化しました

class MutexLock{
public:
    MutexLock()
    {
        pthread_mutexattr_init(&mutexattr);
        pthread_mutex_init(&mutex, nullptr);
    }

    MutexLock(int type)
    {
        int res;
        pthread_mutexattr_init(&mutexattr);
        res = pthread_mutexattr_settype(&mutexattr,type);
        pthread_mutex_init(&mutex, &mutexattr);
    }

    ~MutexLock()
    {
        pthread_mutex_destroy(&mutex);
    }

    void lock()
    {
        int res = pthread_mutex_lock(&mutex);
        std::cout<<res<<std::endl;
    }

    void unLock()
    {
        pthread_mutexattr_destroy(&mutexattr);
        pthread_mutex_unlock(&mutex);
    }
private:
    pthread_mutex_t mutex;
    pthread_mutexattr_t mutexattr;
};

コンストラクターにtypeを渡して、ロックのタイプを判別します

次に、デモを見る

MutexLock mutex(PTHREAD_MUTEX_RECURSIVE);


void foo()
{
    mutex.lock();
    // do something
    mutex.unLock();
}

void* func(void* arg)
{
    mutex.lock();
    printf("3333\n");
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr,func, nullptr);
    foo();
    int res;
    mutex.lock();
    sleep(5);
    mutex.unLock();
    sleep(3);
}

このコードでは、プログラムがメインスレッドのfooとmutex.lockの後にデッドロックしなかったが、実行を継続したことがわかりました。つまり、同じスレッドの再帰ロックは再入可能であり、デッドロックは発生しません。

デフォルトのロックをテストし、アプリケーションメソッドを次のように調整してみましょう。

MutexLock mutex(PTHREAD_MUTEX_DEFAULT);

上記のプログラムが行き詰まっていることがわかります!

ここでChenShuoに同意します。非再帰ロックを使用する利点は非常に明白です。彼はエラーを見つけるのが非常に簡単です。デッドロックが発生した場合でも、gdbを使用して対応するスレッドbtに移動できます。

もちろん、cで属性PTHREAD_MUTEX_ERRORCHECK_NPを使用してエラーをチェックすることもできます。必要なのは、次のようにロックを宣言することだけです。

MutexLock mutex(PTHREAD_MUTEX_ERRORCHECK_NP);

次に、次のプログラムを使用します

int main()
{
    pthread_t tid;
    int res;
    res = mutex.lock();
    printf("%d\n",res);
    res = mutex.lock();
    printf("%d\n",res);
    mutex.unLock();
    printf("end\n");
}

したがって、デッドロックが発生すると、EDEADLKに戻ります。

#define EDEADLK     35  /* Resource deadlock would occur */

したがって、問題のトラブルシューティングが容易であるという観点から、本では非再帰的ロックのみを使用するという慣行に同意し、デッドロックについては説明していません。問題の特定方法についても前述しました。

2.2条件変数

ミューテックスは、計算機リソースの競合を防ぐためのものであり、排他的な特性がありますが、特定の条件が確立されるのを待ちたい場合は、ロックを解除します

UNIXネットワーク環境プログラミングで対応する機能を学んだに違いありません。

pthread_cond_wait
pthread_cond_signal

csdnアドレス:https://blog.csdn.net / shichao1470 / article / details / 89856443
ここで、pthread_code_waitのポイントについて言及する必要があります:
「呼び出し元はロックされたミューテックスを関数に渡し、関数は自動的に呼び出しスレッドを配置します条件を待機しているスレッドのリストでミューテックスのロックを解除します。これ
により、条件チェックと条件の変更を待機しているスレッドの時間チャネルが閉じられ、スレッドが条件を見逃さないようになります。変更があった場合
。pthread_cond_waitの場合が戻り、ミューテックスは再びロックされます。」pthread_cond_waitはバイトのロックを解除します!!

このパッセージの情報量は非常に多く、ミューテックスの動作は次の3つのポイントとして理解できます。

1. pthread_cond_waitを呼び出す前に、&mutexをpthread_cond_wait関数に渡す前にミューテックスミューテックスをロックする必要があります。2。pthread_cond_wait関数
内では、着信ミューテックスが最初にロック解除されます。3
。待機条件が発生すると、pthread_cond_wait関数が内部にあります。戻る前に着信ミューテックスをロックします

条件が確立されるのを待つ必要がある場合は、条件変数を使用する必要があります。条件変数は、複数のスレッド、または特定の条件がウェイクアップされるのを待つスレッドです。条件変数の学名はGuanChengとも呼ばれます。

条件変数を使用する方法は1つしかなく、間違って使用することはほとんど不可能です。待機側の場合:

1.ミューテックスで使用する必要があります。このブール値はミューテックスで保護する必要があります

2.待機はミューテックスがロックされている場合にのみ呼び出すことができます

3.3。判定ブール条件を設定し、whileループで待機します

上記のコードの例を記述します。

簡単なConditionクラスを自分で書くことができます

class Condition:noncopyable
{
public:
    //explicit用于修饰只有一个参数的构造函数,表明结构体是显示是的,不是隐式的,与他相对的另一个是implicit,意思是隐式的
    //explicit关键字只需用于类内的单参数构造函数前面。由于无参数的构造函数和多参数的构造函数总是显示调用,这种情况在构造函数前加explicit无意义。
    Condition(MutexLock& mutex) : mutex_(mutex)
    {
        pthread_cond_init(&pcond_, nullptr);
    }

    ~Condition()
    {
        pthread_cond_destroy(&pcond_);
    }

    void wait()
    {
        pthread_cond_wait(&pcond_,mutex_.getMutex());
    }

    void notify()
    {
        (pthread_cond_signal(&pcond_));
    }

    void notifyAll()
    {
        (pthread_cond_broadcast(&pcond_));
    }

private:
    MutexLock& mutex_;
    pthread_cond_t pcond_;
};

ここでコピー不可能について話すことは主にコピーを禁止することであり、コアはコピーコンストラクターを民営化することです

class noncopyable{
protected:
    noncopyable() = default;
    ~noncopyable() = default;

private:
    noncopyable(const noncopyable&) = delete;
    const noncopyable& operator=( const noncopyable& ) = delete;
};

上記のコードでは、ifステートメントを使用する代わりに、whileループを使用して条件変数を待機する必要があります。その理由は、偽のウェイクアップ(falseウェイクアップ)
がインタビューのテストポイントでもあるためです。
シグナルとブロードキャストが終了する場合:
1。Itミューテックスがロックされている必要はありません。この場合、(理論的には)信号を呼び出します。
2.一般に信号の前にブール式を
変更します3.一般にミューテックスによって保護されるようにブール式を変更します
4.信号とブロードキャストを区別するように注意してください:ブロードキャストは通常​​状態の変化を示し、信号はリソースが利用可能であることを示します

ここで、私たちが判断するために使用する場合、誤った目覚めとは何かについて話します

if(条件满足)
{
    pthread_cond_wait();
}

pthread_cond_waitは、シグナルまたはブロードキャストが呼び出されていないときに中断される可能性があるため(シグナルによって中断またはウェイクアップされる可能性があります)、ここで使用する必要があります

本の例によると、キューのデモを非常に簡単に書くことができます

queue.ccの2つの関数を見てください

int dequeue()
{
    mutex.lock();
    while(queue.empty())
    {
        cond.wait();
    }
    int top = queue.front();
    queue.pop_front();
    mutex.unLock();
    return top;
}

void enqueue(int x)
{
    mutex.lock();
    queue.push_back(x);
    cond.notify();
}

ここで、一緒にポイントについて考えることができます。Cond。Notifyは、スレッドをウェイクアップする必要がありますか?

Cond.notifyは複数のスレッドをウェイクアップする場合がありますが、誤ったウェイクアップを防ぐためにwhileを使用すると、複数のcond_waitがウェイクアップされ、カーネルは基本的にミューテックスをロックするため、
1つのスレッドのみが実行を継続します。while(queue.empty ())判断、それでもスレッドセーフです

条件変数は非常に低レベルのプリミティブであり、直接使用されることはめったにありません。通常、高レベルの同期測定に使用されます。この例では、BlockingQueueについて説明しました。CountDownLatchの学習を続けましょう。

CountDownLatchも同期の一般的な尺度であり、主に2つの用途があります。

1.メインスレッドは複数の子スレッドを開始し、子スレッドが特定のタスクを完了するのを待ってから、メインスレッドの実行を続行します。通常、メインスレッドが複数のサブスレッドの初期化が完了するのを待つために使用されます。

2.メインスレッドが複数のサブスレッドを開始し、サブスレッドがメインスレッドを待機します。メインスレッドが他のタスクを完了すると、サブスレッドが実行を開始します。通常、複数の子スレッドがメインスレッドの開始コマンドを待機するために使用されます

muduoでのcountDownLatchの実装を分析し、これらの関数の意味を少し分析してみましょう。

もう一度__attribute__を見てみましょう。Muzeのコードは現実です

#define THREAD_ANNOTATION_ATTRIBUTE__(x)   __attribute__((x))

__attribute__を確認しましょう(https://blog.csdn.net / qlexcel / article / details / 92656797)

attribute__は、関数属性、変数属性、およびクラス属性を設定できます。__attribute__の使用方法は__attribute((x))です。

__attribute__は構造体の共用体を設定できます。設定できるパラメータは、整列、パック、transparent_union、未使用、非推奨、may_aliasの約6つです。

__attribute__を使用する場合、パラメーターの前後に__アンダースコアを追加することもできます。たとえば、alignedの代わりに__aligned__を使用すると、ヘッダーファイル
を気にせずに対応するヘッダーファイルで使用できます。同じ名前のマクロ定義はありますか。に

1、整列

オブジェクトの配置形式を指定します

struct S {

short b[3];

} __attribute__ ((aligned (8)));


typedef int int32_t __attribute__ ((aligned (8)));

このステートメントは、8バイトが整列されたスペースを割り当てるときに、変数タイプがstructSまたはint32_tであることをコンパイラーに強制します。デモ結果を見てみましょう。

struct S {

    short b[3];

};

sizeof後の結果は6です

struct S {

    short b[3];

}__attribute__((__aligned__(8)));

このように書いた後、結果は8になります

2)Packed
はこの属性を使用して、構造体または共用体の型を定義し、その型の各変数のメモリ制約を設定します。コンパイルプロセスの最適化で構造が整列されて実際の占有
数に整列されるようにコンパイラーに指示します。gcc固有の構文です。この関数はオペレーティングシステムとは関係ありません。コンパイラーとは関係ありません。gccコンパイラーはコンパクトではありません。 、windwosの下でVcはコンパクトではなく、tcプログラミングはコンパクトです

この例をもう一度見てみましょう

struct S {

    int a;
    char b;
    short c;


}__attribute__((__packed__));

パックが削除された場合、これは8バイトになるはずですが、__ packed__が7バイトになると、コンパイラはバイトアラインメントを実行しません。

3).at

絶対ポジショニング、変数または関数は、フラッシュまたはRAMに絶対ポジショニングできます。このソフトウェアは基本的に使用されていません、結局のところ、ラムボードの最下層は

さて、ここで私はmuduoコードのいくつかの非常に詳細な使用法を見ていきたいと思います

muduoで面白いコードをたくさん見ました。

#ifndef MUDUO_BASE_MUTEX_H
#define MUDUO_BASE_MUTEX_H

#include "muduo/base/CurrentThread.h"
#include "muduo/base/noncopyable.h"
#include <assert.h>
#include <pthread.h>

// Thread safety annotations {
// https://clang.llvm.org/docs/ThreadSafetyAnalysis.html

// Enable thread safety attributes only with clang.
// The attributes can be safely erased when compiling with other compilers.
#if defined(__clang__) && (!defined(SWIG))
#define THREAD_ANNOTATION_ATTRIBUTE__(x)   __attribute__((x))
#else
#define THREAD_ANNOTATION_ATTRIBUTE__(x)   // no-op
#endif

#define CAPABILITY(x) \
  THREAD_ANNOTATION_ATTRIBUTE__(capability(x))

#define SCOPED_CAPABILITY \
  THREAD_ANNOTATION_ATTRIBUTE__(scoped_lockable)

#define GUARDED_BY(x) \
  THREAD_ANNOTATION_ATTRIBUTE__(guarded_by(x))

#define PT_GUARDED_BY(x) \
  THREAD_ANNOTATION_ATTRIBUTE__(pt_guarded_by(x))

#define ACQUIRED_BEFORE(...) \
  THREAD_ANNOTATION_ATTRIBUTE__(acquired_before(__VA_ARGS__))

#define ACQUIRED_AFTER(...) \
  THREAD_ANNOTATION_ATTRIBUTE__(acquired_after(__VA_ARGS__))

#define REQUIRES(...) \
  THREAD_ANNOTATION_ATTRIBUTE__(requires_capability(__VA_ARGS__))

#define REQUIRES_SHARED(...) \
  THREAD_ANNOTATION_ATTRIBUTE__(requires_shared_capability(__VA_ARGS__))

#define ACQUIRE(...) \
  THREAD_ANNOTATION_ATTRIBUTE__(acquire_capability(__VA_ARGS__))

#define ACQUIRE_SHARED(...) \
  THREAD_ANNOTATION_ATTRIBUTE__(acquire_shared_capability(__VA_ARGS__))

#define RELEASE(...) \
  THREAD_ANNOTATION_ATTRIBUTE__(release_capability(__VA_ARGS__))

#define RELEASE_SHARED(...) \
  THREAD_ANNOTATION_ATTRIBUTE__(release_shared_capability(__VA_ARGS__))

#define TRY_ACQUIRE(...) \
  THREAD_ANNOTATION_ATTRIBUTE__(try_acquire_capability(__VA_ARGS__))

#define TRY_ACQUIRE_SHARED(...) \
  THREAD_ANNOTATION_ATTRIBUTE__(try_acquire_shared_capability(__VA_ARGS__))

#define EXCLUDES(...) \
  THREAD_ANNOTATION_ATTRIBUTE__(locks_excluded(__VA_ARGS__))

#define ASSERT_CAPABILITY(x) \
  THREAD_ANNOTATION_ATTRIBUTE__(assert_capability(x))

#define ASSERT_SHARED_CAPABILITY(x) \
  THREAD_ANNOTATION_ATTRIBUTE__(assert_shared_capability(x))

#define RETURN_CAPABILITY(x) \
  THREAD_ANNOTATION_ATTRIBUTE__(lock_returned(x))

#define NO_THREAD_SAFETY_ANALYSIS \
  THREAD_ANNOTATION_ATTRIBUTE__(no_thread_safety_analysis)

このプロパティは初めて表示されます。ドキュメントには具体的な説明があります。URLを参照してください:HTTP://clang.llvm.org /docs/ThreadSafetyAnalysis.html

ここでは、主にこのコードで使用されているguarded_byを見ていきます。このマクロの定義を、個別に見てみましょう。

#define GUARDED_BY(x) \
  THREAD_ANNOTATION_ATTRIBUTE__(guarded_by(x))

このマクロは、属性を最初にロックしてから読み取る必要があることを意味します。本に戻って、練習用のコードを書き始めました。コードには、この可変性に関する別の詳細が表示されました。

CountDownLatchの実装を見てみましょう

class CountDownLatch:noncopyable{
public:
    explicit CountDownLatch(int count) : mutex_(),cond_(mutex_),count_(count)
    {
    }
    void wait()
    {
        MutexLockGuard guard(mutex_);
        while(count_ > 0)
        {
            cond_.wait();
        }
    }

    void countDOwn()
    {
        MutexLockGuard guard(mutex_);
        --count_;
        if(count_ == 0)
        {
            cond_.notifyAll();
        }
    }

    int getCount() const
    {
        MutexLockGuard guard(mutex_);
        return count_;
    }

private:
    mutable MutexLock mutex_;
    Condition cond_ __attribute__((guarded_by(mutex_)));
    int count_;
};

可変リテラルの意味は可変で、変更が簡単で、可変ですが、constの制限を破ることもできます。いつ可変を使用する必要があるかという質問があります。c++
は、必要に応じて変更を加えるためにメンバーの属を操作するために機能することがよくあります。可変になる属性メンバー。

定数関数の特性:

1.データメンバーのみ使用可能変更できません

2.定数オブジェクトは定数関数のみを呼び出すことができ、通常の関数は呼び出すことができません

3.定数関数のthisポインターはconst *です。

getCountは定数関数であるため、mutex_は変数である必要があります

このクラスの使用法を見てみましょう

CountDownLatch syncTool(10);

class CThread{
public:
    //线程进程
    static void* threadProc(void* args)
    {
        sleep(4);
        syncTool.countDOwn();
        sleep(3);
    }
};

int main()
{


    int count = 10;
    int i;
    pthread_t tid;
    pthread_t pthread_pool[count];
    CThread threadStack;
    shared_ptr<CThread> threadObj = make_shared<CThread>();
    for(i=0;i<count;i++)
    {
        pthread_create(&tid, nullptr,&CThread::threadProc, nullptr);
    }

    syncTool.wait();

    for(i=0;i<count;i++)
    {
        pthread_join(tid, nullptr);
    }

}

子スレッドがウェイクアップすると、カウンターがデクリメントされます。すべての子スレッドが初期化されている場合は、メインスレッドに下向きに続行するように指示します。

スリープは同期プリミティブではありません

スリープシリーズの関数はテストにのみ使用できます。スレッドでの待機には、主に2つのタイプがあり、リソースが使用可能になるのを待機することと、クリティカルセクションに入るのを待機することです。

通常のプログラムで、既知の期間待機する必要がある場合は、イベントループにタイマーを挿入してから、タイマーのコールバックで作業を続行する必要があります。スレッドは、簡単に
無駄にすることはできず、簡単に無駄にすることのできない貴重なリソースです。睡眠でポーリングされます。

マルチスレッドでのシングルトンの実現

私のコードのシングルトンは次のように実装されています:

T* CSingleton<T, CreationPolicy>::Instance (void)
{
    if (0 == _instance)
    {
        CnetlibCriticalSectionHelper guard(_mutex);

        if (0 == _instance)
        {
            _instance = CreationPolicy<T>::Create ();
        }
    }

    return _instance;
}

これも非常に古典的な認識です。私は仕事を卒業してからシングルトンでこれを行っています。

マルチスレッドサーバープログラミングでは、pthread_onceを使用して

template <typename T>
class Singleton :noncopyable{
public:
    static T& instance()
    {
        pthread_once(&ponce_,&Singleton::init);
        return *value_;
    }

private:
    Singleton() = default;
    ~Singleton() = default;

    static void init()
    {
        value_ = new T();
    }

    static T* value_;
    static pthread_once_t ponce_;
};

カーネルのAPIを使用して達成することは確かに非常に良いメモです。

おすすめ

転載: blog.csdn.net/qq_32783703/article/details/105896010
おすすめ