无论锁定保护如何,都会修改集合

时间:2017-07-03 11:12:40

标签: asynchronous collections locking task .net-core

我知道集合被修改了异常问题,但我在这个实例中看不到它。我知道如何修复它,我只是想了解它为什么会发生在这里。

所以,我有一组TaskCompletionSources,我有一个lockObject来保护对该集合的访问。在一个任务(T1)中,我想创建TCS并等待最多3秒钟才能完成任务。

在另一项任务(T2)中,我想等半秒钟,然后完成T1等待的任务。

TCS的集合在这段代码片段中并没有任何用处,但在我正在处理的实际程序中,这是为了保存一定数量的不同服务员的列表,一旦任务完成,应该通知所有服务员。完成,这也应该清除服务员名单。在这个片段中,我们只有一个服务员(T1),但必须使用一组TCS来重现问题。

程序产生以下输出:

exclude(choice__isnull=True)

我不明白:

  • 为什么抛出异常?
  • 当CompleteAndClear仍然持有锁时,Remove方法可以如何开始和结束?

对我来说,似乎TrySetResult使用保存锁的同一线程导致Wait完成 - 所以当前线程跳转到WaitAsync函数,转到Remove,然后通过此线程执行的操作绕过Lock持有来自CompleteAndClear的锁(锁可以由同一个线程重入)然后由于Remove更改了HashSet,因此调用了异常。但意图是执行CompleteAndClear的线程只是通过设置结果将任务标记为完成,然后清除集合并释放锁定,然后只有Remove可以进入锁定,并且它应该报告“TCS not found。”。

代码中的简单修复是替换

T1 start.
Wait start.
Add start.
Add end.
T2 start.
CompleteAndClear start.
Completing 1 TCSs.
Remove start.
Remove end.
Wait end.
Wait succeeded.
T1 end.

Unhandled Exception: System.AggregateException: One or more errors occurred. (Collection was modified; enumeration operation may not execute.) ---> System.InvalidOperationException: Collection was modified; enumeration operation may not execute.
   at System.Collections.Generic.HashSet`1.Enumerator.MoveNext()
   at ConsoleApp1.Program.CompleteAndClear() in Program.cs:line 104
   at ConsoleApp1.Program.<T2Async>d__5.MoveNext() in Program.cs:line 45
...

      Remove(tcs);

效果很好但不符合意图。另一个是在清除它之前制作一个副本,并在副本上设置结果,这完全解决了这个问题。

代码:

      if (!res) Remove(tcs);

1 个答案:

答案 0 :(得分:1)

问题的核心是TaskCompletionSource<T>.TrySetResult同步调用TaskContinuationOption.ExecuteSynchronously注册的任何任务延续,并结合await does use that flag。< / p>

因此,CompleteAndClear将获得锁定,然后在持有该锁定时调用TrySetResult Since this is in a free-threaded contextTrySetResult同步恢复WaitAsync方法,然后调用Remove进行锁定(成功因为lock允许递归锁定)并修改集合。当TrySetResult返回时(在完成Remove之后),枚举器将检测到问题并抛出异常。

有一些(IMO可疑的)设计决策在这里合作。我反对await using ExecuteSynchronously以及recursive locks in general(“不一致的不变量”一节特别适用于这种情况)。

但是,您仍然可以通过严格遵循key principles of multithreading: never invoke arbitrary code while holding a lock之一来避免这些设计决策中固有的问题。当然,显而易见的是TaskCompletionSource<T>.TrySetResult 可以调用任意代码。

现在,解决方案。

如果您要定位足够新的运行时(我相信netstandard1.3 / .NET Core 1.0及更高版本),那么您可以将Task​Creation​Options.​Run​Continuations​Asynchronously传递给TaskCompletionSource<T>构造函数。这为您提供了最理想的行为:任务立即完成并同步完成,但所有延续都被强制为异步。

对于较旧的平台,您可以在委托中封装“完成”工作(即TrySetResult的调用)(我建议进一步包装在IDisposable中),并延迟工作直到方法释放锁定后。

最后,我建议首先编写异步兼容的协调原语,然后使用它们构建更复杂的结构,例如工作队列。只在部分代码中处理这些棘手的情况要容易得多,甚至可能外包它。例如,我的AsyncEx library具有一整套异步兼容的协调原语; v5 uses the new RunContinuationsAsynchronously标志,v4 uses the delay-completion-with-IDisposable workaround