再入可能関数とは正確には何ですか?


198

ほとんど 時間は、再入の定義はから引用されウィキペディア

コンピュータプログラムまたはルーチンは、以前の呼び出しが完了する前に安全に再度呼び出すことができる(つまり、同時に安全に実行できる)場合、再入可能 と呼ばれます。再入可能であるためには、コンピュータプログラムまたはルーチン:

  1. 静的(またはグローバル)非定数データを保持してはなりません。
  2. 静的(またはグローバル)非定数データにアドレスを返してはなりません。
  3. 呼び出し元から提供されたデータのみを処理する必要があります。
  4. シングルトンリソースへのロックに依存してはなりません。
  5. 独自のコードを変更してはなりません(独自のスレッドストレージで実行する場合を除く)
  6. 非再入可能コンピュータプログラムまたはルーチンを呼び出さないでください。

安全にどのよう定義されていますか?

プログラムを安全に並行して実行できる場合、それは常に再入可能であることを意味しますか?

再入可能機能についてコードをチェックする際に留意する必要がある、言及された6つのポイント間の一般的なスレッドは正確に何ですか?

また、

  1. すべての再帰関数は再入可能ですか?
  2. すべてのスレッドセーフ関数は再入可能ですか?
  3. すべての再帰的でスレッドセーフな関数は再入可能ですか?

この質問を書いていると、1つのことが頭に浮かびます。再入可能性スレッドセーフティなどの用語は絶対的なものですか。具体的な定義が固定されているのでしょうか。そうでない場合、この質問はあまり意味がありません。


6
実際、最初のリストの#2には同意しません。再入可能な関数から好きなようにアドレスを返すことができます-制限は、呼び出しコードでそのアドレスを使用して行うことです。

2
@Neilしかし、リエントラント関数の作成者は、呼び出し元が何を制御できないので、本当にリエントラントであるためには、静的(またはグローバル)非定数データにアドレスを返してはいけません。
Robben_Ford_Fan_boy

2
@drelihan呼び出し元が戻り値をどのように処理するかを制御することは、任意の関数の作成者(再入可能かどうか)の責任ではありません。彼らは確かに発信者がそれで何ができるかを言う必要がありますが、発信者が何か他のことをすることを選択した場合-発信者への幸運。

「スレッドセーフ」は、スレッドが実行していること、およびスレッドの実行によって期待される効果を指定しない限り、意味がありません。しかし、おそらくそれは別の質問であるべきです。

安全に言うと、スケジュールに関係なく、動作は明確に定義され、確定的です。
AturSams 2014年

回答:


191

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

mutexのロックが再帰的でない場合は、メインスレッドで次のようになります。

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

この方法では、コードは再帰的なセーフになり...しかし、それはまだので、問題をマルチスレッドのリエントラントではありません。私たちは、でなければならないことを確認の修正cとのp使用、アトミックに行われます再帰的な mutexを(すべてではないミューテックスは再帰的です):

#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。スレッドセーフなコードが再帰的に安全であることを確認してください

つまり、同じリソースが同じスレッドで2回使用できると思われる場合は、再帰的mutexを使用します。


1
少し混乱させるために、私は実際にはこの場合「安全性」が定義されていると思います-それは関数が提供された変数にのみ作用することを意味します-すなわち、それはその下の定義引用の省略形です そしてポイントは、これが安全の他の考えを意味しないかもしれないということです。
Joe Soul-bringer

最初の例では、ミューテックスの通過に失敗しましたか?
detly

@paercebal:あなたの例は間違っています。実際にはコールバックを気にする必要はありません。単純な再帰があれば同じ問題が発生しますが、唯一の問題は、ロックが割り当てられている場所を正確に伝えるのを忘れていることです。
イットリル

3
@イットリル:私はあなたが最初の例について話していると思います。本質的に、コールバックの匂いがするので、私は「コールバック」を使用しました。もちろん、再帰関数にも同じ問題がありますが、通常、関数とその再帰的な性質を簡単に分析できるため、再入可能か再帰性に問題がないかを検出できます。一方、コールバックとは、コールバックを呼び出す関数の作成者が、コールバックの実行内容について何の情報も持たないことを意味します。そのため、この作成者は、関数が再入可能であることを確認するのが難しい場合があります。これが私が見せたかったこの難しさです。
paercebal

1
@Gab是好人:最初の例を修正しました。ありがとう!シグナルハンドラーには、再入可能性とは異なり、独自の問題が発生します。通常、シグナルが発生した場合、具体的に宣言されたグローバル変数を変更する以外は何もできません。
paercebal

21

「安全に」とは、常識に定められたとおりに定義されます。「他のことを妨げることなく、そのことを正しく行う」ことを意味します。あなたが引用する6つのポイントは、それを達成するための要件を明確に表現しています。

あなたの3つの質問に対する答えは3×「いいえ」です。


すべての再帰関数は再入可能ですか?

番号!

たとえば、同じグローバル/静的データにアクセスする場合、再帰関数を2回同時に呼び出すと、簡単にお互いを台無しにする可能性があります。


すべてのスレッドセーフ関数は再入可能ですか?

番号!

関数は、同時に呼び出されても誤動作しない場合、スレッドセーフです。しかし、これは、たとえば、最初の呼び出しが完了するまで2番目の呼び出しの実行をブロックするためにmutexを使用することによって達成できるため、一度に1つの呼び出しのみが機能します。再入可能とは、他の呼び出しを妨げることなく同時に実行することを意味します


すべての再帰的でスレッドセーフな関数は再入可能ですか?

番号!

上記を参照。


10

共通のスレッド:

割り込み中にルーチンが呼び出された場合の動作は明確に定義されていますか?

次のような関数がある場合:

int add( int a , int b ) {
  return a + b;
}

次に、外部状態に依存しません。動作は明確に定義されています。

次のような関数がある場合:

int add_to_global( int a ) {
  return gValue += a;
}

結果は、複数のスレッドで適切に定義されていません。タイミングを間違えると、情報が失われる可能性があります。

再入可能な関数の最も単純な形式は、渡された引数と定数値のみを操作するものです。それ以外のものは特別な処理が必要で、多くの場合、再入可能ではありません。そしてもちろん、引数は可変グローバルを参照してはなりません。


7

ここで、以前のコメントについて詳しく説明する必要があります。@paercebalの答えが間違っています。例のコードでは、パラメーターであるはずのミューテックスが実際に渡されなかったことに誰も気づきませんでしたか?

私は結論に異議を唱えます、私は主張します:同時実行の存在下で関数が安全であるためには、それは再入可能でなければなりません。したがって、コンカレントセーフ(通常はスレッドセーフで記述)はリエントラントを意味します。

スレッドセーフでもリエントラントでも、引数については何も言いません。不適切なパラメーターが使用されている場合、安全でない可能性がある関数の同時実行について話しています。

たとえば、memcpy()はスレッドセーフであり、(通常は)再入可能です。2つの異なるスレッドから同じターゲットへのポインターを使用して呼び出された場合は、明らかに期待どおりに動作しません。これがSGI定義のポイントであり、クライアントに責任を課して、同じデータ構造へのアクセスがクライアントによって同期されるようにします。

一般に、スレッドセーフな操作にパラメーターを含めることは無意味であることを理解することが重要です。データベースプログラミングを実行したことがあれば、理解できるでしょう。「アトミック」であり、ミューテックスまたはその他の手法によって保護される可能性のあるものの概念は、必然的にユーザーの概念です。データベースでトランザクションを処理するには、複数の中断のない変更が必要になる場合があります。同期を保つ必要があるのはクライアントプログラマであると誰が言えるでしょうか。

ポイントは、「破損」がシリアル化されていない書き込みでコンピューターのメモリをめちゃくちゃにする必要がないことです。個々のすべての操作がシリアル化されていても、破損が発生する可能性があります。したがって、関数がスレッドセーフであるか、再入可能であるかを尋ねる場合、質問は、適切に分離されたすべての引数を意味します。結合引数を使用しても、反例とはなりません。

そこには多くのプログラミングシステムがあります。Ocamlもその1つです。Pythonもそうです。Pythonにも、再入不可能なコードがたくさんありますが、グローバルロックを使用してスレッドアクセスをインターリーブします。これらのシステムはリエントラントではなく、スレッドセーフでもコンカレントセーフでもありません。グローバルに同時実行ができないため、安全に動作します。

良い例はmallocです。再入可能ではなく、スレッドセーフでもありません。これは、グローバルリソース(ヒープ)にアクセスする必要があるためです。ロックを使用しても安全にはなりません。再入可能ではありません。mallocへのインターフェースが適切に設計されていれば、再入可能でスレッドセーフにすることができます。

malloc(heap*, size_t);

これで、単一のヒープへの共有アクセスをシリアル化する責任がクライアントに移るので、安全になります。特に、個別のヒープオブジェクトがある場合、作業は必要ありません。共通ヒープが使用されている場合、クライアントはアクセスをシリアル化する必要があります。関数でロックを使用するだけでは十分ではありません。ヒープ*をロックするmallocを検討すると、信号が送信され、同じポインターでmallocが呼び出されます。中断されます。

一般的に言えば、ロックによってスレッドセーフになることはありません。実際、ロックは、クライアントが所有するリソースを不適切に管理しようとすることで安全性を破壊します。ロックはオブジェクトの製造元が行う必要があります。これは、作成されるオブジェクトの数とそれらの使用方法を認識する唯一のコードです。


「したがって、コンカレントセーフ(通常はスレッドセーフで記述)は、再入可能を意味します。」これは、ウィキペディアの「スレッドセーフだが再入可能ではない」のと矛盾します。
Maggyero

3

リストされているポイントの中で「共通のスレッド」(しゃれが意図されています!?)は、関数が同じ関数への再帰呼び出しまたは同時呼び出しの動作に影響を与えるような動作をしてはならないことです。

したがって、たとえば静的データはすべてのスレッドが所有しているため、問題になります。1つの呼び出しで静的変数を変更すると、すべてのスレッドが変更されたデータを使用するため、その動作に影響します。自己修正コード(まれにしか発生せず、場合によっては防止されます)は問題になります。これは、複数のスレッドが存在するにもかかわらず、コードのコピーが1つしかないためです。コードも静的データです。

本質的に再入可能であるために、各スレッドは、それが唯一のユーザーであるかのように関数を使用できる必要があります。これは、あるスレッドが別のスレッドの動作に非決定的に影響を与える可能性がある場合には当てはまりません。主にこれには、関数が機能する個別のデータまたは定数データの各スレッドが含まれます。

そうは言っても、ポイント(1)は必ずしも真ではありません。たとえば、合法的かつ意図的に、静的変数を使用して再帰カウントを保持し、過度の再帰から保護したり、アルゴリズムをプロファイルしたりできます。

スレッドセーフな関数は再入可能である必要はありません。ロックを使用して再入可能性を明確に防止することにより、スレッドの安全性を実現できます。ポイント(6)は、そのような関数は再入可能ではないと述べています。ポイント(6)に関して、ロックするスレッドセーフ関数を呼び出す関数は、再帰での使用には安全ではない(デッドロックになる)ため、再入可能であるとは言えませんが、同時実行性は安全です。複数のスレッドがそのような関数で(ロックされた領域ではなく)同時にプログラムカウンターを持つことができるという意味では、依然として再入可能です。これは、スレッドセーフとリエンタニーを区別するのに役立つかもしれません(または、おそらく混乱を助長します!)。


1

「また」の質問に対する答えは、「いいえ」、「いいえ」、「いいえ」です。関数が再帰的またはスレッドセーフであるからといって、関数が再入可能になるわけではありません。

これらの各タイプの関数は、引用したすべての点で失敗する可能性があります。(私はポイント5について100%確実ではありませんが)。


1

「スレッドセーフ」および「再入可能」という用語は、それらの定義が言うことのみを意味し、正確に意味します。この文脈での「安全」とは以下で引用する定義が言うことします。

ここで「安全」とは、特定のコンテキストで特定の関数を呼び出してもアプリケーションが完全に保護されないという広い意味での安全を意味するものではありません。まとめると、関数はマルチスレッドアプリケーションで確実に望ましい効果を生み出す可能性がありますが、定義によれば、再入可能またはスレッドセーフのいずれにも該当しません。逆に、マルチスレッドアプリケーションで、さまざまな望ましくない、予期しない、または予測できない影響が生じるような方法で、リエントラント関数を呼び出すことができます。

再帰関数は何でもかまいません。リエントラントはスレッドセーフよりも強力な定義を持っているため、番号付きの質問に対する答えはすべてノーです。

リエントラントの定義を読むと、それを変更するために呼び出すもの以外は何も変更しない関数を意味するものとして要約できます。しかし、要約だけに頼るべきではありません。

マルチスレッドプログラミングは、一般的なケースでは非常に困難です。コードのリエントラントのどの部分を知るかは、この課題の一部にすぎません。スレッドセーフティは付加的ではありません。再入可能な関数をつなぎ合わせるのではなく、全体的にスレッドセーフな 設計パターンを使用し、このパターンを使用して、プログラム内のすべてのスレッドと共有リソースの使用をガイドすることをお勧めします。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.