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

用于线程控制的C#ConcurrentDictionary与ManualResetEvent,c#,multithreading,C#,Multithreading,我有一个Windows服务,它使用System.Threading.Timer和回调来更新端点,如下所示: UpdateEndpointTimer = new Timer( new TimerCallback(UpdateSBEndpoints), Endpoint.Statuses.Online, EndpointUpdateFrequency, EndpointUp

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

UpdateEndpointTimer = new Timer(
                new TimerCallback(UpdateSBEndpoints),
                Endpoint.Statuses.Online,
                EndpointUpdateFrequency,
                EndpointUpdateFrequency);
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();
   }
}
下面是我的更新方法的大致外观:

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
         }
         ...
}
现在,由于计时器使用线程池中的线程触发回调,我需要采取措施来控制线程。当多个线程可以在第一个线程完成工作之前从db抓取同一个端点时,就会出现一个特定的问题,这会导致在foreach循环中完成重复的工作

我知道这个问题有两种可能的解决办法,我想知道哪一种更好、更可取。解决方案是
ConcurrentDictionary
ManualResetEvent

在第一种情况下,我将把它放在我的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);
在第二种情况下,我会像这样控制线程:

UpdateEndpointTimer = new Timer(
                new TimerCallback(UpdateSBEndpoints),
                Endpoint.Statuses.Online,
                EndpointUpdateFrequency,
                EndpointUpdateFrequency);
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
,因为它允许对线程进行更精细的过滤,即不允许两个线程使用特定端点,而不允许两个线程使用特定端点类型(ManualResetEvents中的PUB和SUB)。可能还有比我更好的解决方案,因此我们将非常感谢您提供任何信息。

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


您似乎有一个相当标准的生产者/消费者场景。在这种情况下,在我看来,与其使用
ConcurrentDictionary
,不如使用
ConcurrentQueue
。您的生产者会将事物排队,而您的消费线程会将它们排出来进行处理。

我会采取不同的做法;我会使用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);
        }
    }
}
类程序
{
专用静态并发包(bag);;
公共静态void Main()
{
//构造并填充ConcurrentBag
_bag=新的ConcurrentBag();
var bagPopulateTask=新任务(PopulateBag);
bagPopulateTask.Start();
var bageptytask1=新任务(EmptyBag);
var bagEmptyTask2=新任务(EmptyBag);
var bageptytask3=新任务(EmptyBag);
bageptytask1.Start();
bageptytask2.Start();
bageptytask3.Start();
Console.ReadKey();
}
私有静态void EmptyBag()
{
int i;
while(true)
{
如果(_bag.TryTake(out i))
{
WriteLine(“线程{0}取{1}”,Thread.CurrentThread.ManagedThreadId,i);
}
其他的
{
WriteLine(“线程{0}什么也没拿”,Thread.CurrentThread.ManagedThreadId);
}
睡眠(50);
}
}
私有静态void PopulateBag()
{
var i=0;
而(i<1000)
{
WriteLine(“线程{0}添加了{1}”,Thread.CurrentThread.ManagedThreadId,i);
_添加(i);
i++;
睡眠(i);
}
}
}

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

我应该提供有关更新方法的更多详细信息。基本上,它会定期在数据库中搜索未处理的端点,当它找到它们时,会根据端点类型创建服务总线发布者和订阅者。我认为我应该你的意思是什么,但现在我想知道如何处理循环更新。如果我放弃计时器(它本质上是多线程的,没有提供对线程的控制),保留自动更新功能的最佳方式是什么?不管怎样,非常感谢您不鼓励使用ManualResetEvent。我认为没有必要放弃使用计时器。从原始帖子中我不清楚为什么要同时处理更新。但是假设Hans推断是因为计时器如果处理单个计时器事件的速度比处理单个计时器事件的速度快,那么他的建议应该有效。或者,您可以将间隔延长(在仍在同步的情况下,以防您获得对事件的并发处理)。只需将计时器的period参数设置为0。Call Change()在您的方法结束时重新启动它。现在它不会重叠。如果我正确理解您的问题,计时器将在第一次回调完成之前再次启动。您可以使用我在回答此问题时介绍的技术来防止这种情况: