【问题标题】:How to invoke async methods without automatic downgrade to synchronous mode?如何在不自动降级到同步模式的情况下调用异步方法?
【发布时间】:2016-10-18 15:18:27
【问题描述】:

在开发一些异步代码时,我和我的同事在部分代码中遇到了一个奇怪的问题。一个组件以连续的速率触发,但我们希望它调用的其中一个依赖项一次永远不会执行多个任务。

我们决定只缓存任务,没问题。我们对缓存任务的分配进行了锁定,并让单例任务在完成后自行删除。

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,并且由于异步代码可能会将自身降级为完全同步代码,因此第二个锁是在同一个线程上获取的。没有死锁。

如果 SomeWork 在其执行的某个时刻产生了结果,那么它的行为似乎符合我们的预期,但它确实引发了一些关于其确切行为的问题。我不确定当异步代码同步执行与异步执行时线程和锁如何交互的确切细节。

我想指出的另一件事是,这似乎可以通过使用 Task.Run 来解决,以强制工作在自己的任务上运行。这有一个主要问题:此代码旨在在 .NET 服务中运行,而it's bad form to use Task.Run there

一种看似解决此问题的方法是,是否有某种方法可以强制任务始终表现得像它具有异步实现一样。我想它不存在,但我不妨问问。

所以,关于这个问题:

  1. 即使 SomeWork() 产生,这种情况也会死锁吗?
  2. 如果 TaskScheduler 在同一个线程上继续运行并且 SomeWork() 足够快,那么异步情况能否像同步情况一样(在设置之前清除 onlyOneTask)?
  3. 有没有办法强制现有任务始终表现得好像它是异步的,而不会影响线程池?
  4. 有更好的选择吗?

如果我们只是检查缓存任务的状态以确定何时适合再次启动 SomeWork(),那么我们发现的价值是什么,我们的问题就消失了,所以这主要是学术性的。

【问题讨论】:

  • “某种方式来强制一个任务总是表现得像一个异步实现”:你试过await Task.Yield()吗? (就在await SomeWork(); 之前)
  • 直到现在我还没有,看来应该可以了!

标签: c# .net task


【解决方案1】:

问题是当await 命中你的函数时返回不完整的任务并退出lock 允许更多的东西运行任务。你通常会通过在锁内等待来解决它,但是下面的代码不会编译

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

我们修复它的方法是从lock 切换到SemaphoreSlimInitialCount 为 1,并使用其WaitAsync() 方法。

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
}

更新: 前面的代码没有复制旧的行为,如果您调用该函数 20 次原始代码,它将从 20 次调用中返回单个实例,并且只运行一次代码。我的代码将运行代码 20 次,但一次只能运行一次。

此解决方案实际上只是 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);
    }
}

【讨论】:

  • 这与上面的代码示例的功能不太一样(如果我没看错的话)。在问题的示例中,如果我调用 TryToBeginSomeProcess 100 次,但所有这些都在 DoSomethingButOnlyOneAtATime 完成之前被调用,我将只执行一次 DoSomething。在您的示例中,我将始终调用 DoSomething 100 次。话虽如此,我并不知道 SemaphoreSlim 的这种用法,它似乎能够解决问题。
  • @BillNadeau 你是对的,我完全没有重新创造这种行为。给我一分钟,我也许可以调整它。
  • @BillNadeau 确保您获得最新版本,我有一段时间的版本存在一些潜在的竞争条件问题。
【解决方案2】:

Scott 的回答可能是做您想做的事情的最佳方式。但是,作为对如何调用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;
    }
}

【讨论】:

  • 这是一个我完全忽略的非常有用的小功能。利基,也许,但仍然有用!
猜你喜欢
  • 2018-03-11
  • 1970-01-01
  • 2021-12-19
  • 1970-01-01
  • 2014-09-20
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多