两个任务在同一个线程上运行,这会使锁失效

Tom*_*sun 1 c# asynchronous locking task

编辑

我发现构建异步协调基元,第 1 部分:AsyncManualResetEvent可能与我的主题相关。

对于 TaskCompletionSource,这意味着同步延续可以作为调用 {Try}Set* 的一部分发生,这意味着在我们的 AsyncManualResetEvent 示例中,这些延续可以作为 Set 方法的一部分执行。根据您的需求(以及 Set 的调用者是否可以接受在所有同步延续执行时可能运行更长时间的 Set 调用),这可能是也可能不是您想要的。

非常感谢所有的答案,感谢您的知识和耐心!


原始问题

我知道它Task.Run在线程池线程上运行,并且线程可以重入。但我从来不知道两个任务可以在同一个线程上运行,当它们都处于活动状态时!

我的问题是:设计合理吗?这是否意味着lock异步方法内部毫无意义(或者说,lock如果我想要一个不允许重入的方法,则在异步方法块中不能被信任)?

代码:

namespace TaskHijacking
{
    class Program
    {
        static TaskCompletionSource<bool> tcs = new TaskCompletionSource<bool>();
        static object methodLock = new object();

        static void MethodNotAllowReetrance(string callerName)
        {
            lock(methodLock)
            {
                Console.WriteLine($"Enter MethodNotAllowReetrance, caller: {callerName}, on thread: {Thread.CurrentThread.ManagedThreadId}");
                if (callerName == "task1")
                {
                    tcs.SetException(new Exception("Terminate tcs"));
                }
                Thread.Sleep(1000);
                Console.WriteLine($"Exit MethodNotAllowReetrance, caller: {callerName}, on thread: {Thread.CurrentThread.ManagedThreadId}");
            }
        }

        static void Main(string[] args)
        {
            var task1 = Task.Run(async () =>
            {
                await Task.Delay(1000);
                MethodNotAllowReetrance("task1");
            });

            var task2 = Task.Run(async () =>
            {
                try
                {
                    await tcs.Task;  // await here until task SetException on tcs
                }
                catch
                {
                    // Omit the exception
                }
                MethodNotAllowReetrance("task2");
            });

            Task.WaitAll(task1, task2);

            Console.ReadKey();
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

输出:

Enter MethodNotAllowReetrance, caller: task1, on thread: 6
Enter MethodNotAllowReetrance, caller: task2, on thread: 6
Exit MethodNotAllowReetrance, caller: task2, on thread: 6
Exit MethodNotAllowReetrance, caller: task1, on thread: 6
Run Code Online (Sandbox Code Playgroud)

线程6的控制流程如图所示:

在此输入图像描述

Ste*_*ary 5

您已经有了几种解决方案。我只是想多描述一下这个问题。这里有几个因素在起作用,这些因素结合起来导致了观察到的重入。

首先,lock是可重入的。严格来说是线程lock的互斥,这与代码的互斥不同。我认为在 99% 的情况下,可重入锁是一个坏主意(如我的博客中所述),因为开发人员通常希望互斥代码而不是线程。,由于它不可重入,因此相互排除code。IMO 可重入锁是几十年前的遗留物,当时它们是作为操作系统概念引入的,操作系统只关心管理线程。SemaphoreSlim

接下来,TaskCompletionSource<T>默认情况下同步调用任务延续。

另外,await将其方法延续安排为同步任务延续(如我的博客中所述)。

即使同步计划,任务延续有时也会异步运行,但在这种情况下,它们将同步运行。捕获的上下文await是线程池上下文,完成线程(调用的线程TCS.TrySet*)是线程池线程,在这种情况下,延续几乎总是同步运行。

因此,您最终会得到一个获取锁的线程,完成 TCS,从而执行该任务的延续,其中包括继续另一个方法,然后该方法能够获取相同的锁。

要重复其他答案中的现有解决方案,要解决此问题,您需要在某个时刻打破该链:

  • (好的)使用不可重入锁。SemaphoreSlim.WaitAsync仍将在持有锁的同时执行延续(不是一个好主意),但由于SemaphoreSlim不可重入,因此方法延续将(异步)等待锁可用。
  • (最佳)使用TaskCompletionSource.RunContinuationsAsynchronously,这将强制任务继续到(不同的)线程池线程上。这是一个更好的解决方案,因为您的代码在持有锁(即任务继续)时不再调用任意代码。

await您还可以通过对 TCS方法使用非线程池上下文来打破该链。例如,如果该方法必须在 UI 线程上恢复,则它无法从线程池线程同步运行。

从更广泛的角度来看,如果您混合使用锁和TaskCompletionSource实例,听起来您可能正在构建(或可能需要)异步协调原语。我有一个开源库,它实现了其中的一些,如果有帮助的话。