web-dev-qa-db-ja.com

SemaphoreSlim(.NET)は同じスレッドがブロックに入らないようにしますか?

私はSemaphoreSlimのドキュメントを読みました SemaphoreSlim MSDN これは、次のように構成した場合、SemaphoreSlimが一度に実行するコードのセクションを1つのスレッドのみに制限することを示しています。

SemaphoreSlim _semaphoreSlim = new SemaphoreSlim(1, 1);

ただし、sameスレッドがそのコードにアクセスするのを停止するかどうかは示しません。これは非同期で発生し、待機します。メソッドでawaitを使用すると、コントロールはそのメソッドを離れ、タスクまたはスレッドが完了したときに戻ります。私の例では、非同期ボタンハンドラーを備えたボタンを使用しています。 'await'で別のメソッド(Function1)を呼び出します。次にFunction1が呼び出します

await Task.Run(() => Function2(beginCounter));

私のTask.Run()の周りにSemaphoreSlimがあります。それは確かにそれが同じスレッドがFunction2に到達するのを止めるようです。しかし、これはドキュメントから(私が読んだように)保証されていません。

以下の完全な例を投稿しました。

おかげで、

デイブ

 using System;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Windows;

 namespace AsynchAwaitExample
 {
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
    private readonly SemaphoreSlim _semaphoreSlim = new SemaphoreSlim(1, 1);
    public MainWindow()
    {
        InitializeComponent();
    }

    static int beginCounter = 0;
    static int endCounter = 0;
    /// <summary>
    /// Suggest hitting button 3 times in rapid succession
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    private async void button_Click(object sender, RoutedEventArgs e)
    {
        beginCounter++;
        endCounter++;
        // Notice that if you click fast, you'll get all the beginCounters first, then the endCounters
        Console.WriteLine("beginCounter: " + beginCounter + " threadId: " + Thread.CurrentThread.ManagedThreadId);
        await Function1(beginCounter);
        Console.WriteLine("endCounter: " + endCounter + " threadId: " + Thread.CurrentThread.ManagedThreadId);
    }

    private async Task Function1(int beginCounter)
    {
        try
        {
            Console.WriteLine("about to grab lock" + " threadId: " + Thread.CurrentThread.ManagedThreadId + " beginCounter: " + beginCounter);
            await _semaphoreSlim.WaitAsync();  // get rid of _semaphoreSlim calls and you'll get into beginning of Function2 3 times before exiting
            Console.WriteLine("grabbed lock" + " threadId: " + Thread.CurrentThread.ManagedThreadId + " beginCounter: " + beginCounter);
            await Task.Run(() => Function2(beginCounter));
        }
        finally
        {
            Console.WriteLine("about to release lock" + " threadId: " + Thread.CurrentThread.ManagedThreadId + " beginCounter: " + beginCounter);
            _semaphoreSlim.Release();
            Console.WriteLine("released lock" + " threadId: " + Thread.CurrentThread.ManagedThreadId + " beginCounter: " + beginCounter);
        }

    }

    private void Function2(int beginCounter)
    {
        Console.WriteLine("Function2 start" + " threadId: " + Thread.CurrentThread.ManagedThreadId + " beginCounter: " + beginCounter);
        Thread.Sleep(1000);
        Console.WriteLine("Function2 end" + " threadId: " + Thread.CurrentThread.ManagedThreadId + " beginCounter: " + beginCounter);
        return;
    }
}
}

ボタンを3回クリックした場合の出力例。 Function2は、再び開始する前に、常に特定のカウンターに対して終了することに注意してください。

    beginCounter: 1 threadId: 9
about to grab lock threadId: 9 beginCounter: 1
grabbed lock threadId: 9 beginCounter: 1
Function2 start threadId: 13 beginCounter: 1
beginCounter: 2 threadId: 9
about to grab lock threadId: 9 beginCounter: 2
beginCounter: 3 threadId: 9
about to grab lock threadId: 9 beginCounter: 3
Function2 end threadId: 13 beginCounter: 1
about to release lock threadId: 9 beginCounter: 1
released lock threadId: 9 beginCounter: 1
grabbed lock threadId: 9 beginCounter: 2
Function2 start threadId: 13 beginCounter: 2
endCounter: 3 threadId: 9
Function2 end threadId: 13 beginCounter: 2
about to release lock threadId: 9 beginCounter: 2
released lock threadId: 9 beginCounter: 2
endCounter: 3 threadId: 9
grabbed lock threadId: 9 beginCounter: 3
Function2 start threadId: 13 beginCounter: 3
Function2 end threadId: 13 beginCounter: 3
about to release lock threadId: 9 beginCounter: 3
released lock threadId: 9 beginCounter: 3
endCounter: 3 threadId: 9

SemaphoreSlim呼び出しを取り除くと、次のようになります。

beginCounter: 1 threadId: 10
about to grab lock threadId: 10 beginCounter: 1
grabbed lock threadId: 10 beginCounter: 1
Function2 start threadId: 13 beginCounter: 1
beginCounter: 2 threadId: 10
about to grab lock threadId: 10 beginCounter: 2
grabbed lock threadId: 10 beginCounter: 2
Function2 start threadId: 14 beginCounter: 2
beginCounter: 3 threadId: 10
about to grab lock threadId: 10 beginCounter: 3
grabbed lock threadId: 10 beginCounter: 3
Function2 start threadId: 15 beginCounter: 3
Function2 end threadId: 13 beginCounter: 1
about to release lock threadId: 10 beginCounter: 1
released lock threadId: 10 beginCounter: 1
endCounter: 3 threadId: 10
Function2 end threadId: 14 beginCounter: 2
about to release lock threadId: 10 beginCounter: 2
released lock threadId: 10 beginCounter: 2
endCounter: 3 threadId: 10
18
Dave

ドキュメント から:

SemaphoreSlimクラスは、Wait、WaitAsync、Releaseメソッドの呼び出しにスレッドまたはタスクIDを適用しません。

言い換えると、クラスはどのスレッドがそれを呼び出しているかを確認しません。単なるカウンターです。同じスレッドがセマフォを複数回取得できるため、複数のスレッドがセマフォを取得した場合と同じになります。残りのスレッド数が0になっている場合、そのスレッドがセマフォを獲得したスレッドである場合でも、Wait()を呼び出すと、他のスレッドがセマフォを解放するまでブロックされます。

したがって、async/awaitに関しては、awaitが開始されたのと同じスレッドで再開するかどうかは関係ありません。 Wait()Release()の呼び出しのバランスを保つ限り、期待どおりに機能します。

あなたの例では、セマフォを非同期で待機しているため、どのスレッドもブロックしていません。そうしないと、2回目にボタンを押したときにUIスレッドをデッドロックしてしまうからです。


関連資料:
メインスレッドの反復間のリソースロック(非同期/待機)
このコードがデッドロックで終了しないのはなぜですか
ネストされた非同期呼び出しによるロック

特にasync/awaitを使用する場合は、再入可能/再帰的ロックについて特に注意してください。スレッドの同期はそのままでは十分にトリッキーであり、その難しさがasync/awaitを簡素化するように設計されています。そして、それはほとんどの場合それを大幅に行います。しかし、それをさらに別の同期/ロックメカニズムと組み合わせる場合はそうではありません。

19
Peter Duniho