用于线程控制的C#ConcurrentDictionary与ManualResetEvent

时间:2014-10-29 19:39:19

标签: c# multithreading

我有一个Windows服务,它使用带有回调的System.Threading.Timer来更新端点,如下所示:

UpdateEndpointTimer = new Timer(
                new TimerCallback(UpdateSBEndpoints),
                Endpoint.Statuses.Online,
                EndpointUpdateFrequency,
                EndpointUpdateFrequency);

以下是我的更新方法大致如下:

private void UpdateSBEndpoints(object state)
{
    ...
    using (var context = new TestHarnessContext())
    {
        var endpoints = context.Endpoints.Where(p =>
            p.Binding == Endpoint.Bindings.ServiceBus
            && p.State == Endpoint.States.Enabled
            && p.Status != status).ToList();

         foreach (var endpoint in endpoints)
         {
             //Do stuff here
         }
         ...
}

现在,当计时器使用ThreadPool中的线程来触发回调时,我需要采取措施来控制线程。当多个线程可以在第一个线程完成工作之前从db中获取相同的端点时,会发生一个特定的问题,这会导致在foreach循环中完成重复的工作。

我知道这个问题有两个可能的解决方案,我想知道哪一个更好,更可取。解决方案为ConcurrentDictionaryManualResetEvent

在第一种情况下,我会把它放在我的foreach循环中,以确保一次只有一个线程可以在给定的端点上运行:

if (EndpointsInAction.TryAdd(endpoint.Id, endpoint.Id) == false)
    // If we get here, another thread has started work with this endpoint.
    return;
...
//Do stuff with the endpoint, once done, remove its Id from the dictionary
...
int id;
EndpointsInAction.TryRemove(endpoint.Id, out id);

在第二种情况下,我会像这样控制线程:

protected ManualResetEvent PubIsBeingCreated { get; set; }
protected ManualResetEvent SubIsBeingCreated { get; set; }
...
this.PubIsBeingCreated = new ManualResetEvent(true);
this.SubIsBeingCreated = new ManualResetEvent(true);
...
foreach (var endpoint in endpoints)
{
   if (!this.PubIsBeingCreated.WaitOne(0))
// If we get here, another thread has started work with this endpoint.
        return;

   try
   {
       // block other threads (Timer Events)
       PubIsBeingCreated.Reset();
       // Do stuff
   }
   ...
   finally
   {
       // Restore access for other threads
       PubIsBeingCreated.Set();
   }
}

现在两种方法似乎都有效我想知道哪种方法更适合使用(效率更高?)。我倾向于使用ConcurrentDictionary因为它允许更精细地过滤线程,即不允许两个线程与特定端点一起工作,而不允许两个线程与特定端点一起工作 type (ManualResetEvents中的pubs和subs)。可能有另一个优于我的解决方案,因此任何信息都将受到赞赏。

2 个答案:

答案 0 :(得分:0)

在任何情况下,您都不应将ManualResetEvent用于此目的。使用提供更高级别抽象的对象总是更好,但即使您想要在较低级别控制它,使用.NET的Monitor类(通过lock语句,Monitor.Wait()和{ {1}}方法比使用Monitor.Pulse()更好。

您似乎有一个合理标准的生产者/消费者场景。在这种情况下,在我看来,与ManualResetEvent相比,ConcurrentDictionary会更好。你的生产者会把东西排队,而你的消费线程会将它们排队等待处理。

答案 1 :(得分:0)

我会这样做;我使用ConcurrentBag并让一个线程从DB加载它,多个线程通过TryTake监听它。加载行李的单个线程将处理您可能从DB中获取相同项目的问题。下面将在控制台应用程序中说明它:

class Program
{
    private static ConcurrentBag<int> _bag;

    public static void Main()
    {
        // Construct and populate the ConcurrentBag
        _bag = new ConcurrentBag<int>();
        var bagPopulateTask = new Task(PopulateBag);
        bagPopulateTask.Start();

        var bagEmptyTask1 = new Task(EmptyBag);
        var bagEmptyTask2 = new Task(EmptyBag);
        var bagEmptyTask3 = new Task(EmptyBag);

        bagEmptyTask1.Start();
        bagEmptyTask2.Start();
        bagEmptyTask3.Start();

        Console.ReadKey();
    }

    private static void EmptyBag()
    {
        int i;
        while (true)
        {
            if (_bag.TryTake(out i))
            {
                Console.WriteLine("Thread {0} took {1}", Thread.CurrentThread.ManagedThreadId, i);
            }
            else
            {
                Console.WriteLine("Thread {0} took nothing", Thread.CurrentThread.ManagedThreadId);
            }
            Thread.Sleep(50);
        }
    }

    private static void PopulateBag()
    {
        var i = 0;

        while (i < 1000)
        {
            Console.WriteLine("Thread {0} added {1}", Thread.CurrentThread.ManagedThreadId, i);
            _bag.Add(i);
            i++;
            Thread.Sleep(i);
        }
    }
}

请注意,如果订单很重要,您可以使用ConcurrentQueue或ConcurrentStack执行类似操作。