加入收藏 | 设为首页 | 会员中心 | 我要投稿 李大同 (https://www.lidatong.com.cn/)- 科技、建站、经验、云计算、5G、大数据,站长网!
当前位置: 首页 > 百科 > 正文

c# – 如何在不自动降级到同步模式的情况下调用异步方法?

发布时间:2020-12-15 23:33:32 所属栏目:百科 来源:网络整理
导读:在开发一些异步代码时,我和我的同事遇到了一段代码中的奇怪问题.一个组件是以连续的速率触发的,但是我们希望它所调用的一个依赖项永远不会一次执行多个任务. 我们决定只缓存任务,没问题.我们围绕缓存任务的分配进行了锁定,并使单例任务在完成后自行删除. Tas
在开发一些异步代码时,我和我的同事遇到了一段代码中的奇怪问题.一个组件是以连续的速率触发的,但是我们希望它所调用的一个依赖项永远不会一次执行多个任务.

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

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中运行.

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

所以,问的问题是:

>即使SomeWork()产生,这种情况也会陷入僵局吗?
>如果TaskScheduler在同一个线程上继续并且SomeWork()足够快,异步情况是否可以像同步情况一样(在设置之前清除onlyOneTask)?
>有没有办法强制现有的任务总是表现得好像它是异步的而不会损害线程池?
>有更好的选择吗?

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

解决方法

问题是当await命中你的函数返回不完整的任务并退出锁时允许更多的事情来运行任务.您通常会通过等待锁内部来解决它,但以下代码将无法编译

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

我们修复它的方法是使用InitialCount为1并使用其WaitAsync()方法从lock切换到SemaphoreSlim.

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);
    }
}

(编辑:李大同)

【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容!

    推荐文章
      热点阅读