如何在处理异常时异步延迟所有将来的调用

时间:2017-02-11 18:04:56

标签: c# .net multithreading asynchronous async-await

我有一个使用有限资源的异步方法。如果资源不可用,我想延迟将来的呼叫,直到资源再次可用。

基本上,如果在访问有限资源时发生异常,则单个线程将通过延迟也捕获异常的所有线程以及调用该方法的其他线程来处理错误。在5秒后,线程将重试访问资源。这有点像节流。

我通过滥用 TaskCompletionSource并结合SemaphoreSlim来实现此目的。它似乎工作。这可以改善,以减少... hacky?

// Use SemaphoreSlim to make sure only one thread handles an error at a time.
private static readonly SemaphoreSlim mySemaphore = new SemaphoreSlim(1);

// Use TaskCompletionSource as a flag to delay threads while an error is handled.
private static volatile TaskCompletionSource<bool> myFlag;

static MyClass()
{
    myFlag = new TaskCompletionSource<bool>();
    myFlag.SetResult(false); // At startup there is no error being handled.
}

public async Task DoSomethingAsync()
{
    while (true)
    {
        await myFlag.Task; // Wait if an error is being handled.

        try
        {
            await ... // Call some asynchronous operations here that can cause errors.
            return;
        }
        catch
        {
            await mySemaphore.WaitAsync(); // Wait so only one thread handles an error.
            bool wasHandled = await myFlag.Task; // Wait and check if error was handled.

            if (wasHandled == false)
            {
                // Reset TaskCompletionSource so error handling on other threads waits.
                myFlag = new TaskCompletionSource<bool>();
                mySemaphore.Release();

                await Task.Delay(5000); // "Handle" the error by waiting 5 seconds.
                myFlag.SetResult(true); // Notify waiting threads an error was handled.

                // Reset TaskCompletionSource 
                myFlag = new TaskCompletionSource<bool>();
                myFlag.SetResult(false);
            }
            else // (wasHandled == true)
            {
                mySemaphore.Release(); // Move along, nothing to see here.
            }
        }
    }
}

澄清我认为应该改进的原因:我使用TaskCompletionSource来创建一个等待的布尔状态,并且要重置它,我必须每次实例化一个新的TaskCompletionSource。我不认为这是TaskCompletionSource的预期用途。

我看过ManualResetEventAutoResetEvent因为它们似乎做了我需要的事情,但它们不提供异步功能。

2 个答案:

答案 0 :(得分:2)

这里有一些想法,包含代码示例。但是,您真的应该调查Circuit Breaker模式,以便您可以自己实现它或获得有效的实现。

正如您现在所知,重置事件实现可能对您有用,因为AutoResetEvent涵盖了逐个处理异常的情况(因为它只允许一个线程运行它),并且{{ 3}}覆盖正常执行时运行所有线程的情况:

var manual = new ManualResetEventSlim(true);
var auto = new AutoResetEvent(true);
while (true)
{
    // check for normal work
    manual.Wait();
    try
    {
    }
    catch
    {
        auto.Wait();
        // only one thread here
        // stop all the worker threads
        manual.Reset();
        // handling here

        // start all the workers
        manual.Set();
        // start exception handlers
        auto.Set();
    }
}

请注意,您可以模拟ManualResetEventSlim(就像您已经做过的那样)。您应该使用SemaforSlim(1, 1)构造函数,以确保1线程能够继续,并且最初设置semafor。

尽管如此,这个版本是非同步的,所以你在这里有一些选择。

  1. slim version for auto event with SemaforSlim,还有一个Semafor methods could be awaited
  2. 此外,您可以使用一些具有超时参数的重试逻辑来等待循环。在这种情况下,您可以使用AsyncManualResetEvent implementation from @StephenCleary方法yield执行时间,或者,如果要设置重试超时,只需使用TPL中的Thread.Yield

    while (!manual.Wait(SMALL_TIMEOUT_FOR_WAITING)
    {
        // Thread.Yield();
        await Task.Delay(LARGE_RETRY_TIMEOUT);
    }
    
  3. 您还应该注意,对于您的循环和面向任务的方法来说Delay task是一种常见的做法,因此在停止整个系统的情况下,您的代码会得到更多的管理。

答案 1 :(得分:0)

如果我理解你的情况:消费者 - &gt;您的服务 - &gt;另一项服务/资源

您不应该在服务中实施退避策略,因为最终客户端正在等待该行的另一端。让客户实现后退,您的服务只需要实现断路器,甚至是可选的。

因此...

服务方面:

你应该有一个私人工作者方法(例如DoSomething),它只会照顾“做某事”&#39;。它应该包含在一个实现Circuit Breaker pattern的公共方法中。

在&#39; 消费者&#39;侧:

您服务的消费者&#39; (公共方法)应该实现Exponential Backoff Strategy