【问题标题】:Better approach to concurrently "do or wait and skip"更好的方法同时“做或等待和跳过”
【发布时间】:2016-11-02 14:25:14
【问题描述】:

我想知道这个任务是否有更好的解决方案。一个有一个由一定数量的线程并发调用的函数,但是如果某个线程已经在执行代码,则其他线程应该跳过该部分代码并等待该线程完成执行。这是我现在所拥有的:

int _flag = 0;
readonly ManualResetEventSlim Mre = new ManualResetEventSlim();

void Foo()
{
    if (Interlocked.CompareExchange(ref _flag, 1, 0) == 0)
    {
        Mre.Reset();
        try
        {
            // do stuff
        }
        finally
        {
            Mre.Set();
            Interlocked.Exchange(ref _flag, 0);
        }
    }
    else
    {
        Mre.Wait();
    }
}

我想要实现的是更快的执行速度、更低的开销和更漂亮的外观。

【问题讨论】:

  • 我很好奇,你有什么场景对方法结束时的 Wait() 有用?换句话说,为什么那个线程会等待?
  • 该方法实际上做了什么?您可以使用ActionBlock<T> 将类型 T 的消息逐个排队以供执行。或者 TransformBlock<T1,T2> 一次处理一个输入并将输出发送到后续块
  • 处理并发问题的最佳方法是完全避免使用原始线程。 .NET 已经包含实现大多数并发方案的类。大多数情况下,您最终会实现 BCL 中已有的内容
  • @Dragos 例如,我有一个过期超时的缓存。当有人试图与缓存交互时,过期的项目会被删除,但只有一个线程应该处理它们,其他线程应该等到它结束。
  • @ilivit 如果其他线程通过像IReadDictionary<TKey,TValue> 这样的只读接口访问缓存,这将容易得多。 Dictionary&lt;&gt;ConcurrentDictionary&lt;&gt; 都实现了这个接口

标签: c# .net multithreading concurrency manualresetevent


【解决方案1】:

您可以使用AutoResetEventBarrier 的组合来执行此操作。

您可以使用AutoResetEvent 来确保只有一个线程进入“工作”方法。

Barrier 用于确保所有线程等待,直到进入“work”方法的线程从它返回。

这里有一些示例代码:

using System;
using System.Threading;
using System.Threading.Tasks;

namespace Demo
{
    class Program
    {
        const int TASK_COUNT = 3;
        static readonly Barrier barrier = new Barrier(TASK_COUNT);
        static readonly AutoResetEvent gate = new AutoResetEvent(true);

        static void Main()
        {
            Parallel.Invoke(task, task, task);
        }

        static void task()
        {
            while (true)
            {
                Console.WriteLine(Thread.CurrentThread.ManagedThreadId + " is waiting at the gate.");

                // This bool is just for test purposes to prevent the same thread from doing the
                // work every time!

                bool didWork = false; 

                if (gate.WaitOne(0))
                {
                    work();
                    didWork = true;
                    gate.Set();
                }

                Console.WriteLine(Thread.CurrentThread.ManagedThreadId + " is waiting at the barrier.");
                barrier.SignalAndWait();

                if (didWork)
                    Thread.Sleep(10); // Give a different thread a chance to get past the gate!
            }
        }

        static void work()
        {
            Console.WriteLine(Thread.CurrentThread.ManagedThreadId + " is entering work()");
            Thread.Sleep(3000);
            Console.WriteLine(Thread.CurrentThread.ManagedThreadId + " is leaving work()");
        }
    }
}

不过,Task Parallel Library 可能有更好、更高级别的解决方案。值得一读。

【讨论】:

    【解决方案2】:

    首先,等待的线程不会做任何事情,它们只是等待,在它们从事件中得到信号后,它们只是简单地移出方法,所以你应该添加while循环。之后,您可以使用AutoResetEvent 而不是手动的,正如@MatthewWatson 建议的那样。另外,您可以考虑在循环中使用SpinWait,这是一种轻量级的解决方案。

    其次,如果flag 字段肯定是bool 属性,为什么要使用int

    第三,为什么不使用简单的锁定,就像@grrrrrrrrrrrrr 建议的那样?这正是您在这里所做的:强制其他线程等待一个。如果您的代码应该在给定时间内仅由一个线程write,但可以由多个线程read,您可以使用ReaderWriterLockSlim 对象进行此类同步。

    【讨论】:

      【解决方案3】:

      我想要实现的是更快的执行速度、更低的开销和更漂亮的外观。

      更快的执行

      除非您的“Do Stuff”速度非常快,否则此代码不应有任何重大开销。

      降低开销

      同样,Interlocked Exchange 和/CompareExchange 的开销非常低,手动重置事件也是如此。

      如果你的“做事”真的很快,例如移动一个链表头,然后你可以旋转:

      更漂亮

      与正确的单线程 C# 代码相比,正确的多线程 C# 代码很少看起来漂亮。语言习语还没有。

      也就是说:如果你有一个真的快速操作(“几十个周期”),那么你可以旋转:(虽然不知道你的代码在做什么,我不能说这是否正确)。

        if (Interlocked.CompareExchange(ref _flag, 1, 0) == 0)
              {
                  try
                  {
                      // do stuff that is very quick.
                  }
                  finally
                  {
                      Interlocked.Exchange(ref _flag, 0);
                  }
              }
              else
              {
                  SpinWait.SpinUntil(() => _flag == 0);
              }
      

      【讨论】:

        【解决方案4】:

        首先想到的是将其更改为使用锁。这不会跳过代码,但会导致每个线程在第一个线程执行其内容时暂停。这样在出现异常的情况下,锁也会自动释放。

        object syncer = new object();
        void Foo()
        {
            lock(syncer)
            {
                //Do stuff
            }
        }
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2017-11-16
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2012-03-23
          • 2013-08-10
          • 2018-11-19
          相关资源
          最近更新 更多