为什么在同一 (UI) 线程中顺序调用 AsyncLock 与不同线程(如通过 Task.Run)的工作方式不同?
Why sequential call of AsyncLock within same (UI) thread doesn't work same way as for different threads (like via Task.Run)?
部分地,这个问题与有点相似,但是由于另一个问题没有被正确地问到(并且没有被完全问到)我试着问一般的,所以这个问题不能认为是重复。
问题是关于理解 AsyncLock 的实际工作原理。 (在这种情况下,我指的是 Neosmart.AsyncLock 库,但是,我认为它在 AsyncLock 实现中使用了通用方法)。
所以。例如,我们有一个主线程(假设它是一个 UI-thread):
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 时 "lock"(再次,在异步上下文中)"long lasting job" 开始直到上一个完成。
实际上有一个实际要求,为什么我需要该代码以这种方式工作,(真正的问题是 我如何使用 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(); })
,问题将得到解决,但它看起来不是很干净和漂亮的方法。
正如 Matthew 在评论中指出的那样,AsyncLock
是可重入的,这意味着如果同一个线程再次尝试获取锁,它会识别并允许它继续。 AsyncLock
的作者写了一篇长篇文章,讲述了 重入是他写这篇文章的真正原因:AsyncLock: an async/await-friendly locking library for C# and .NET
这不是错误;这是一个功能。™
在 "Update 5/25/2017" 标题之后,有一些代码示例准确地演示了您在这里遇到的情况,并展示了它如何成为一项功能。
想要重新进入的原因是:
- 如果您只关心多个线程接触同一个变量(防止竞争条件),那么根本没有理由阻塞已经拥有锁的线程。
- 它使得使用锁的递归函数更容易编写,因为你不需要测试你是否已经拥有锁。缺乏重入支持 + 草率的递归编码 = 死锁。
如果你真的想让它不可重入,你可以用他说的不适合重入: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();
}
});
部分地,这个问题与
问题是关于理解 AsyncLock 的实际工作原理。 (在这种情况下,我指的是 Neosmart.AsyncLock 库,但是,我认为它在 AsyncLock 实现中使用了通用方法)。
所以。例如,我们有一个主线程(假设它是一个 UI-thread):
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 时 "lock"(再次,在异步上下文中)"long lasting job" 开始直到上一个完成。
实际上有一个实际要求,为什么我需要该代码以这种方式工作,(真正的问题是 我如何使用 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(); })
,问题将得到解决,但它看起来不是很干净和漂亮的方法。
正如 Matthew 在评论中指出的那样,AsyncLock
是可重入的,这意味着如果同一个线程再次尝试获取锁,它会识别并允许它继续。 AsyncLock
的作者写了一篇长篇文章,讲述了 重入是他写这篇文章的真正原因:AsyncLock: an async/await-friendly locking library for C# and .NET
这不是错误;这是一个功能。™
在 "Update 5/25/2017" 标题之后,有一些代码示例准确地演示了您在这里遇到的情况,并展示了它如何成为一项功能。
想要重新进入的原因是:
- 如果您只关心多个线程接触同一个变量(防止竞争条件),那么根本没有理由阻塞已经拥有锁的线程。
- 它使得使用锁的递归函数更容易编写,因为你不需要测试你是否已经拥有锁。缺乏重入支持 + 草率的递归编码 = 死锁。
如果你真的想让它不可重入,你可以用他说的不适合重入: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();
}
});