4


3

C#のスレッドセーフな非同期コード

数週間前に以下の質問をしました。 さて、私の質問とすべての回答を確認すると、非常に重要な詳細が目に飛び込んできました。2番目のコード例では、メイン(UI)スレッドで `DoTheCodeThatNeedsToRunAsynchronously()`が実行されていませんか? タイマーは1秒待ってからメインスレッドにイベントをポストしませんか? これは、非同期に実行する必要があるコードがまったく非同期に実行されないことを意味しますか?!

元の質問:

'' '' '

私は最近何度か問題に直面し、さまざまな方法でそれを解決しました。スレッドセーフかどうかは常に不確かでした。C#コードを非同期に実行する必要があります。 (編集:.NET 3.5を使用していることを忘れていました!

そのコードは、メインスレッドコードによって提供されるオブジェクトに対して機能します。 *(編集:オブジェクトはそれ自体がスレッドセーフであると仮定しましょう。)*私が試した2つの方法(簡略化)を紹介し、*これらの4つの質問*を示します:

  1. 私が望むものを達成するための最良の方法は何ですか? それは2つのうちの1つですか 別のアプローチ?

  2. スレッドセーフではない2つの方法のいずれか(両方とも恐れています…​)とその理由は?

  3. 最初のアプローチは、スレッドを作成し、それにオブジェクトを渡します コンストラクタ。 それは私がオブジェクトを渡すことになっている方法ですか?

  4. 2番目のアプローチは、それを提供しないタイマーを使用します 可能性があるため、匿名デリゲートでローカル変数を使用します。 それは安全ですか、またはデリゲートコードによって評価される前に変数の参照が変更されることは理論上可能ですか? (これは、匿名のデリゲートを使用する場合の非常に一般的な質問です)。 Javaでは、ローカル変数を* final *(つまり、 一度割り当てたら変更できません)。 C#ではそのような可能性はありません、ありますか?

'' '' '

アプローチ1:スレッド

new Thread(new ParameterizedThreadStart(
    delegate(object parameter)
    {
        Thread.Sleep(1000); // wait a second (for a specific reason)

        MyObject myObject = (MyObject)parameter;

        DoTheCodeThatNeedsToRunAsynchronously();
        myObject.ChangeSomeProperty();

    })).Start(this.MyObject);

このアプローチには1つの問題があります。メインスレッドがクラッシュする可能性がありますが、ゾンビスレッドが原因でプロセスは引き続きメモリに残ります。

'' '' '

アプローチ2:タイマー

MyObject myObject = this.MyObject;

System.Timers.Timer timer = new System.Timers.Timer();
timer.Interval = 1000;
timer.AutoReset = false; // i.e. only run the timer once.
timer.Elapsed += new System.Timers.ElapsedEventHandler(
    delegate(object sender, System.Timers.ElapsedEventArgs e)
    {
        DoTheCodeThatNeedsToRunAsynchronously();
        myObject.ChangeSomeProperty();
    });

DoSomeStuff();
myObject = that.MyObject; // hypothetical second assignment.

ローカル変数 myObject`は、私が質問4で話していることです。 例として2番目の割り当てを追加しました。 タイマーが2番目の割り当ての* after *を経過すると、デリゲートコードは `this.MyObject`または that.MyObject`で動作しますか?

7 Answer


5


これらのコードのいずれかが安全かどうかは、 `MyObject`インスタンスの構造に関係しています。 どちらの場合も、フォアグラウンドスレッドとバックグラウンドスレッドの間で `myObject`変数を共有しています。 バックグラウンドスレッドの実行中に、フォアグラウンドスレッドが `myObject`を変更するのを止めるものは何もありません。

これは安全である場合とそうでない場合があり、 `MyObject`の構造に依存します。 ただし、特にその計画を立てていない場合、それは間違いなく安全でない操作です。


4


`Task`オブジェクトを使用し、コードを再構築して、バックグラウンドタスクが何らかの共有状態を変更するのではなく、その計算値を「返す」ことを推奨します。

バックグラウンドタスクへの5つの異なるアプローチ( Task、` BackgroundWorker`、 Delegate。 BeginInvokeThreadPool.QueueUserWorkItem、および` Thread`)、それぞれの長所と短所。

具体的にあなたの質問に答えるには:

  1. _私が望むものを達成する最良の方法は何ですか? それは2つのうちの1つですか 別のアプローチ?_最良の解決策は、特定の「スレッド」またはタイマーコールバックの代わりに「タスク」オブジェクトを使用することです。 すべての理由については私のブログ投稿をご覧ください。しかし、要約すると: `Task`はhttp://msdn.microsoft.com/en-us/library/dd537613.aspx [結果を返す]、http://msdn.microsoftをサポートしています.com / en-us / library / ee372288.aspx [完了時のコールバック]、http://msdn.microsoft.com/en-us/library/dd997415.aspx [適切なエラー処理]、およびhttp:/との統合.NETの/msdn.microsoft.com/en-us/library/dd997364.aspx [ユニバーサルキャンセルシステム]。

  2. スレッドセーフではない2つの方法の1つ(両方を恐れる…​)とその理由は? As 他の人が述べているように、これは「MyObject.ChangeSomeProperty」がスレッドセーフかどうかに完全に依存しています。 非同期システムを扱う場合、各非同期操作が共有状態を変更せず、_result_を返す場合、スレッド安全性について簡単に推論できます。

  3. _最初のアプローチはスレッドを作成し、それにオブジェクトを渡します コンストラクタ。 それは私がオブジェクトを渡すことになっているのですか?_個人的には、ラムダバインディングを使用する方が好きです。

  4. _2番目のアプローチは、それを提供しないタイマーを使用します 可能性があるため、匿名デリゲートでローカル変数を使用します。 それは安全ですか、理論的には変数の参照がデリゲートコードによって評価される前に変更される可能性がありますか?デリゲートによって使用される前。 参照が変更される可能性がある場合、通常の解決策は、ラムダ式でのみ使用される個別のローカル変数を作成することです。

など:

MyObject myObject = this.MyObject;
...
timer.AutoReset = false; // i.e. only run the timer once.
var localMyObject = myObject; // copy for lambda
timer.Elapsed += new System.Timers.ElapsedEventHandler(
  delegate(object sender, System.Timers.ElapsedEventArgs e)
  {
    DoTheCodeThatNeedsToRunAsynchronously();
    localMyObject.ChangeSomeProperty();
  });
// Now myObject can change without affecting timer.Elapsed

ReSharperなどのツールは、ラムダにバインドされたローカル変数が変化する可能性があるかどうかを検出しようとし、この状況を検出すると警告します。

私の推奨ソリューション( `Task`を使用)は次のようになります。

var ui = TaskScheduler.FromCurrentSynchronizationContext();
var localMyObject = this.myObject;
Task.Factory.StartNew(() =>
{
  // Run asynchronously on a ThreadPool thread.
  Thread.Sleep(1000); // TODO: review if you *really* need this

  return DoTheCodeThatNeedsToRunAsynchronously();
}).ContinueWith(task =>
{
  // Run on the UI thread when the ThreadPool thread returns a result.
  if (task.IsFaulted)
  {
    // Do some error handling with task.Exception
  }
  else
  {
    localMyObject.ChangeSomeProperty(task.Result);
  }
}, ui);

UIスレッドは `MyObject.ChangeSomeProperty`を呼び出すものなので、そのメソッドはスレッドセーフである必要はありません。 もちろん、「DoTheCodeThatNeedsToRunAsynchronously」はまだスレッドセーフである必要があります。


3


最初の試みはかなり良いですが、 IsBackground`プロパティを true`に設定しなかったため、アプリケーションが終了した後もスレッドは存在し続けました…​ コードの単純化(および改善)バージョンを以下に示します。

MyObject myObject = this.MyObject;
Thread t = new Thread(()=>
    {
        Thread.Sleep(1000); // wait a second (for a specific reason)
        DoTheCodeThatNeedsToRunAsynchronously();
        myObject.ChangeSomeProperty();
    });
t.IsBackground = true;
t.Start();

スレッドの安全性に関しては、複数のスレッドが同時に実行されたときにプログラムが正しく機能するかどうかを判断するのは困難です。なぜなら、この例では競合のポイントを示していないからです。 プログラムが `MyObject`で競合している場合、同時実行の問題が発生する可能性が非常に高くなります。

Javaには「final」キーワードがあり、C#には「readonly」と呼ばれる対応するキーワードがありますが、「final」も「readonly」も、変更するオブジェクトの状態がスレッド間で一貫していることを保証しません。 これらのキーワードが行う唯一のことは、オブジェクトが指している参照を変更しないようにすることです。 2つのスレッドが同じオブジェクトで読み取り/書き込みの競合を起こしている場合、スレッドの安全性を確保するために、そのオブジェクトで何らかのタイプの同期またはアトミック操作を実行する必要があります。

更新

OK、 myObject`が指している参照を変更すると、競合は myObject`になります。 私の答えはあなたの実際の状況と100%一致しないと確信していますが、あなたが提供したサンプルコードを考えると、何が起こるかを伝えることができます:

*どのオブジェクトが変更されるかは保証されません:*それは that.MyObject`または this.MyObject`です。 JavaとC#のどちらを使用しているかに関係なく、それは事実です。 スケジューラは、2番目の割り当ての前、後、または実行中にスレッド/タイマーを実行するようにスケジュールできます。 特定の実行順序を頼りにしている場合、その実行順序を保証するために_何かをする必要があります。 通常、その「何か」は、「ManualResetEvent」、「Join」などの信号の形式でのスレッド間の通信です。

結合の例を次に示します。

MyObject myObject = this.MyObject;
Thread task = new Thread(()=>
    {
        Thread.Sleep(1000); // wait a second (for a specific reason)
        DoTheCodeThatNeedsToRunAsynchronously();
        myObject.ChangeSomeProperty();
    });
task.IsBackground = true;
task.Start();
task.Join(); // blocks the main thread until the task thread is finished
myObject = that.MyObject; // the assignment will happen after the task is complete

以下は `ManualResetEvent`の例です:

ManualResetEvent done = new ManualResetEvent(false);
MyObject myObject = this.MyObject;
Thread task = new Thread(()=>
    {
        Thread.Sleep(1000); // wait a second (for a specific reason)
        DoTheCodeThatNeedsToRunAsynchronously();
        myObject.ChangeSomeProperty();
        done.Set();
    });
task.IsBackground = true;
task.Start();
done.WaitOne(); // blocks the main thread until the task thread signals it's done
myObject = that.MyObject; // the assignment will happen after the task is done

もちろん、この場合、複数のスレッドを同時に生成することは無意味です。なぜなら、それらを同時に実行することを許可しないからです。 これを回避する1つの方法は、スレッドを開始した後に「myObject」への参照を変更しないことです。そうすると、「ManualResetEvent」で「Join」や「WaitOne」を使用する必要がなくなります。

だからこれは私に質問につながります:なぜ新しいオブジェクトを `myObject`に割り当てるのですか? これは、複数の非同期タスクを実行するために複数のスレッドを開始するforループの一部ですか?


3


「スレッドセーフ」は扱いにくい獣です。 どちらのアプローチでも、問題は、スレッドが使用している「MyObject」が複数のスレッドによって変更/読み取りされ、状態が矛盾したように見える、または実際の状態と矛盾するようにスレッドが動作する可能性があることです。

たとえば、 `MyObject.DoSomethingElse()`の前に `MyObject.ChangeSomeproperty()`を呼び出さなければならない、またはスローします。 どちらのアプローチでも、 `ChangeSomeProperty()`を呼び出すスレッドが終了する前に、他のスレッドが `DoSomethingElse()`を呼び出すのを止めるものは何もありません。

または、 `ChangeSomeProperty()`が2つのスレッドによって呼び出され、(内部的に)状態が変更された場合、最初のスレッドが作業中にスレッドコンテキストの切り替えが行われ、最終結果は実際の新しい両方のスレッドが「間違っている」状態。

ただし、どちらのアプローチも本質的にスレッドセーフではないため、状態の変化がシリアル化され、状態へのアクセスが常に一貫した結果をもたらすことを確認する必要があります。

個人的には、2番目のアプローチは使用しません。 「ゾンビ」スレッドに問題がある場合は、スレッドで `IsBackground`をtrueに設定します。


2


_ 私が望むものを達成するための最良の方法は何ですか? それは2つまたは別のアプローチの1つですか? _

どちらも正常に見えますが、…​

_ 2つの方法の1つはスレッドセーフではありませんか(私は両方を恐れています…​)、なぜですか? _

…​they are not thread safe unless MyObject.ChangeSomeProperty() is スレッドセーフ。

_ 最初のアプローチでは、スレッドを作成し、コンストラクターでオブジェクトを渡します。 それは私がオブジェクトを渡すことになっている方法ですか? _

Yes. (2番目のアプローチのように)クロージャーを使用しても問題ありませんが、キャストを行う必要がないという利点もあります。

_ 2番目のアプローチでは、その可能性を提供しないタイマーを使用するため、匿名デリゲートでローカル変数を使用します。 それは安全ですか、またはデリゲートコードによって評価される前に変数の参照が変更されることは理論上可能ですか? (これは、匿名のデリゲートを使用する場合の非常に一般的な質問です)。 _

もちろん、「timer.Elapsed」を設定した直後に「myObject = null;」を追加すると、スレッド内のコードは失敗します。 しかし、なぜそれをしたいのですか? `this.MyObject`を変更しても、スレッドにキャプチャされた変数には影響しないことに注意してください。

'' '' '

だから、このスレッドセーフにする方法は? 問題は、 `myObject.ChangeSomeProperty();`が `myObject`の状態を変更する他のコードと並行して実行される可能性があることです。 それには基本的に2つの解決策があります:

*オプション1 *:メインUI theadで myObject.ChangeSomeProperty()`を実行します。 これは、 `ChangeSomeProperty`が高速な場合の最も簡単なソリューションです。 `Dispatcher(WPF)または` Control.Invoke`(WinForms)を使用してUIスレッドに戻ることができますが、最も簡単な方法は `BackgroundWorker`を使用することです:

MyObject myObject = this.MyObject;
var bw = new BackgroundWorker();

bw.DoWork += (sender, args) => {
    // this will happen in a separate thread
    Thread.Sleep(1000);
    DoTheCodeThatNeedsToRunAsynchronously();
}

bw.RunWorkerCompleted += (sender, args) => {
    // We are back in the UI thread here.

    if (args.Error != null)  // if an exception occurred during DoWork,
        MessageBox.Show(args.Error.ToString());  // do your error handling here
    else
        myObject.ChangeSomeProperty();
}

bw.RunWorkerAsync(); // start the background worker

*オプション2 *: lock`キーワードを使用して、 ChangeSomeProperty() のコードをスレッドセーフにします( ChangeSomeProperty`内、および同じバッキングフィールドを変更または読み取る他のメソッド内)。


2


ここでのより大きなスレッド安全性の懸念は、私の考えでは、1秒のスリープかもしれません。 他の操作と同期するためにこれが必要な場合(完了までの時間を与える)、スリープに依存するのではなく、適切な同期パターンを使用することを強くお勧めします。 Monitor.Pulseまたはhttp://msdn.microsoft.com/en -us / library / system.threading.autoresetevent.aspx [AutoResetEvent]は、同期を実現する2つの一般的な方法です。 微妙な競合状態が発生しやすいため、両方を慎重に使用する必要があります。 ただし、同期にスリープを使用することは、発生を待機している競合状態です。

また、スレッドを使用する場合(および.NET 4.0のタスク並列ライブラリにアクセスできない場合)、http://msdn.microsoft.com/en-us/library/system.threading.threadpoolを使用します。 queueuserworkitem.aspx [ThreadPool.QueueUserWorkItem]は、短時間のタスクに適しています。 また、スレッドプールスレッドは、バックグラウンド以外のスレッドの停止を妨げるデッドロックがない限り、アプリケーションが停止してもハングアップしません。


1


  • これまで言及されていなかったことが1つあります: *スレッド化メソッドの選択は、特に `DoTheCodeThatNeedsToRunAsynchronously()`の機能に大きく依存します。

さまざまな要件には、さまざまな.NETスレッド化アプローチが適しています。 非常に大きな懸念事項の1つは、この方法がすぐに完了するか、しばらく時間がかかるか(短命ですか、それとも長時間かかるか)です。

`ThreadPool.QueueUserWorkItem()`のようないくつかの.NETスレッド化メカニズムは、短命のスレッドで使用するためのものです。 「リサイクルされた」スレッドを使用することでスレッドを作成する費用を回避しますが、リサイクルするスレッドの数は制限されているため、長時間実行されるタスクがThreadPoolのスレッドを占有することはありません。

考慮すべき他のオプションは以下を使用しています:

  • `ThreadPool.QueueUserWorkItem()`は便利な手段です ThreadPoolスレッドで小さなタスクを実行して忘れる

  • `System.Threading.Tasks.Task`は.NET 4の新機能であり、 非同期/並列モードで簡単に実行できる小さなタスク。

  • Delegate.BeginInvoke()`および `Delegate.EndInvoke()BeginInvoke() コードを非同期で実行しますが、潜在的なリソースリークを回避するために、 `EndInvoke()`が呼び出されることを確認することが重要です。 私が信じている「ThreadPool」スレッドにも基づいています。

  • 例に示すように、 System.Threading.Thread。 スレッドが提供する 最も制御しやすいが、他の方法よりも高価であるため、長時間実行されるタスクや詳細指向のマルチスレッドに最適です。

全体的に私の個人的な好みは、 `Delegate.BeginInvoke()/ EndInvoke()`を使用することでした-制御と使いやすさのバランスが取れているようです。