2

2 つのボタン (button1button2) とリソース オブジェクト ( r) を持つフォームがあるとします。リソースには、同時実行性を処理するための独自のロックおよびロック解除コードがあります。リソースは、任意のスレッドによって変更される可能性があります。

button1クリックされると、そのハンドラーはそれ自体を何らかの変更を加えてから、生成されたタスクで何らかの変更を行う非同期r呼び出しを行います。これを行う前に のロックを取得します。また、ハンドラーが自分自身をいじっているため、のロックも取得します。_IndependentResourceModifierAsync()r_IndependentResourceModifierAsync()rrr

をクリックすると、直接button2呼び出すだけです。_IndependentResourceModifierAsync()それ自体はロックしません。

ご存知のように、ボタンのハンドラーは常にメイン スレッドで実行されます ( spawned を除くTask)。

保証したいことが2つあります。

  1. リソースがメイン スレッドによってロックされているときに または をクリックすると、例外がスローされますbutton1button2( MonitororMutexはスレッド駆動であるため使用できません)
  2. からのロックのネストによってbutton1_Click()デッド_IndependentResourceModiferAsync()ロックが発生することはありません。(使用できませんSemaphore)。

基本的に、私が探しているのは「スタックベースのロック」であり、そのようなものが存在するか、可能でさえあると思います。非同期メソッドが await 後に続行すると、スタック状態が復元されるためです。私は、この問題を抱えていたが乾いてしまった他の人をたくさん探しました. それはおそらく私が物事を複雑にしすぎていることを意味しますが、人々がそれについて何を言わなければならないのか興味があります. 私が見逃している本当に明白な何かがあるかもしれません。どうもありがとう。

public class Resource
{
    public bool TryLock();
    public void Lock();
    public void Unlock();
    ...
}

public class MainForm : Form
{
    private Resource r;
    private async void button1_Click(object sender, EventArgs e)
    {
        if (!r.TryLock())
            throw InvalidOperationException("Resource already acquired");
        try
        {
            //Mess with r here... then call another procedure that messes with r independently.
            await _IndependentResourceModiferAsync();
        }
        finally
        {
            r.Unlock();
        }
    }

    private async void button2_Click(object sender, EventArgs e)
    {
        await _IndependentResourceModifierAsync();
    }

    private async void _IndependentResourceModiferAsync()
    {
        //This procedure needs to check the lock too because he can be called independently
        if (!r.TryLock())
            throw InvalidOperationException("Resource already acquired");
            try
            {
                await Task.Factory.StartNew(new Action(() => {
                    // Mess around with R for a long time.
                }));
            }
            finally
            {
                r.Unlock();
            }
    }
}
4

3 に答える 3

4

私は、合理的に適切に動作する非同期再入可能ロックは不可能だと考えています。これは、非同期操作を開始するときに、すぐに開始する必要がないためですawait

たとえば、イベント ハンドラーを次のように変更したとします。

private async void button1_Click(object sender, EventArgs e)
{
    if (!r.TryLock())
        throw InvalidOperationException("Resource already acquired");
    try
    {
        var task = _IndependentResourceModiferAsync();
        // Mess with r here
        await task;
    }
    finally
    {
        r.Unlock();
    }
}

ロックが非同期的に再入可能である場合r、イベント ハンドラーで動作するコードと呼び出された非同期メソッドのコードは同時に動作する可能性があります (異なるスレッドで実行できるため)。これは、そのようなロックが安全ではないことを意味します。

于 2013-04-24T14:54:14.483 に答える
4

リソースには、同時実行性を処理するための独自のロックおよびロック解除コードがあります。リソースは、任意のスレッドによって変更される可能性があります。

黄色い旗があります。リソース自体を保護するのではなく、リソースを保護する設計は、通常、長期的には優れていることがわかりました。

button1 がクリックされると、そのハンドラーは r 自体の変更を行い、_IndependentResourceModifierAsync() を非同期的に呼び出します。これにより、生成されたタスクで r の変更が行われます。_IndependentResourceModifierAsync() は、これを行う前に r のロックを取得します。また、ハンドラーは r 自体をいじっているため、r のロックも取得します。

そして、赤い旗があります。再帰ロックは、ほとんどの場合、悪い考えです。ブログで理由を説明しています。

デザインに関して私が拾った別の警告もあります:

リソースがメイン スレッドによってロックされているときに、button1 または button2 のいずれかをクリックすると、例外がスローされます。(モニターやミューテックスはスレッド駆動であるため使用できません)

それは私には正しく聞こえません。これを行う他の方法はありますか?状態が変化したときにボタンを無効にするのは、はるかに優れたアプローチのようです。


ロック再帰の要件を取り除くリファクタリングを強くお勧めします。次に、 with を使用SemaphoreSlimWaitAsyncて非同期にロックを取得しWait(0)、「ロックの試行」を行うことができます。

したがって、コードは次のようになります。

class Resource
{
  private readonly SemaphoreSlim mutex = new SemaphoreSlim(1);

  // Take the lock immediately, throwing an exception if it isn't available.
  public IDisposable ImmediateLock()
  {
    if (!mutex.Wait(0))
      throw new InvalidOperationException("Cannot acquire resource");
    return new AnonymousDisposable(() => mutex.Release());
  }

  // Take the lock asynchronously.
  public async Task<IDisposable> LockAsync()
  {
    await mutex.WaitAsync();
    return new AnonymousDisposable(() => mutex.Release());
  }
}

async void button1Click(..)
{
  using (r.ImmediateLock())
  {
    ... // mess with r
    await _IndependentResourceModiferUnsafeAsync();
  }
}

async void button2Click(..)
{
  using (r.ImmediateLock())
  {
    await _IndependentResourceModiferUnsafeAsync();
  }
}

async Task _IndependentResourceModiferAsync()
{
  using (await r.LockAsync())
  {
    await _IndependentResourceModiferUnsafeAsync();
  }
}

async Task _IndependentResourceModiferUnsafeAsync()
{
  ... // code here assumes it owns the resource lock
}

私は、この問題を抱えていたが乾いてしまった他の人をたくさん探しました. それはおそらく私が物事を複雑にしすぎていることを意味しますが、人々がそれについて何を言わなければならないのか興味があります.

長い間、それは不可能でした (まったく、ピリオド、ピリオド)。.NET 4.5 では可能ですが、きれいではありません。とても複雑です。実際に本番環境でこれを行っている人を私は知りませんし、もちろんお勧めしません。

そうは言っても、私はAsyncEx ライブラリの例として非同期再帰ロックをいじっています (公開 API の一部になることはありません)。次のように使用できます (同期的に動作する既にキャンセルされたトークンの AsyncEx 規則に従います)。

class Resource
{
  private readonly RecursiveAsyncLock mutex = new RecursiveAsyncLock();
  public RecursiveLockAsync.RecursiveLockAwaitable LockAsync(bool immediate = false)
  {
    if (immediate)
      return mutex.LockAsync(new CancellationToken(true));
    return mutex.LockAsync();
  }
}

async void button1Click(..)
{
  using (r.LockAsync(true))
  {
    ... // mess with r
    await _IndependentResourceModiferAsync();
  }
}

async void button2Click(..)
{
  using (r.LockAsync(true))
  {
    await _IndependentResourceModiferAsync();
  }
}

async Task _IndependentResourceModiferAsync()
{
  using (await r.LockAsync())
  {
    ...
  }
}

のコードRecursiveAsyncLockはそれほど長くはありませんが、考えるのは非常に気が遠くなるようなものです。これは、ブログで詳細に説明している暗黙の非同期コンテキスト(それだけでは理解するのが難しい) から始まり、カスタム awaitables を使用して、エンド ユーザーasyncメソッドに適切なタイミングでコードを "挿入" します。

あなたはまさに、誰もが実験したことの限界にいます。RecursiveAsyncLockは完全にテストされておらず、今後もテストされることはないでしょう。

慎重に進みましょう、探検家。ここにドラゴンがいます。

于 2013-04-25T00:18:30.070 に答える
3

私はあなたが見るべきだと思いますSemaphoreSlim(カウントは1):

  • 再入可能ではありません (スレッドによって所有されていません)。
  • 非同期待機をサポートします ( WaitAsync)

今はあなたのシナリオをチェックする時間はありませんが、ぴったりだと思います

編集:私はちょうどこの質問のビットに気づいた:

非同期メソッドが await 後に続行すると、スタック状態が復元されるためです。

いいえ、絶対にありません。これは簡単に表示できます。次のように、ボタンのクリックに応答する非同期メソッドを追加します。

public void HandleClick(object sender, EventArgs e)
{
    Console.WriteLine("Before");
    await Task.Delay(1000);
    Console.WriteLine("After");
}

両方の呼び出しにブレーク ポイントを設定します。 のに、WinForms の「ボタン処理」コードを含むスタック トレースがあることにConsole.WriteLine気付くでしょう。その後、スタックは非常に異なって見えます。await

于 2013-04-24T13:51:15.563 に答える