我知道集合是修改过的异常问题,但是在这个实例中我看不到它。我知道怎么解决它,我只想知道为什么会发生在这里。
因此,我有一组TaskCompletionSources,还有一个保护对该集合的访问的lockObject。在一个任务(T1)中,我希望创建TCS并等待3秒才能完成任务。
在另一个任务(T2)中,我想等待半秒钟,然后完成T1正在等待的任务。
这组TCS在这个代码片段中没有任何用处,但是在我正在进行的实际程序中,这是保存一定数量的不同服务员的列表,一旦任务完成就应该通知所有的服务员,这也应该清除服务员的列表。在这个片段中,我们只有一个服务生(T1),但是这组TCS必须用来重现问题。
该程序产生以下输出:
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
...我不明白的是:
在我看来,TrySetResult似乎导致等待使用持有锁的同一个线程完成--因此当前线程跳转到WaitAsync函数,转到Remove,然后,这个线程确实持有来自CompleteAndClear的锁(锁由同一个线程重入),然后由于删除更改了HashSet,将调用异常。但其意图是执行CompleteAndClear的线程通过设置任务结果将任务标记为完成,然后清除集合并释放锁,然后Remove才能进入锁,并且应该报告"TCS“。
代码中最简单的修正是替换
Remove(tcs);使用
if (!res) Remove(tcs);这是完美的,但不符合意图。另一种方法是在清除集合之前制作一份副本,并在副本上设置结果,这样就完全解决了问题。
代码:
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Threading;
namespace ConsoleApp1
{
class Program
{
static object lockObject = new object();
static HashSet<TaskCompletionSource<bool>> completionSources = new HashSet<TaskCompletionSource<bool>>();
static void Main(string[] args)
{
MainAsync().Wait();
}
static async Task MainAsync()
{
Task t1 = T1Async();
Task t2 = T2Async();
await t1;
await t2;
}
static async Task T1Async()
{
Console.WriteLine("T1 start.");
if (await WaitAsync()) Console.WriteLine("Wait succeeded.");
else Console.WriteLine("Wait failed.");
Console.WriteLine("T1 end.");
}
static async Task T2Async()
{
Console.WriteLine("T2 start.");
await Task.Delay(500);
CompleteAndClear();
Console.WriteLine("T2 end.");
}
static async Task<bool> WaitAsync()
{
Console.WriteLine("Wait start.");
bool res = false;
TaskCompletionSource<bool> tcs = new TaskCompletionSource<bool>();
using (CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(3000))
{
using (CancellationTokenRegistration cancellationTokenRegistration = cancellationTokenSource.Token.Register(() => { tcs.TrySetResult(false); }))
{
Add(tcs);
res = await tcs.Task;
Remove(tcs);
}
}
Console.WriteLine("Wait end.");
return res;
}
static void Add(TaskCompletionSource<bool> TaskCompletionSource)
{
Console.WriteLine("Add start.");
lock (lockObject)
{
completionSources.Add(TaskCompletionSource);
}
Console.WriteLine("Add end.");
}
static void Remove(TaskCompletionSource<bool> TaskCompletionSource)
{
Console.WriteLine("Remove start.");
lock (lockObject)
{
if (!completionSources.Remove(TaskCompletionSource)) Console.WriteLine("TCS not found.");
}
Console.WriteLine("Remove end.");
}
static void CompleteAndClear()
{
Console.WriteLine("CompleteAndClear start.");
lock (lockObject)
{
if (completionSources.Count > 0)
{
Console.WriteLine("Completing {0} TCSs.", completionSources.Count);
foreach (TaskCompletionSource<bool> tcs in completionSources)
tcs.TrySetResult(true);
Console.WriteLine("Clearing TCS list.");
completionSources.Clear();
}
}
Console.WriteLine("CompleteAndClear end.");
}
}
}发布于 2017-07-12 18:16:35
问题的核心是TaskCompletionSource<T>.TrySetResult将同步调用在TaskContinuationOption.ExecuteSynchronously中注册的任何任务延续,并结合使用那个标志吗?这一事实。
因此,CompleteAndClear将取下锁,然后在持有该锁时调用TrySetResult。因为这是在一个自由线程的上下文中。,TrySetResult将同步恢复WaitAsync方法,然后调用Remove,该方法接受锁(因为lock允许递归锁而成功)并修改集合。当TrySetResult返回时( Remove完成后),枚举数将检测问题并抛出异常。
在这里,有几个(海事组织有问题的)设计决定正在一起工作。我反对ExecuteSynchronously和一般递归锁 (“不一致不变量”一节特别适用于这个场景)。
但是,通过严格遵循多线程的关键原则:永远不要在持有锁时调用任意代码。之一,您仍然可以避免这些设计决策中固有的问题。当然,不明显的是TaskCompletionSource<T>.TrySetResult可以调用任意代码。
现在,谈谈解决方案。
如果您的目标是一个足够新的运行时(我相信netstandard1.3/.NET Core 1.0和更高版本),那么您可以将TaskCreationOptions.RunContinuationsAsynchronously传递到TaskCompletionSource<T>构造函数中。这给您提供了最理想的行为:任务是立即和同步完成的,但是所有的连续都被迫是异步的。
对于较旧的平台,您可以将“完成”工作(即调用TrySetResult)封装在委托中(我建议进一步封装在IDisposable中),并将工作推迟到方法释放锁之后。
最后,我建议首先编写与异步兼容的协调原语,然后使用这些元素构建更复杂的结构,如工作队列。在代码的一部分中处理这些棘手的情况要容易得多,甚至可能将其外包。例如,我的AsyncEx库有一整套异步兼容的协调原语;RunContinuationsAsynchronously标志,而解决办法。
https://stackoverflow.com/questions/44884117
复制相似问题