2016-06-16 10 views
3

私の同僚のいくつかの非同期コードを開発しているうちに、コードの一部で奇妙な問題が発生しました。コンポーネントは継続的なレートでトリガされましたが、一度に複数のタスクを実行することは絶対にありませんでした。同期モードに自動ダウングレードせずに非同期メソッドを呼び出す方法は?

私たちはタスクをキャッシュすることにしましたが、問題はありません。私たちは、キャッシュされたタスクの割り当てを回避し、完了したシングルトンタスクを削除しました。

Task onlyOneTask; 

public Task TryToBeginSomeProcess() 
{ 
    lock (someLock) 
    { 
     if (onlyOneTask == null) 
     { 
      onlyOneTask = DoSomethingButOnlyOneAtATime(); 
     } 

     return onlyOneTask; 
    } 
} 

public async Task DoSomethingButOnlyOneAtATime() 
{ 
    // do some work 

    await SomeWork(); 

    lock (someLock) 
    { 
     onlyOneTask = null; 
    } 
} 

これは実際にはうまくいくようです... SomeWork()コールの中の何かが実際に生じると仮定します。ただし、次のような作業がある場合:

public Task SomeWork() 
{ 
    return Task.FromResult(0); 
} 

失敗します。しかし、それほど魅力的ではありません。 onlyOneTaskをクリアする行は、タスクが割り当てられる前に実行されます!その後では、onlyOneTaskに常にタスクインスタンスがあり、決してその作業を再実行しません。

デッドロックが発生すると考えられますが、threads may reacquire locksと思うかもしれません。非同期コードが完全同期コードにダウングレードする可能性があるため、同じスレッドで2番目のロックが取得されます。デッドロックはありません。

SomeWorkが実行中のある時点で発生すると、期待どおりの動作をしているように見えますが、正確な動作についていくつか質問があります。私は非同期コードが非同期に同期して実行されるときにスレッドとロックがどのように相互作用するかについて正確にはわかりません。

私が指摘したいことは、Task.Runを使用して作業を強制的に実行して独自のタスクを実行するように見えるということです。これには大きな問題があります。このコードは.NETサービスで実行するためのもので、it's bad form to use Task.Run thereです。

これを一見して修正する方法の1つは、非同期実装のようにタスクを常に動作させる何らかの方法がある場合です。私はそれが存在しないと思っていますが、私はよく尋ねるかもしれません。

ので、質問の提示へ:

  1. でき、このシナリオのデッドロックさえSomeWork()収量を場合は?
  2. TaskSchedulerが同じスレッドで継続してSomeWork()が十分に速い場合、非同期のケースは同期ケース(set only前にclearOneTask)のように動作できますか?
  3. スレッドプールを妥協することなく非同期であるかのように既存のタスクを常に動作させる方法はありますか?
  4. もっと良い選択肢はありますか?それは私達がちょうどSomeWorkを(開始するのが適切だったかを決定するために、キャッシュされたタスクの状態を検査した場合、私たちが発見価値がある何のため

)、再び私たちの問題は消えたので、これは自然の中で主に学術的です。

+1

「いつものように動作するタスクを強制的にいくつかの方法非同期の実装を持っていました ":あなたは' 'Task.Yield()'を待ちましたか? ( 'await SomeWork();'の直前) – StriplingWarrior

+0

これまでのところ、私はそうではありませんでした。 –

答えて

4

問題は、awaitが関数に不完全なタスクを返して戻り、lockを終了してタスクを実行することが許可されてしまうという問題があります。あなたは、通常、ロックの内側に待つことによってそれを解決するだろうが、次のコードでは、我々はそれを修正する方法は、1のInitialCountlockからSemaphoreSlimに切り替えて、そのWaitAsync()方法を使用することです

public async Task TryToBeginSomeProcess() 
{ 
    lock (someLock) 
    { 
     if (onlyOneTask == null) 
     { 
      onlyOneTask = DoSomethingButOnlyOneAtATime(); 
     } 
     //does not compile 
     await onlyOneTask; 
    } 
} 

をコンパイルしません。

private SemaphoreSlim someLock = new SemaphoreSlim(1); 

public async Task TryToBeginSomeProcess() 
{ 
    try 
    { 
     await someLock.WaitAsync(); 

     await DoSomethingButOnlyOneAtATime().ConfigureAwait(false); 
    } 
    finally 
    { 
     someLock.Release(); 
    } 
} 

public async Task DoSomethingButOnlyOneAtATime() 
{ 
    // do some work 

    await SomeWork(); 

    // do some more work 
} 

UPDATE:前のコードは、関数を20回呼び出された場合、それは20回の呼び出しからの単一のインスタンスを返し、一度だけのコードを実行します、元の古い動作を複製しませんでした 。私のコードはコードを20回実行しますが、一度に1つのみを実行します。

このソリューションは、実際にはStriplingWarrior's answerの非同期バージョンです。Task.Yeild()での追加を容易にする拡張メソッドも作成します。

SemaphoreSlim someLock = new SemaphoreSlim(1); 
private Task onlyOneTask; 

public async Task TryToBeginSomeProcess() 
{ 
    Task localCopy; 
    try 
    { 
     await someLock.WaitAsync(); 

     if (onlyOneTask == null) 
     { 
      onlyOneTask = DoSomethingButOnlyOneAtATime().YeildImmedeatly(); 
     } 
     localCopy = onlyOneTask; 
    } 
    finally 
    { 
     someLock.Release(); 
    } 

    await localCopy.ConfigureAwait(false); 
} 

public async Task DoSomethingButOnlyOneAtATime() 
{ 
    //Do some synchronous work. 

    await SomeWork().ConfigureAwait(false); 

    try 
    { 
     await someLock.WaitAsync().ConfigureAwait(false); 
     onlyOneTask = null; 
    } 
    finally 
    { 
     someLock.Release(); 
    } 
} 


//elsewhere 
public static class ExtensionMethods 
{ 
    public static async Task YeildImmedeatly(this Task @this) 
    { 
     await Task.Yield(); 
     await @this.ConfigureAwait(false); 
    } 
    public static async Task<T> YeildImmedeatly<T>(this Task<T> @this) 
    { 
     await Task.Yield(); 
     return await @this.ConfigureAwait(false); 
    } 
} 
+0

私はそれで完全に冷静です。このことを学ぶのが大好きです。 – StriplingWarrior

+0

これは上記コードサンプルと全く同じ機能ではありません(正しく読んでいる場合)。質問の例で、TryToBeginSomeProcessを100回呼び出すと、DoSomethingButOnlyOneAtATimeが完了する前にすべてが呼び出されますが、DoSomethingを1回だけ実行します。あなたの例では、私はいつもDoSomethingを100回呼びます。それを言って、私はこの問題を整理することができるように思われるSemaphoreSlimのこの使用法を認識していませんでした。 –

+0

@BillNadeauあなたは正しいです、私は完全に動作を再作成しませんでした。私には分があり、私はそれを微調整することができるかもしれません。 –

3

おそらく、あなたのやりたいことを行うには、スコットの答えが最適です。しかし、asyncメソッドを呼び出すと、彼らは同期返されないことを確実にする方法についての質問への一般的な答えとして、便利な小さな方法がTask.Yield()そこに呼ばれています:

public async Task DoSomethingButOnlyOneAtATime() 
{ 
    // Make sure that the rest of this method runs after the 
    // current synchronous context has been resolved. 
    await Task.Yield(); 
    // do some work 
    await SomeWork(); 

    lock (someLock) 
    { 
     onlyOneTask = null; 
    } 
} 
+0

それは私が完全に見落とした信じられないほど便利な小さな機能です。ニッチ、おそらく、まだ便利です! –

関連する問題