Multi-Thread Processing in .NET

左心房为你撑大大i 提交于 2020-01-14 06:35:08

问题


I already have a few ideas, but I'd like to hear some differing opinions and alternatives from everyone if possible.

I have a Windows console app that uses Exchange web services to connect to Exchange and download e-mail messages. The goal is to take each individual message object, extract metadata, parse attachments, etc. The app is checking the inbox every 60 seconds. I have no problems connecting to the inbox and getting the message objects. This is all good.

Here's where I am accepting input from you: When I get a message object, I immediately want to process the message and do all of the busy work explained above. I was considering a few different approaches to this:

  • Queuing the e-mail objects up in a table and processing them one-by-one.
  • Passing the e-mail object off to a local Windows service to do the busy work.

I don't think db queuing would be a good approach because, at times, multiple e-mail objects need to be processed. It's not fair if a low-priority e-mail with 30 attachments is processed before a high-priority e-mail with 5 attachments is processed. In other words, e-mails lower in the stack shouldn't need to wait in line to be processed. It's like waiting in line at the store with a single register for the bonehead in front of you to scan 100 items. It's just not fair. Same concept for my e-mail objects.

I'm somewhat unsure about the Windows service approach. However, I'm pretty confident that I could have an installed service listening, waiting on demand for an instruction to process a new e-mail. If I have 5 separate e-mail objects, can I make 5 separate calls to the Windows service and process without collisions?

I'm open to suggestions or alternative approaches. However, the solution must be presented using .NET technology stack.


回答1:


One option is to do the processing in the console application. What you have looks like a standard producer-consumer problem with one producer (the thread that gets the emails) and multiple consumers. This is easily handled with BlockingCollection.

I'll assume that your message type (what you get from the mail server) is called MailMessage.

So you create a BlockingCollection<MailMessage> at class scope. I'll also assume that you have a timer that ticks every 60 seconds to gather messages and enqueue them:

private BlockingCollection<MailMessage> MailMessageQueue =
    new BlockingCollection<MailMessage>();

// Timer is created as a one-shot and re-initialized at each tick.
// This prevents the timer proc from being re-entered if it takes
// longer than 60 seconds to run.
System.Threading.Timer ProducerTimer = new System.Threading.Timer(
    TimerProc, null, TimeSpan.FromSeconds(60), TimeSpan.FromMilliseconds(-1));


void TimerProc(object state)
{
    var newMessages = GetMessagesFromServer();
    foreach (var msg in newMessages)
    {
        MailMessageQueue.Add(msg);
    }
    ProducerTimer.Change(TimeSpan.FromSeconds(60), TimeSpan.FromMilliseconds(-1));
}

Your consumer threads just read the queue:

void MessageProcessor()
{
    foreach (var msg in MailMessageQueue.GetConsumingEnumerable())
    {
        ProcessMessage();
    }
}

The timer will cause the producer to run once per minute. To start the consumers (say you want two of them):

var t1 = Task.Factory.StartNew(MessageProcessor, TaskCreationOptions.LongRunning);
var t2 = Task.Factory.StartNew(MessageProcessor, TaskCreationOptions.LongRunning);

So you'll have two threads processing messages.

It makes no sense to have more processing threads than you have available CPU cores. The producer thread presumably won't require a lot of CPU resources, so you don't have to dedicate a thread to it. It'll just slow down message processing briefly whenever it's doing its thing.

I've skipped over some detail in the description above, particularly cancellation of the threads. When you want to stop the program, but let the consumers finish processing messages, just kill the producer timer and set the queue as complete for adding:

MailMessageQueue.CompleteAdding();

The consumers will empty the queue and exit. You'll of course want to wait for the tasks to complete (see Task.Wait).

If you want the ability to kill the consumers without emptying the queue, you'll need to look into Cancellation.

The default backing store for BlockingCollection is a ConcurrentQueue, which is a strict FIFO. If you want to prioritize things, you'll need to come up with a concurrent priority queue that implements the IProducerConsumerCollection interface. .NET doesn't have such a thing (or even a priority queue class), but a simple binary heap that uses locks to prevent concurrent access would suffice in your situation; you're not talking about hitting this thing very hard.

Of course you'd need some way to prioritize the messages. Probably sort by number of attachments so that messages with no attachments are processed quicker. Another option would be to have two separate queues: one for messages with 0 or 1 attachments, and a separate queue for those with lots of attachments. You could have one of your consumers dedicated to the 0 or 1 queue so that easy messages always have a good chance of being processed first, and the other consumers take from the 0 or 1 queue unless it's empty, and then take from the other queue. It would make your consumers a little more complicated, but not hugely so.

If you choose to move the message processing to a separate program, you'll need some way to persist the data from the producer to the consumer. There are many possible ways to do that, but I just don't see the advantage of it.




回答2:


I'm somewhat a novice here, but it seems like an initial approach could be to have a separate high-priority queue. Every time a worker is available to obtain a new message, it could do something like:

If DateTime.Now - lowPriorityQueue.Peek.AddedTime < maxWaitTime Then
    ProcessMessage(lowPriorityQueue.Dequeue())
Else If highPriorityQueue.Count > 0 Then
    ProcessMessage(highPriorityQueue.Dequeue())
Else
    ProcessMessage(lowPriorityQueue.Dequeue())
End If

In a single thread, while you can still have one message blocking the others, higher priority messages could be processed sooner.

Depending on how fast most messages get processed, the application could create a new worker on a new thread if the queues are getting too big or too old.

Please tell me if I'm completely off-base here though.



来源:https://stackoverflow.com/questions/19821344/multi-thread-processing-in-net

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!