首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >集合被修改,而不管它的锁保护。

集合被修改,而不管它的锁保护。
EN

Stack Overflow用户
提问于 2017-07-03 11:12:40
回答 1查看 37关注 0票数 0

我知道集合是修改过的异常问题,但是在这个实例中我看不到它。我知道怎么解决它,我只想知道为什么会发生在这里。

因此,我有一组TaskCompletionSources,还有一个保护对该集合的访问的lockObject。在一个任务(T1)中,我希望创建TCS并等待3秒才能完成任务。

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

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

该程序产生以下输出:

代码语言:javascript
复制
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
...

我不明白的是:

  • 为什么会抛出异常?
  • 当CompleteAndClear仍然持有锁时,Remove方法如何启动和完成?

在我看来,TrySetResult似乎导致等待使用持有锁的同一个线程完成--因此当前线程跳转到WaitAsync函数,转到Remove,然后,这个线程确实持有来自CompleteAndClear的锁(锁由同一个线程重入),然后由于删除更改了HashSet,将调用异常。但其意图是执行CompleteAndClear的线程通过设置任务结果将任务标记为完成,然后清除集合并释放锁,然后Remove才能进入锁,并且应该报告"TCS“。

代码中最简单的修正是替换

代码语言:javascript
复制
      Remove(tcs);

使用

代码语言:javascript
复制
      if (!res) Remove(tcs);

这是完美的,但不符合意图。另一种方法是在清除集合之前制作一份副本,并在副本上设置结果,这样就完全解决了问题。

代码:

代码语言:javascript
复制
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.");
    }

  }
}
EN

回答 1

Stack Overflow用户

回答已采纳

发布于 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和更高版本),那么您可以将Task​Creation​Options.​Run​Continuations​Asynchronously传递到TaskCompletionSource<T>构造函数中。这给您提供了最理想的行为:任务是立即和同步完成的,但是所有的连续都被迫是异步的。

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

最后,我建议首先编写与异步兼容的协调原语,然后使用这些元素构建更复杂的结构,如工作队列。在代码的一部分中处理这些棘手的情况要容易得多,甚至可能将其外包。例如,我的AsyncEx库有一整套异步兼容的协调原语;RunContinuationsAsynchronously标志,而解决办法

票数 1
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/44884117

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档