使锁定无效的同一线程上的两个任务 运行
Two Tasks run on the same thread which invalidates lock
编辑
我发现 Building Async Coordination Primitives, Part 1: AsyncManualResetEvent 可能与我的主题相关。
In the case of TaskCompletionSource, that means that synchronous continuations can happen as part of a call to {Try}Set*, which means in our AsyncManualResetEvent example, those continuations could execute as part of the Set method. Depending on your needs (and whether callers of Set may be ok with a potentially longer-running Set call as all synchronous continuations execute), this may or may not be what you want.
非常感谢所有的回答,感谢您的知识和耐心!
原问题
我知道线程池线程上的 Task.Run
运行s 和线程可以重入。但是我从来不知道两个任务可以运行在同一个线程上,当它们都alive!
我的问题是:这样设计合理吗?这是否意味着异步方法中的 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();
}
}
}
输出:
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
线程6的控制流程如图:
使用 SemaphoreSlim
而不是 lock
,因为正如文档所说:
The SemaphoreSlim class doesn't enforce thread or task identity
在你的情况下,它看起来像这样:
// Semaphore only allows one request to enter at a time
private static readonly SemaphoreSlim _semaphoreSlim = new SemaphoreSlim(1, 1);
void SyncMethod() {
_semaphoreSlim.Wait();
try {
// Do some sync work
} finally {
_semaphoreSlim.Release();
}
}
try
/finally
块是可选的,但它确保即使代码中某处抛出异常也能释放信号量。
注意SemaphoreSlim
还有一个WaitAsync()
方法,如果要异步等待进入信号量
任务是对一定量工作的抽象。通常这意味着工作被分成几个部分,可以在部分之间暂停和恢复执行。恢复时它很可能 运行 在另一个线程上。但是 pausing/resuming 只能在 await
语句中完成。值得注意的是,虽然任务是 'paused',例如因为它正在等待 IO,它根本不消耗任何线程,它只会使用一个线程,而它实际上是 运行ning.
My Question is: is that reasonable by design? Does that mean lock inside an async method is meaningless?
异步方法中的锁远非毫无意义,因为它允许您确保一段代码一次只能 运行 由一个线程执行。
在您的第一个示例中,一次只能有一个线程拥有锁。当锁被持有时,任务不能是 paused/resumed 因为 await
在锁体中是不合法的。所以单个线程将执行整个锁体,并且该线程在完成锁体之前不能做任何其他事情。因此,除非您调用一些可以回调到相同方法的代码,否则不存在重新进入的风险。
在您更新的示例中,问题的发生是由于 TaskCompletionSource.SetException
,这允许重用当前线程以立即 运行 任务的任何延续。为避免此问题和许多其他问题,请确保仅在 运行 宁 有限 代码量时持有锁。任何可能 运行 任意代码的方法调用都有导致死锁、重入和许多其他问题的风险。
您可以通过使用 ManualResetEvent(Slim) 在线程之间发送信号而不是使用 TaskCompletionSource 来解决特定问题。
所以你的方法基本上是这样的:
static void MethodNotAllowReetrance()
{
lock (methodLock) tcs.SetResult();
}
...并且 tcs.Task
附加了一个调用 MethodNotAllowReetrance
的延续。如果您的方法是这样的,那么会发生什么:
static void MethodNotAllowReetrance()
{
lock (methodLock) MethodNotAllowReetrance();
}
道德教训是,每次在 lock
保护区内调用任何方法时都必须非常小心。在这种特殊情况下,您有几个选择:
- 不要在按住
lock
的同时完成 TaskCompletionSource
。推迟完成,直到您离开保护区:
static void MethodNotAllowReetrance()
{
bool doComplete = false;
lock (methodLock) doComplete = true;
if (doComplete) tcs.SetResult();
}
配置 TaskCompletionSource
以便它通过传递 TaskCreationOptions.RunContinuationsAsynchronously
in its constructor. This is an option that you don't have very often. For example when you cancel a CancellationTokenSource
异步调用其延续,您无法选择异步调用注册到其关联的回调CancellationToken
.
以可以处理重入的方式重构 MethodNotAllowReetrance
方法。
您已经有多种解决方案。我只是想多描述一下问题。这里有几个因素在起作用,共同导致观察到的重入。
首先,lock
是可重入的。 lock
严格来说是threads的互斥,与code的互斥不一样。我认为 re-entrant locks are a bad idea 在 99% 的情况下(如我的博客所述),因为开发人员通常希望互斥 code 而不是 threads. SemaphoreSlim
,由于不可重入,互斥code。 IMO 可重入锁是几十年前的产物,当时它们作为 OS 概念引入,而 OS 只关心管理线程。
接下来,TaskCompletionSource<T>
by default invokes task continuations synchronously.
此外,await
will schedule its method continuation as a synchronous task continuation(如我的博客所述)。
Task continuations will sometimes run asynchronously even if scheduled synchronously,但在这种情况下,它们将同步 运行。 await
捕获的上下文是线程池上下文,完成线程(调用 TCS.TrySet*
的线程)是线程池线程,在这种情况下,延续几乎总是 运行 同步.
因此,您最终得到一个获取锁的线程,完成 TCS,从而执行该任务的延续,其中包括继续另一个方法,然后该方法能够获取相同的锁。
要重复其他答案中的现有解决方案,要解决此问题,您需要在某个时候打破该链条:
- (OK) 使用不可重入锁。
SemaphoreSlim.WaitAsync
仍将在持有锁的同时执行延续(这不是一个好主意),但由于 SemaphoreSlim
不可重入,方法延续将(异步地)等待锁可用。
- (最佳)使用
TaskCompletionSource.RunContinuationsAsynchronously
,这将强制任务继续到(不同的)线程池线程。这是一个更好的解决方案,因为您的代码在持有锁时不再调用任意代码(即任务延续)。
您还可以通过为方法 await
ing TCS 使用非线程池上下文来打破链条。例如,如果该方法必须在 UI 线程上恢复,则它不能 运行 从线程池线程同步。
从更广泛的角度来看,如果您混合使用锁和 TaskCompletionSource
实例,听起来您可能正在构建(或可能需要)异步协调原语。我有 an open-source library that implements a bunch of them,如果有帮助的话。
编辑
我发现 Building Async Coordination Primitives, Part 1: AsyncManualResetEvent 可能与我的主题相关。
In the case of TaskCompletionSource, that means that synchronous continuations can happen as part of a call to {Try}Set*, which means in our AsyncManualResetEvent example, those continuations could execute as part of the Set method. Depending on your needs (and whether callers of Set may be ok with a potentially longer-running Set call as all synchronous continuations execute), this may or may not be what you want.
非常感谢所有的回答,感谢您的知识和耐心!
原问题
我知道线程池线程上的 Task.Run
运行s 和线程可以重入。但是我从来不知道两个任务可以运行在同一个线程上,当它们都alive!
我的问题是:这样设计合理吗?这是否意味着异步方法中的 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();
}
}
}
输出:
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
线程6的控制流程如图:
使用 SemaphoreSlim
而不是 lock
,因为正如文档所说:
The SemaphoreSlim class doesn't enforce thread or task identity
在你的情况下,它看起来像这样:
// Semaphore only allows one request to enter at a time
private static readonly SemaphoreSlim _semaphoreSlim = new SemaphoreSlim(1, 1);
void SyncMethod() {
_semaphoreSlim.Wait();
try {
// Do some sync work
} finally {
_semaphoreSlim.Release();
}
}
try
/finally
块是可选的,但它确保即使代码中某处抛出异常也能释放信号量。
注意SemaphoreSlim
还有一个WaitAsync()
方法,如果要异步等待进入信号量
任务是对一定量工作的抽象。通常这意味着工作被分成几个部分,可以在部分之间暂停和恢复执行。恢复时它很可能 运行 在另一个线程上。但是 pausing/resuming 只能在 await
语句中完成。值得注意的是,虽然任务是 'paused',例如因为它正在等待 IO,它根本不消耗任何线程,它只会使用一个线程,而它实际上是 运行ning.
My Question is: is that reasonable by design? Does that mean lock inside an async method is meaningless?
异步方法中的锁远非毫无意义,因为它允许您确保一段代码一次只能 运行 由一个线程执行。
在您的第一个示例中,一次只能有一个线程拥有锁。当锁被持有时,任务不能是 paused/resumed 因为 await
在锁体中是不合法的。所以单个线程将执行整个锁体,并且该线程在完成锁体之前不能做任何其他事情。因此,除非您调用一些可以回调到相同方法的代码,否则不存在重新进入的风险。
在您更新的示例中,问题的发生是由于 TaskCompletionSource.SetException
,这允许重用当前线程以立即 运行 任务的任何延续。为避免此问题和许多其他问题,请确保仅在 运行 宁 有限 代码量时持有锁。任何可能 运行 任意代码的方法调用都有导致死锁、重入和许多其他问题的风险。
您可以通过使用 ManualResetEvent(Slim) 在线程之间发送信号而不是使用 TaskCompletionSource 来解决特定问题。
所以你的方法基本上是这样的:
static void MethodNotAllowReetrance()
{
lock (methodLock) tcs.SetResult();
}
...并且 tcs.Task
附加了一个调用 MethodNotAllowReetrance
的延续。如果您的方法是这样的,那么会发生什么:
static void MethodNotAllowReetrance()
{
lock (methodLock) MethodNotAllowReetrance();
}
道德教训是,每次在 lock
保护区内调用任何方法时都必须非常小心。在这种特殊情况下,您有几个选择:
- 不要在按住
lock
的同时完成TaskCompletionSource
。推迟完成,直到您离开保护区:
static void MethodNotAllowReetrance()
{
bool doComplete = false;
lock (methodLock) doComplete = true;
if (doComplete) tcs.SetResult();
}
配置
TaskCompletionSource
以便它通过传递TaskCreationOptions.RunContinuationsAsynchronously
in its constructor. This is an option that you don't have very often. For example when you cancel aCancellationTokenSource
异步调用其延续,您无法选择异步调用注册到其关联的回调CancellationToken
.以可以处理重入的方式重构
MethodNotAllowReetrance
方法。
您已经有多种解决方案。我只是想多描述一下问题。这里有几个因素在起作用,共同导致观察到的重入。
首先,lock
是可重入的。 lock
严格来说是threads的互斥,与code的互斥不一样。我认为 re-entrant locks are a bad idea 在 99% 的情况下(如我的博客所述),因为开发人员通常希望互斥 code 而不是 threads. SemaphoreSlim
,由于不可重入,互斥code。 IMO 可重入锁是几十年前的产物,当时它们作为 OS 概念引入,而 OS 只关心管理线程。
接下来,TaskCompletionSource<T>
by default invokes task continuations synchronously.
此外,await
will schedule its method continuation as a synchronous task continuation(如我的博客所述)。
Task continuations will sometimes run asynchronously even if scheduled synchronously,但在这种情况下,它们将同步 运行。 await
捕获的上下文是线程池上下文,完成线程(调用 TCS.TrySet*
的线程)是线程池线程,在这种情况下,延续几乎总是 运行 同步.
因此,您最终得到一个获取锁的线程,完成 TCS,从而执行该任务的延续,其中包括继续另一个方法,然后该方法能够获取相同的锁。
要重复其他答案中的现有解决方案,要解决此问题,您需要在某个时候打破该链条:
- (OK) 使用不可重入锁。
SemaphoreSlim.WaitAsync
仍将在持有锁的同时执行延续(这不是一个好主意),但由于SemaphoreSlim
不可重入,方法延续将(异步地)等待锁可用。 - (最佳)使用
TaskCompletionSource.RunContinuationsAsynchronously
,这将强制任务继续到(不同的)线程池线程。这是一个更好的解决方案,因为您的代码在持有锁时不再调用任意代码(即任务延续)。
您还可以通过为方法 await
ing TCS 使用非线程池上下文来打破链条。例如,如果该方法必须在 UI 线程上恢复,则它不能 运行 从线程池线程同步。
从更广泛的角度来看,如果您混合使用锁和 TaskCompletionSource
实例,听起来您可能正在构建(或可能需要)异步协调原语。我有 an open-source library that implements a bunch of them,如果有帮助的话。