2 つの異なる関数を使用する 2 つのスレッドがあります。1 つ目は最初から最後まで検索し、2 つ目は最後から最後まで検索します。
現在Thread.Sleep(10)
同期に使用していますが、時間がかかりすぎて、そのような状態ではテストできません。
2 つのスレッドを異なる機能で同期するにはどうすればよいですか?
2 つの異なる関数を使用する 2 つのスレッドがあります。1 つ目は最初から最後まで検索し、2 つ目は最後から最後まで検索します。
現在Thread.Sleep(10)
同期に使用していますが、時間がかかりすぎて、そのような状態ではテストできません。
2 つのスレッドを異なる機能で同期するにはどうすればよいですか?
それはあなたが何をしたいかによって少し異なります。
Interlocked
せずにこれを行うために使用します (以下を参照)。Task
なオブジェクトを使用するBarrier
A* 検索を行っていることを考えると、とにかく 2 つまたは 3 つすべての組み合わせが必要になる可能性があります。
Barrier
ステップを調整し、ステップ間のオープンセットを更新するTask
オブジェクトをCancellationToken
使用して、呼び出し元が検索をキャンセルできるようにします。別の回答が提案されましたSemaphore
-これはあなたのニーズにはあまり適していません(以下のコメントを参照)。
Barrier
次の方法で、このような検索に使用できます。
Interlocked
最初の部分は、成功を確認することです。「ロックレス」のままにしたい場合は、Interlocked
これを行うために使用できます。一般的なパターンは次のとおりです。
// global success indicator
private const int NotDone = 0;
private const int AllDone = 1;
private int _allDone = NotDone;
private GeneralSearchFunction(bool directionForward) {
bool iFoundIt = false;
... do some search operations that won't take much time
if (iFoundIt) {
// set _allDone to AllDone!
Interlocked.Exchange(ref _allDone, AllDone);
return;
}
... do more work
// after one or a few iterations, if this thread is still going
// see if another thread has set _allDone to AllDone
if (Interlocked.CompareExchange(ref _allDone, NotDone, NotDone) == AllDone) {
return; // if they did, then exit
}
... loop to the top and carry on working
}
// main thread:
Thread t1 = new Thread(() => GeneralSearchFunction(true));
Thread t2 = new Thread(() => GeneralSearchFunction(false));
t1.Start(); t2.Start(); // start both
t1.Join(); t2.Join();
// when this gets to here, one of them will have succeeded
これは、あらゆる種類の成功トークンまたはキャンセル トークンの一般的なパターンです。
したがって、実装は次のようになります。
class Program
{
// global success indicator
private const int NotDone = 0;
private const int AllDone = 1;
private static int _allDone = NotDone;
private static int _forwardsCount = 0; // counters to simulate a "find"
private static int _backwardsCount = 0; // counters to simulate a "find"
static void Main(string[] args) {
var searchItem = "foo";
Thread t1 = new Thread(() => DoSearchWithBarrier(SearchForwards, searchItem));
Thread t2 = new Thread(() => DoSearchWithBarrier(SearchBackwards, searchItem));
t1.Start(); t2.Start();
t1.Join(); t2.Join();
Console.WriteLine("all done");
}
private static void DoSearchWithBarrier(Func<string, bool> searchMethod, string searchItem) {
while (!searchMethod(searchItem)) {
// after one or a few iterations, if this thread is still going
// see if another thread has set _allDone to AllDone
if (Interlocked.CompareExchange(ref _allDone, NotDone, NotDone) == AllDone) {
return; // if they did, then exit
}
}
Interlocked.Exchange(ref _allDone, AllDone);
}
public static bool SearchForwards(string item) {
// return true if we "found it", false if not
return (Interlocked.Increment(ref _forwardsCount) == 10);
}
public static bool SearchBackwards(string item) {
// return true if we "found it", false if not
return (Interlocked.Increment(ref _backwardsCount) == 20); // make this less than 10 to find it backwards first
}
}
もちろん、これを使用しないと .NET 4.5 にはなりませんTask
。
class Program
{
private static int _forwardsCount = 0; // counters to simulate a "find"
private static int _backwardsCount = 0; // counters to simulate a "find"
static void Main(string[] args) {
var searchItem = "foo";
var tokenSource = new CancellationTokenSource();
var allDone = tokenSource.Token;
Task t1 = Task.Factory.StartNew(() => DoSearchWithBarrier(SearchForwards, searchItem, tokenSource, allDone), allDone);
Task t2 = Task.Factory.StartNew(() => DoSearchWithBarrier(SearchBackwards, searchItem, tokenSource, allDone), allDone);
Task.WaitAll(new[] {t2, t2});
Console.WriteLine("all done");
}
private static void DoSearchWithBarrier(Func<string, bool> searchMethod, string searchItem, CancellationTokenSource tokenSource, CancellationToken allDone) {
while (!searchMethod(searchItem)) {
if (allDone.IsCancellationRequested) {
return;
}
}
tokenSource.Cancel();
}
...
}
ただし、CancellationToken
間違ったことに を使用してしまいました。実際には、これは検索の呼び出し元が検索をキャンセルするために保持する必要があるCancellationToken
ため、要求されたキャンセル (呼び出し元のみが必要tokenSource
とする) を確認するために を使用し、別の成功を確認する必要があります。同期 (Interlocked
上記のサンプルなど) を終了します。
これは多くの理由で難しくなりますが、簡単な方法があります。(.NET 4 の新機能) を終了シグナルと組み合わせて使用Barrier
すると、次のことができます。
達成したい内容に応じて、スレッド同期にはさまざまなアプローチがあります。いくつかは次のとおりです。
Barrier
: 前方検索と後方検索の両方を同時に実行する場合は、おそらくこれが最も適しています。また、「すべてのスレッドがバリアに到達するまで、すべてのスレッドを続行できません」という意図を叫びます。ManualResetEvent
- 1 つのスレッドがシグナルを解放すると、他のすべてのスレッドはシグナルが再設定されるまで続行できます。AutoResetEvent
は似ていますが、再度ブロックする前に 1 つのスレッドのみを続行できる点が異なります。Interlocked
- これと組み合わせることSpinWait
で、実行可能なロックレス ソリューションとなりますSemaphore
- 使用可能ですが、シナリオにはあまり適していませんBarrier
あなたの場合に最も適していると思われるため、ここでは完全なサンプルのみを提供しました。( ref. albahariBarrier
)に次ぐ最もパフォーマンスの高いものの 1 つですが、使用するにはより複雑なコードが必要です。ManualResetEventSlim
ManualResetEvent
上記のいずれも機能しない場合(現在はロックを使用しています)、およびタスクの継続Monitor.Wait
を確認するための他の手法を参照してください。Monitor.Pulse
後者は、ある非同期操作から別の非同期操作にデータを渡すために使用されますが、シナリオには使用できます。また、回答の上部にあるサンプルと同様に、一方を他方の代わりに使用Task
するよりも、組み合わせる可能性が高くなります。Barrier
タスクの継続は、A* 検索でオープン セットのステップ後のリビジョンを実行するために使用できますBarrier
が、とにかく簡単に使用できます。
このコードは、Barrier
作品を使用しています。本質的にDoSearchWithBarrier
は、同期を行う唯一のビットであり、残りはすべてセットアップと破棄のコードです。
class Program {
...
private static int _forwardsCount = 0; // counters to simulate a "find"
private static int _backwardsCount = 0; // counters to simulate a "find"
static void Main(string[] args) {
Barrier barrier = new Barrier(numThreads,
b => Console.WriteLine("Completed search iteration {0}", b.CurrentPhaseNumber));
var searchItem = "foo";
Thread t1 = new Thread(() => DoSearchWithBarrier(SearchForwards, searchItem, barrier));
Thread t2 = new Thread(() => DoSearchWithBarrier(SearchBackwards, searchItem, barrier));
t1.Start(); Console.WriteLine("Started t1");
t2.Start(); Console.WriteLine("Started t2");
t1.Join(); Console.WriteLine("t1 done");
t2.Join(); Console.WriteLine("t2 done");
Console.WriteLine("all done");
}
private static void DoSearchWithBarrier(Func<string, bool> searchMethod, string searchItem, Barrier barrier) {
while (!searchMethod(searchItem)) {
// while we haven't found it, wait for the other thread to catch up
barrier.SignalAndWait(); // check for the other thread AFTER the barrier
if (Interlocked.CompareExchange(ref _allDone, NotDone, NotDone) == AllDone) {
return;
}
}
// set success signal on this thread BEFORE the barrier
Interlocked.Exchange(ref _allDone, AllDone);
// wait for the other thread, and then exit (and it will too)
barrier.SignalAndWait();
}
...
}
ここでは次の 2 つのことが行われています。
Barrier
2 つのスレッドを同期するために使用されるため、他のスレッドが追いつくまで次のステップを実行できません。Interlocked
最初に説明したように、終了シグナルは を使用します。これを A* 検索に実装することは、上記のサンプルと非常によく似ています。すべてのスレッドがバリアに到達して続行すると、ManualResetEvent またはシンプルlock
を使用して、1 つ (および 1 つだけ) にオープン セットを変更させることができます。
Semaphore
リソースのプールが限られており、リソースを所有しているよりも多くのリソース ユーザーがアクセスを必要としている場合に最もよく使用されるため、これはおそらくあなたが望むものではありません。
社員食堂の隅にある CoD を搭載した PlayStation を考えてみてください。コントローラーが 4 つ、20 人WaitOne
が使用を待っRelease
ています。特定の FIFO/LIFO 順序付けが強制されることはなく、実際Release
、避けられない戦いを防ぐために使用する用心棒によって呼び出すことができます (つまり、スレッド ID は強制されません)。
lock
単純な成功表示のための使用ロックでも同じことができます。両方ともInterlocked
、lock
スレッド間の共通変数の読み取りでメモリ キャッシュの問題が発生しないようにします。
private readonly object _syncAllDone = new object();
...
if (iFoundIt) {
lock (_syncAllDone) { _allDone = AllDone };
return;
}
...
// see if another thread has set _allDone to AllDone
lock (_syncAllDone) {
if (_allDone == AllDone) {
return; // if they did, then exit
}
}
これの欠点は、ロックが遅くなる可能性があることですが、状況をテストする必要があります。利点は、ロックを使用してスレッドから結果を書き出すなどの他のことを行う場合、余分なオーバーヘッドがないことです。
ManualResetEvent
単純な成功表示のための使用これは実際にはリセット イベントの使用目的ではありませんが、機能する可能性があります。(.NET 4 以降を使用している場合は、ManualResetEventSlim
代わりに使用しますManualResetEvent
):
private ManualResetEvent _mreAllDone = new ManualResetEvent(true); // will not block a thread
...
if (iFoundIt) {
_mreAllDone.Reset(); // stop other threads proceeding
return;
}
...
// see if another thread has reset _mreAllDone by testing with a 0 timeout
if (!_mreAllDone.WaitOne(0)) {
return; // if they did, then exit
}
他のアプローチはすべて、競合状態や永久にブロックされたスレッドを防ぐために双方向の継続チェックを行う必要があるため、はるかに複雑になります。私はそれらをお勧めしませんので、ここではサンプルを提供しません (長くて複雑です)。
参考文献:
thread.Join()
おそらくあなたの後です。これにより、他のスレッドが終了するまで現在のスレッドがブロックされます。
すべてのスレッドを 1 点に同期することで、そこで複数のスレッドを結合することができます。
List<Thread> threads = new List<Thread>();
threads.Add(new Thread(new ThreadStart(<Actual method here>)));
threads.Add(new Thread(new ThreadStart(<Another method here>)));
threads.Add(new Thread(new ThreadStart(<Another method here>)));
foreach(Thread thread in threads)
{
thread.Start();
}
//All your threads are now running
foreach(Thread thread in threads)
{
thread.Join();
}
//You wont get here until all those threads have finished