首页 > 解决方案 > 为什么在同一(UI)线程中顺序调用 AsyncLock 与不同线程(如通过 Task.Run)的工作方式不同?

问题描述

部分地,这个问题与这个问题有点相似,但是由于另一个问题没有被正确地问到(也没有被完全问到),所以我试图一般地问它,所以这个问题不能被视为重复。

问题在于了解AsyncLock 的实际工作原理。(在这种情况下,我指的是Neosmart.AsyncLock库,但是,我认为它在 AsyncLock 实现中使用了通用方法)。

所以。例如,我们有一个主线程(让它成为一个 UI 线程):

    static void Main(string[] args)
    {
        Console.WriteLine("Press Esc for exit");

        var lck = new AsyncLock();

        var doJob = new Action(async () =>
        {
            using (await lck.LockAsync())
            {
                // long lasting job
                Console.WriteLine("+++ Job starts");
                await Task.Delay(TimeSpan.FromSeconds(10));
                Console.WriteLine("--- Job finished");
            }
        });

        while (Console.ReadKey().Key != ConsoleKey.Escape)
        {
            doJob();
        }
    }

因此,每次按 Enter 键都会开始doJob,而无需等到上一个作业完成。

但是,当我们将其更改为:

    Task.Run(() =>
    {
        doJob();
    });

... 一切都像魅力一样,在之前完成之前不会运行新的工作。

很明显,异步逻辑与经典逻辑大不相同lock(_myLock),无法直接进行比较,但是,当第二次调用 LockAsync 将“锁定”(再次,在异步上下文中)时,为什么第一种方法不起作用“持久的工作”开始直到以前完成。


实际上有一个实际要求,为什么我需要该代码以这种方式工作,(真正的问题是我如何使用 await LockAsync 实现这一点?):

例如,在我的应用程序(例如,我的移动应用程序)中,在启动时,我开始预加载一些数据(这是一项公共服务,需要将该数据保存在缓存中以供进一步使用),然后,当UI 启动时,特定页面请求相同的数据以显示 UI,并要求相同的服务加载相同的数据。因此,如果没有任何自定义逻辑,该服务将启动两个持久的作业来检索相同的数据包。相反,我希望我的 UI 在数据预加载完成后从缓存中接收数据。 像那样(一个抽象的可能场景):

    class MyApp
    {
        string[] _cache = null;
        AsyncLock _lock = new AsyncLock();

        async Task<IEnumerable<string>> LoadData()
        {
            using (await _lock.LockAsync())
            {
                if (_cache == null)
                {
                    await Task.Delay(TimeSpan.FromSeconds(10));
                    _cache = new[] {"one", "two", "three"};
                }
                return _cache;
            }
        }

        void OnAppLaunch()
        {
            LoadData();
        }

        async void OnMyCustomEvent()
        {
            var data = await LoadData();
            // to do something else with the data
        }
    }

如果我将其更改为,问题将得到解决,Task.Run(async () => { var data = await LoadData(); })但它看起来不是很干净和不错的方法。

标签: c#async-awaitlocking

解决方案


正如 Matthew 在评论中指出的那样,AsyncLock它是可重入的,这意味着如果同一个线程再次尝试获取锁,它会识别并允许它继续。作者AsyncLock写了一篇关于重入是他写这篇文章的真正原因的长篇文章:AsyncLock: an async/await-friendly locking library for C# and .NET

这不是错误;这是一项功能。™</p>

在“2017 年 5 月 25 日更新”标题之后,有一些代码示例准确地演示了您在此处遇到的情况并展示了它是如何成为一项功能的。

想要重入的原因是:

  1. 如果您只关心多个线程接触同一个变量(防止竞争条件),那么根本没有理由阻塞已经拥有锁的线程。
  2. 它使使用锁的递归函数更容易编写,因为您不需要测试是否已经拥有锁。缺乏重入支持+草率的递归编码=死锁。

如果你真的不希望它是可重入的,你可以使用他所说的不适合重入的内容SemaphoreSlim::

var lck = new SemaphoreSlim(1);

var doJob = new Action(async () => {

    await lck.WaitAsync();

    try {
        // long lasting job
        Console.WriteLine("+++ Job starts");
        await Task.Delay(TimeSpan.FromSeconds(2));
        Console.WriteLine("--- Job finished");
    } finally {
        lck.Release();
    }
});

推荐阅读