1. ホーム
  2. c++

[解決済み】リエントラント関数とは一体何でしょうか?

2022-04-16 07:26:41

質問

最も その から、再入場の定義が引用されています。 ウィキペディア :

コンピュータプログラムまたはルーチンは リエントラントと表現するのは、次のような場合です。 安全に の前に再度呼び出されます。 前回の起動が終了している (安全に実行することができます。 を同時に使用することができます。) リエントラントであるためには コンピュータのプログラムまたはルーチン。

  1. 静的な(またはグローバルな)ものを保持しないこと。 定数でないデータ。
  2. にアドレスを返してはならない。 静的(またはグローバル)非定数 のデータを使用します。
  3. 提供されたデータに対してのみ動作しなければならない を呼び出す。
  4. シングルトンのロックに依存してはならない リソースを使用します。
  5. 自分自身のコードを変更してはならない(ただし 独自のスレッド ストレージ)
  6. 非再入可能なコンピュータを呼び出してはならない プログラムまたはルーチン。

どのように 無事 を定義していますか?

もし、あるプログラムが 安全に同時実行できる というのは、常にリエントラントであることを意味するのでしょうか?

自分のコードがリエントラントであるかどうかをチェックする際に、留意すべき6つのポイントに共通することは何でしょうか?

また

  1. 再帰的な関数はすべてリエントラントですか?
  2. スレッドセーフな関数はすべてリエントラントか?
  3. 再帰的でスレッドセーフな関数はすべてリエントラントか?

この質問を書きながら、ひとつだけ思い当たることがあります。 のような用語はありますか? 再入場 スレッドセーフ 絶対的なもの、つまり具体的な定義が決まっているのでしょうか?もしそうでないなら、この質問はあまり意味がありません。

どのように解決するのか?

1. どのように 無事に の定義は?

意味的に。この場合、これは難しく定義された言葉ではありません。ただ、「リスクなしで、それができる」という意味です。

2. プログラムが安全に同時実行できる場合、それは常にリエントラントであることを意味するのでしょうか?

いいえ。

例えば、ロックとコールバックの両方をパラメータとして受け取るC++関数があるとします。

#include <mutex>

typedef void (*callback)();
std::mutex m;

void foo(callback f)
{
    m.lock();
    // use the resource protected by the mutex

    if (f) {
        f();
    }

    // use the resource protected by the mutex
    m.unlock();
}

別の関数が同じミューテックスをロックする必要があることも十分あり得ます。

void bar()
{
    foo(nullptr);
}

一見すると、すべてがうまくいっているように見えますが、ちょっと待ってください。

int main()
{
    foo(bar);
    return 0;
}

もし、ミューテックスへのロックが再帰的でない場合、メインスレッドでは次のようなことが起こります。

  1. main を呼び出します。 foo .
  2. foo はロックを取得します。
  3. foo を呼び出します。 bar を呼び出し、それが foo .
  4. 第2回 foo は、ロックを取得しようとして失敗し、ロックが解放されるのを待ちます。
  5. デッドロックです。
  6. おっと...

よし、コールバックを使ってごまかしたぞ。しかし、もっと複雑なコードでも同様の効果が得られることは容易に想像がつきます。

3. リエントラント機能を持つコードをチェックする際に留意すべき、挙げられた6つのポイントの共通点は一体何でしょうか?

あなたは におい もしあなたの関数が変更可能な永続的なリソースにアクセスしたり、あるいは 匂う .

( さて、99%のコードは匂うはずですが、では...最後のセクションで対処しましょう... )

ですから、コードを勉強していると、その中の1つのポイントで警告が出るはずです。

  1. 関数が状態を持つ (すなわち、グローバル変数、あるいはクラスのメンバ変数にアクセスする)
  2. この関数は複数のスレッドから呼び出されたり、プロセスの実行中にスタックに2回現れる可能性があります(つまり、この関数は直接または間接的に自分自身を呼び出すことができます)。コールバックをパラメータとする関数 匂う が多い。

リエントラントでない関数を呼び出す可能性のある関数は、リエントラントとはみなされない。

また、C++のメソッド 匂う にアクセスできるため this そのため、これらのコードがおかしな相互作用をしていないかどうかを確認するために、コードを勉強する必要があります。

4.1. すべての再帰的関数はリエントラントか?

いいえ。

マルチスレッドの場合、共有リソースにアクセスする再帰的な関数が複数のスレッドから同時に呼び出され、不良データ/破損データが発生する可能性があります。

シングルスレッドの場合、再帰的な関数が再入可能でない関数(悪名高い strtok また、グローバルデータを使用する場合は、そのデータが既に使用中であるという事実を処理しません。つまり、あなたの関数は直接または間接的に自分自身を呼び出すので再帰的ですが、それでも 再帰的安全でない .

4.2. すべてのスレッドセーフな関数はリエントラントか?

上の例では、一見スレッドセーフな関数がいかにリエントラントでないかを示しました。コールバック・パラメータがあるので、ごまかしました。しかし、スレッドに非再帰的なロックを2度取得させることで、スレッドをデッドロックさせる方法はいくつもあるのです。

4.3. 再帰的でスレッドセーフな関数はすべてリエントラントか?

もし、「再帰的」という言葉が「再帰的安全」という意味であれば、「イエス」と言えるでしょう。

ある関数が複数のスレッドから同時に呼び出され、直接または間接的に自分自身を問題なく呼び出せることを保証できれば、その関数はリエントラントであると言えます。

問題はこの保証の評価ですね...^_^。

5. リエントランスやスレッドセーフなどの用語は絶対的なものですか、つまり、固定された具体的な定義があるのですか?

しかし、スレッドセーフかリエントラントかを判断するのは難しいものです。そのため、私は 匂う 上記の通りです。ある関数がリエントラントでないことはわかりますが、複雑なコード片がリエントラントであることを確認するのは難しいかもしれません。

6. 例

例えば、リソースを使用する必要があるメソッドを1つ持つオブジェクトがあるとします。

struct MyStruct
{
    P * p;

    void foo()
    {
        if (this->p == nullptr)
        {
            this->p = new P();
        }

        // lots of code, some using this->p

        if (this->p != nullptr)
        {
            delete this->p;
            this->p = nullptr;
        }
    }
};

最初の問題は、この関数が再帰的に呼び出された場合(つまり、この関数が直接または間接的に自分自身を呼び出した場合)、コードがクラッシュする可能性があるということです。 this->p は最後の呼び出しが終わった時点で削除され、最初の呼び出しが終わる前にまだ使われている可能性があります。

したがって、このコードは 再帰的安全性 .

これを修正するために、参照カウンターを使うことができる。

struct MyStruct
{
    size_t c;
    P * p;

    void foo()
    {
        if (c == 0)
        {
            this->p = new P();
        }

        ++c;
        // lots of code, some using this->p
        --c;

        if (c == 0)
        {
            delete this->p;
            this->p = nullptr;
        }
    }
};

しかし、マルチスレッドの問題があるので、まだリエントラントではない。を修正する必要があります。 cp はアトミックに行われます。 再帰的 ミューテックス(すべてのミューテックスが再帰的であるわけではありません)。

#include <mutex>

struct MyStruct
{
    std::recursive_mutex m;
    size_t c;
    P * p;

    void foo()
    {
        m.lock();

        if (c == 0)
        {
            this->p = new P();
        }

        ++c;
        m.unlock();
        // lots of code, some using this->p
        m.lock();
        --c;

        if (c == 0)
        {
            delete this->p;
            this->p = nullptr;
        }

        m.unlock();
    }
};

そしてもちろん、これはすべて lots of code の使用も含め、それ自体がリエントラントです。 p .

そして、上のコードは、まったくもって 例外セーフ が、これはまた別の話です...^_^。

7. 私たちのコードの99%はリエントラントではありません。

スパゲッティ・コードではその通りです。しかし、コードを正しく分割すれば、リエントランシー問題を回避することができます。

7.1. すべての関数が状態を持たないことを確認する

パラメータ、自身のローカル変数、状態を持たない他の関数のみを使用し、データのコピーを返す場合は、そのデータのコピーを返さなければなりません。

7.2. オブジェクトが再帰的安全であることを確認する。

オブジェクトのメソッドは this そのため、そのオブジェクトの同じインスタンスのすべてのメソッドと状態を共有します。

ですから、スタックのある地点(つまりメソッドAを呼び出す)でオブジェクトを使用し、その後、別の地点(つまりメソッドBを呼び出す)で、オブジェクト全体を破壊することなく使用できることを確認します。メソッド終了時に、オブジェクトが安定して正しい状態になるように設計してください(ぶら下がりポインタ、矛盾したメンバ変数など)。

7.3. すべてのオブジェクトが正しくカプセル化されていることを確認する

他の人が自分の内部データにアクセスできないようにする。

    // bad
    int & MyObject::getCounter()
    {
        return this->counter;
    }

    // good
    int MyObject::getCounter()
    {
        return this->counter;
    }

    // good, too
    void MyObject::getCounter(int & p_counter)
    {
        p_counter = this->counter;
    }

ユーザーがデータのアドレスを取得した場合、const参照を返すだけでも危険です。const参照を保持しているコードに知らせずに、コードの他の部分がデータを変更する可能性があります。

7.4. オブジェクトがスレッドセーフでないことをユーザが知っていることを確認する

したがって、スレッド間で共有されるオブジェクトを使用するには、ユーザーの責任でミューテックスを使用する必要があります。

STL のオブジェクトは、(パフォーマンスの問題から) スレッドセーフでないように設計されています。 std::string 2つのスレッド間のアクセスは、並行処理プリミティブで保護する必要があります。

7.5. スレッドセーフなコードが再帰的に安全であることを確認する

つまり、同じスレッドが同じリソースを二度使用する可能性がある場合、再帰的ミューテックスを使用することです。