スレッドセーフなオブジェクトの寿命管理に関する注意事項

スレッドセーフクラスの作成は難しい作業ではありません。同期プリミティブを使用して内部状態を保護できますが、オブジェクトが所有するミューテックスミューテックスによってオブジェクトの生死を保護することはできません。オブジェクトの破棄中に存在する可能性のある競合状態を回避する方法は、C ++マルチスレッドプログラミングの基本的な問題であり、boostライブラリのshared_ptrとweak_ptrを使用して完全に解決できます。これは、スレッドセーフなオブザーバーモードを実現するために必要なテクノロジーでもあります。

デストラクタが複数のスレッドに遭遇したとき

他のオブジェクト指向言語とは異なり、C ++ではプログラマーがオブジェクトのライフサイクルを自分で管理する必要があります。これはマルチスレッド環境では非常に困難です。オブジェクトが複数のスレッドで同時に表示されると、オブジェクトの破棄時間は次のようになります。あいまいなレース条件が発生する可能性があります。

オブジェクトが破棄されようとしているときに、別のスレッドがこのオブジェクトのメンバー関数を実行しているかどうかをどのようにして知ることができますか?

メンバー関数の実行中に、オブジェクトが別のスレッドで破棄されないようにする方法

オブジェクトのメンバー関数を呼び出す前に、オブジェクトがまだ存在するかどうか、またはそのデストラクタがたまたま実行されているかどうかをどのようにして知ることができますか?

これらのレースの問題を解決することは、C ++マルチスレッドプログラミングが直面する基本的な問題です。このテキストは、shared_ptrを使用してこれらの問題を完全に解決し、C ++マルチスレッドプログラミングの精神的な負担を解決しようとします。

1.1.1スレッドセーフの定義

複数のスレッドから同時にアクセスすると、正しい動作を示します。

オペレーティングシステムがこれらのスレッドをどのように呼び出しても、これらのスレッドの実行順序がどのように絡み合っていても

呼び出し元のコードに追加の同期やその他の調整されたアクションは必要ありません

std :: string、std :: vector、std :: mapなど、ほとんどのC ++クラスはスレッドセーフではありません。これらのクラスは通常、複数のスレッドへのアクセスを提供するために外部でロックする必要があるためです。

MutexLockおよびMutexLockGuard

将来の議論を容易にするために、最初に2つのツールクラスに同意します。C++マルチスレッドプログラムのすべての人が、私が同様の機能クラスを使用したことを認識していると思います。

MutexLockは、ミューテックスの作成と破棄をカプセル化する単純なリソースクラスであるクリティカルセクションをカプセル化します。Windowsでは、struct CRITI-CAL_SECTIONは再入可能ですが、Linuxでは、pthread_mutex_tはデフォルトで再入可能ではありません。MutexLockは通常、他のクラスのデータメンバーです。

スレッドセーフな反例

単一のスレッドセーフクラスを作成することはそれほど難しくありません。内部状態を保護するために必要なのは同期プリミティブのみです。たとえば、次の単純なカウンタクラスCounter:

1.2オブジェクトの作成は簡単です

スレッドセーフクラスは、次の3つの条件を満たす必要があります

オブジェクトの構築はスレッドセーフである必要があります。唯一の要件は、構築中にthisポインターを開示しないこと、つまり、コンストラクターにコールバック関数を登録しないことです。
また、これをクロススレッドオブジェクト
渡さないことです。コンストラクター。関数の最後の行も機能しません。

これは、コンストラクターの実行中にオブジェクトが初期化されていないためです。これが他のオブジェクトにリークされると、他のスレッドが完成品にアクセスし、予期しない結果を引き起こす可能性があります。

これをしないでください

#include <iostream>
class Observable
{
public:
    void register_(Observable* x);
    virtual ~Observable();
    //纯虚函数是在声明虚函数时被“初始化”为0的函数。声明纯虚函数的一般形式是 virtual 函数类型 函数名 (参数表列) =0;
    virtual void update()=0;
};

class Foo : public Observable{
public:
    Foo(Observable* s)
    {
        s->register_(this); // 错误,非线程安全
    }
    virtual void update();

};
int main() {
    std::cout << "Hello, World!" << std::endl;
    return 0;
}

これは、2段階の構築、つまりコンストラクタの初期化が良い方法である場合があることを示しています。C++の教義に準拠していませんが、マルチスレッドには選択肢がありません。さらに、2段階の構築が許可されているため、コンストラクターは積極的に例外をスローする必要はありません。呼び出し元は、初期化の戻り値に依存して、オブジェクトが正常に構築されたかどうかを判断します。この関数は、エラー処理を簡素化します。

fooは基本クラスである可能性があり、累積は派生クラスの前に構築されるため、これが最後の行であってもリークしないでください。Foo:: Foo()を実行するコードの最後の行は、引き続きコンストラクターを実行します。派生クラス。現時点では、派生クラスのほとんどのオブジェクトはまだ作成中であり、安全ではありません。

比較的簡単に言えば、オブジェクトの構築でスレッドセーフを実現するのは比較的簡単です。結局のところ、露出が少なく、戻り率は0です。破壊のスレッドセーフはそれほど単純ではありません。これがこの章の焦点です。

1.3破壊は難しすぎる

オブジェクトの破壊。これは単一のスレッドでは問題になりません。せいぜいダングリングポインタとワイルドポインタは避ける必要があります。

問題は、
ダングリングポインタとは何かです。ダングリングポインタは、破壊されたオブジェクトまたはリサイクルされたアドレス
指します。ケース1:

{
    char *dp = NULL;
    {
        char c;
        dp = &c;
    }
}

状況2:

#include <stdlib.h>

void func()
{
    char *dp = (char *)malloc(A_CONST);
    free(dp);         //dp变成一个空悬指针
    dp = NULL;        //dp不再是空悬指针
    /* ... */
}

状況3:

int * func ( void )
{
    int num = 1234;
    /* ... */
    return &num;
}

numはスタックに基づく変数です。func関数が戻ると、変数スペースが再利用されます。このとき、ポインターが指すスペースは上書きされる可能性があります。、ドリップリバースチュートリアルの関数スタックの紹介が本当にとても良いことを確認することを本当にお勧めします、記事はアセンブリレジスタの観点からプロセス全体を紹介します

ワイルドポインタ:

初期化されていないポインタはワイルドポインタと呼ばれます

int func()
{
    char *dp;//野指针,没有初始化
    static char *sdp;//非野指针,因为静态变量会默认初始化为0
}

マルチスレッドプログラムには競合状態が多すぎます。一般的なメンバー関数の場合、スレッドセーフを実現する方法は、各メンバー関数のクリティカルセクションが重複しないように、スレッドを同時にではなく順次実行することです。これは明らかですが、誰もが考えているとは限らない暗黙の条件があります。クリティカルセクションを保護するためにメンバー関数によって使用されるミューテックスは有効である必要があります。デストラクタはこの仮定を破棄し、ミューテックスメンバー変数を破棄します。

1.3.1ミューテックスは方法ではありません

ミューテックスは、関数の実行を次々に保証することしかできません。ミューテックスロックでデストラクタを保護しようとする次のコードについて考えてみます。

Foo::~Foo()
{
    MutexLockGuard lock(mutex_);
}

void Foo::update()
{
    MutexLockGuard lock(mutex_);
}

この時点で、2つのスレッドAとBがFooオブジェクトxを認識し、スレッドAが破棄されx、スレッドBがx-> updateを呼び出す準備をしています。

スレッドA:

delete x;
x = NULL;

スレッドB:

if(x)
{
    x->update();
}

スレッドAはオブジェクトを破棄した後にポインターをNUllに設定しますが、スレッドBはxのメンバー関数を呼び出す前にポインターxの値をチェックしますが、それでも競合状態を回避することはできません。

1.スレッドAはデストラクタの(1)まで実行され、すでにミューテックスロックを保持しており、実行を継続します。
2.スレッドBは、(x)のチェックに合格し、(2)でブロックされます。

次に何が起こるか、神だけが知っています。デストラクタはmutex_を破棄するため、(2)の位置が永久にブロックされたり、コアダンプが発生したり、その他のより悪い状況が発生したりする可能性があります。

この例は、オブジェクトを削除した後にポインタをNULLに設定しても意味がないことを示しています。プログラムがこれを使用して二次リリースを防止したい場合は、論理的な問題があることを示しています。

1.3.2データメンバーとしてのミューテックスはデストラクタを保護できません

前の例では、クラスのデータメンバーとしてのmutexLockは、このクラスの他のデータメンバーの読み取りと書き込みを同期するためにのみ使用でき、安全な破棄を保護できないと述べています。なぜなら、mutexLockメンバーの宣言期間はせいぜいオブジェクトと同じ長さであり、破壊アクションはオブジェクトの死後に発生したと言えます。さらに、累積オブジェクトの場合、累積デストラクタが呼び出されると、派生クラスのオブジェクト部分が破棄されているため、累積オブジェクトが持つべきミューテックスロックでは、破棄プロセス全体を保護できません。さらに、デストラクタは他のスレッドがオブジェクトにアクセスできない場合にのみ安全であるため、デストラクタを保護する必要はありません。そうしないと、競合状態が発生します。

さらに、クラスの2つのオブジェクトを同時に読み書きすると、デッドロックが発生する可能性があります。たとえば、スワップ関数は次のとおりです。

void swap(Counter& a,Counter &b)
{
    MutexLockGuard aLock(a.mutex_); // potential dead lock
    MutexLockGuard bLock(b.mutex_);
    int64_t value = a.value_;
    a.value_ = b.value_;
    b.value_ = value;
}

スレッドAがswap(a、b)を実行し、スレッドBがswap(b、a)を実行すると、デッドロックが発生する可能性があります。operator =()も同様です。

Counter& Counter::operator=(const Counter& rhs) 
{
    if(this == &rhs)
    {
        return *this;
    }

    MutexLockGuard myLock(mutex_);
    // potential dead lock
    MutexLockGuard itsLock(rhs.mutex_);
    value_ = rhs.value_; // 改成 value_ = rhs.value() 会死锁
    return *this;
}

関数が同じタイプの複数の変数をロックしたい場合、常に同じ順序でロックするために、ミューテックスオブジェクトのアドレスを比較し、常に小さいアドレスでミューテックスを最初にロックできます。

関数が同じタイプの複数のオブジェクトをロックしたい場合、ロックが常に同じ順序でロックされるようにするため
に、ミューテックスオブジェクトのアドレスを比較し、常に小さい方のアドレスでミューテックスを最初にロックできます。

1.4スレッドセーフなオブザーバーの難しさ

動的に作成されたオブジェクトがまだ生きているかどうかは、ポインタを見てもわかりません(参照も非表示です
)。ポインタはメモリの一部を指しています。このメモリのオブジェクトが破壊されていると、まったく表示されません。非常に簡単な例を書きました。

#include <iostream>
int main() {
    void* ptr = malloc(100);
    free(ptr);
    if(ptr)
    {
        printf("111\n");
    }
    return 0;
}

ポインタが生きているかどうかを簡単に判断できない

非常に簡単な方法は、作成するだけで破壊しないことです。プログラムはオブジェクトを使用して、使用済みオブジェクトを一時的に保存します。次回新しいオブジェクトを申請するときに、オブジェクトが在庫にある場合は既存のオブジェクトを再利用し、そうでない場合は別のオブジェクトを作成します。オブジェクトが使い果たされると、直接リリースされませんが、プールに戻します。この方法には多くの欠点がありますが、少なくともポインタの無効化の問題を回避できます。

このソリューションには、次の問題があります。

オブジェクトプールのスレッドセーフ、部分的なリターンレースを防ぐために、オブジェクトを安全かつ完全にプールに配置する方法は?

グローバル共有データによって引き起こされるロックの競合の場合、この集中型オブジェクトプールはマルチスレッドの同時操作をシリアル化しますか?

共有オブジェクトのタイプが複数ある場合は、オブジェクトプールを繰り返すか、クラステンプレートを使用しますか?

メモリリークや断片化を引き起こしますか?

もちろん、プロキシモードを使用して処理し、対応するオブジェクトにカウンターを追加し、プロキシオブジェクトを使用してオブジェクトを申請または解放することもできます。

最後に、c ++ 11スマートポインターを使用できます。ポインターが使用されていないときに確実に解放されるため、魔法のように効率的です。管理がはるかに便利です。

以下に、c ++ 11のスマートポインタの内容を引用します。

C ++では、動的メモリの管理は一連の演算子を介して行われます。新規、動的メモリ内のオブジェクトにスペースを割り当て、オブジェクトへのポインタを返します。オブジェクトを初期化することを選択できます。削除、動的ポインタの受け入れオブジェクトを破棄し、オブジェクトに関連付けられているメモリを解放します。

動的メモリの使用は、メモリが正しい時間に解放されることを保証することが非常に難しいため、問題が発生しやすくなります。時々メモリを解放するのを忘れる

メモリを動的に使用しやすくするために、新しい標準ライブラリは2つのスマートポインタを使用して動的オブジェクトを管理します。

スマートポインタには2つのタイプがあります。Shared_ptrを使用すると、多くのポインタが同じオブジェクトを指すことができます。unique_ptrはオブジェクトのみを指します。標準ライブラリは、shared_ptrによって管理されるオブジェクトを指す、弱参照の一種であるweak_ptrと呼ばれるコンパニオンクラスも定義します。これらの3つのタイプはすべてメモリヘッダーファイルにあります。

shared_ptr 类

ベクトルと同様に、スマートポインターもテンプレートです。したがって、スマートポインターを作成するときは、追加情報(ポインターが指すことができるタイプ)を提供する必要があります。ベクトルの場合と同様に、山かっこを使用してタイプを指定し、その後に定義済みのポインターのみの名前を付けます。

12.1.1 shared_ptr类

ベクトルと同様に、スマートポインターもテンプレートです。したがって、スマートポインターを作成するときは、追加情報を提供する必要があります------ポインターが指すことができるタイプ。ベクトルのように、山かっこでタイプを指定し、その後にロックで定義されたスマートポインターの名前を指定します

デフォルトではNull

#include <stdio.h>
#include <memory>
#include <iostream>
using namespace std;
class A{

};

int main()
{
    shared_ptr<A> d;
    if(d)
    {
        cout<<"not null"<<endl;
    }else{
        cout<<"null"<<endl;
    }
    return 0;
}

shared_ptrとunique_ptrの両方でサポートされる操作。

shared_ptr<T> sp 空智能指针,可以指向类型为T的对象
unique_ptr<T> sp

p   将p作为一个对象判断,如果p是一个对象,则为true
*p  解引用p,获得它的指定对象

p->mem 等价于*p

swap(p,q) 交换p和q的指针
p.swap(q)

Shared_ptr固有の操作

make_shared<T>(args) 返回一个shared_ptr,指向一个动态分配的类型为T的对象。使用args初始化此对象

shared_ptr<T>p(a) p是shared_ptr q的拷贝;此操作会递增q中的计数器

p=q p和q都是都是share_ptr,所保存的指针必须相互转换。此操作,会递减p的引用计数,递增q的引用计数;若p的引用计数变为0,则将其管理的原内存释放

p.unique 若p.user_count()为1,返回true,否则 返回false
p.user_count 返回共享对象智能指针的数量

make_shared函数

最安全的分配和使用动态内存的方法是调用一个名为make_shared的标准库函数。次函数在动态内存中分配一个对象,并且初始化他,返回指向这个对象的shared_ptr。与智能指针一样,他在memory里。

当要用make_shared的时候,必须要指定想要创建的对象类型。我们可以使用make_shared进行赋值
#include <stdio.h>
#include <memory>
#include <iostream>
#include <cstring>

using namespace std;
class A{

};

int main()
{
    shared_ptr<int> data = make_shared<int>(42);
    cout<<*data<<endl;
    return 0;
}

shared_ptrのコピーと割り当て:

当进行拷贝或赋值的时候,每个shared_ptr都有一个关联的的计数器,通常称为引用计数。

每一个shared_ptr都有一个引用计数,无论何时我们拷贝一个shared_ptr,计数器都会增加。例如当我们使用shared_ptr初始化另一个shared_ptr的时候,或将他作为参数 传递给一个函数以及作为函数值返回的时候,他所关联的计数器都会增加。当我们给shared_ptr设置一个新值,或者shared_ptr离开作用域的时候计数器都会递减。

一旦shared_ptr引用技术为0,就会被自动释放掉。

コードの一部:

#include <memory>
using namespace std;
int main()
{
    shared_ptr<int> q;
    auto r = make_shared<int>(42);
    r = q;
    printf("%d\n",*r);
}

コアダンプが発生していることがわかりました。r= qなので、rのカウンタが1つ減り、rのカウンタが0になって解放されます。

shared_ptrは、関連付けられたメモリを自動的に解放します

shared_ptr<Foo> factory(int arg)
{
    return make_shared<Foo>(arg);
}

動的メモリは、次の3つの理由で使用されます。

1.プログラムは使用するオブジェクトの数を知りません
2.プログラムは
必要な正確なタイプを知りません3.プログラムは複数のオブジェクトを共有する必要があります

12.1.3newとshared_ptrの組み合わせ

int main()
{
    auto data = shared_ptr<int>(new int(32));
    printf("%d\n",*data);
}

リセットはカウンタと値をリセットします

int main()
{
    auto data = shared_ptr<int>(new int(32));
    data.reset();
    printf("%d\n",*data);
}

独自のデストラクタを定義する

#include <memory>
using namespace std;

class Foo{

};
void end_data(Foo* a)
{
    printf("1111\n");
}

int main()
{
    shared_ptr<Foo> p1(new Foo,end_data);

    //使用定制的deleter创建shared_ptr

    return 0;
}

unique_ptrパスデリッター

#include <memory>
using namespace std;

class Foo{
public:
    Foo(int a)
    {

    }
};
void end_data(Foo* a)
{
    printf("1111\n");
}

int main()
{
    unique_ptr<Foo,void(*)(Foo*)> p1(new Foo(3),end_data);

    return 0;
}

unique_ptrはコピーまたは割り当てることができないことに注意してください

weak_ptrは、shared_ptrオブジェクトの参照カウンターを変更しませんが、オブジェクトがまだ生きているかどうかを通知できます。

2つの非常に重要な操作

#include <memory>
using namespace std;

class Foo{
public:
    int b;
    Foo(int a)
    {
        b = a;
    }

    ~Foo()
    {
        printf("333\n");
    }
};
void end_data(Foo* a)
{
    printf("1111\n");
}

int main()
{
    shared_ptr<Foo> p1 = make_shared<Foo>(3);
    weak_ptr<Foo> p2;
    p2 = p1;
    printf("%d\n",p2.expired());
    return 0;
}

参照カウントは変更されませんが、オブジェクトが生きているかどうかを判断できます

アロケータはn個の初期化されていない文字列を割り当てます

//すべてのリリースにはwhileループデータが必要です++

int main()
{
    allocator<Foo> alloc;
    Foo* data = alloc.allocate(10);
    alloc.construct(data,1);
    alloc.deallocate(data,10);
    return 0;
}

(3)アロケータクラスアルゴリズム

1)uninitialized_copy(begin、end、begin2); //イテレータbegin1end(ポストエンドイテレータ)で表される入力範囲をbegin2の先頭のメモリにコピーし、begin2が指すメモリは必要なメモリよりも大きくする必要があります始まりまでに;

2)uninitialized_copy_n(begin、n、begin2); //イテレータbが指す要素からbegin2から始まるメモリ空間にnをコピーします

3)uninitialized_fill(begin、end、t); //イテレータbegin〜endの範囲でtのコピーを作成します。

4)uninitialized_fill_n(begin、n、t); //開始時に開始するメモリからtのn個のコピーを作成します。

おすすめ

転載: blog.csdn.net/qq_32783703/article/details/104346242