我知道集合被修改了异常问题,但我在这个实例中看不到它。我知道如何修复它,我只是想了解它为什么会发生在这里。
所以,我有一组TaskCompletionSources,我有一个lockObject来保护对该集合的访问。在一个任务(T1)中,我想创建TCS并等待最多3秒钟才能完成任务。
在另一项任务(T2)中,我想等半秒钟,然后完成T1等待的任务。
TCS的集合在这段代码片段中并没有任何用处,但在我正在处理的实际程序中,这是为了保存一定数量的不同服务员的列表,一旦任务完成,应该通知所有服务员。完成,这也应该清除服务员名单。在这个片段中,我们只有一个服务员(T1),但必须使用一组TCS来重现问题。
程序产生以下输出:
exclude(choice__isnull=True)
我不明白:
对我来说,似乎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);
答案 0 :(得分:1)
问题的核心是TaskCompletionSource<T>.TrySetResult
将同步调用TaskContinuationOption.ExecuteSynchronously
注册的任何任务延续,并结合await
does use that flag。< / p>
因此,CompleteAndClear
将获得锁定,然后在持有该锁定时调用TrySetResult
。 Since this is in a free-threaded context,TrySetResult
将同步恢复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
及更高版本),那么您可以将TaskCreationOptions.RunContinuationsAsynchronously
传递给TaskCompletionSource<T>
构造函数。这为您提供了最理想的行为:任务立即完成并同步完成,但所有延续都被强制为异步。
对于较旧的平台,您可以在委托中封装“完成”工作(即TrySetResult
的调用)(我建议进一步包装在IDisposable
中),并延迟工作直到方法释放锁定后。
最后,我建议首先编写异步兼容的协调原语,然后使用它们构建更复杂的结构,例如工作队列。只在部分代码中处理这些棘手的情况要容易得多,甚至可能外包它。例如,我的AsyncEx library具有一整套异步兼容的协调原语; v5 uses the new RunContinuationsAsynchronously
标志,v4 uses the delay-completion-with-IDisposable
workaround。