Executing event at regular interval with .NET 4.5

2019-08-27 17:46发布

问题:

I'm building a small stat client library in .NET 4.5 that needs to collect data in a queue and then periodically process the data from the queue by sending some HTTP messages. I'm looking for some guidance on the best way of implementing the periodic portion of this code.

I figured using the ConcurrentQueue class makes the most sense to hold the data and a simplified version of the client class looks like this:

public class StatClient
{
    private static readonly ConcurrentQueue<Stat> Stats = new ConcurrentQueue<Stat>();

    public void RecordCount(string name, long count)
    {
        Stats.Enqueue(new CounterStat(name, count));
    }
}

So what is the preferred method of triggering a background process every 10 seconds that will read from the queue? Should I just use a System.Timers.Timer and then kick off a new thread or task, or create a background thread on start-up and have it sleep for 10 seconds, or ???

回答1:

I would suggest that you use BlockingCollection<stat> rather than ConcurrentQueue<Stat>. And rather than having a periodic task that services the queue, just have a consumer thread that's continually looking at the queue. For example:

BlockingCollection<Stat> _stats = new BlockingCollection<Stat>();

public void RecordCount(string name, long count)
{
    Stats.Add(new CounterStat(name, count));
}

Your consumer looks like this:

public void Consumer()
{
    foreach (var stat in _stats.GetConsumingEnumerable())
    {
        // process stat
    }
}

And you can start the consumer as a thread or as a task. As a task, for example:

Task consumer = Task.Factory.StartNew(Consumer, TaskCreationOptions.LongRunning);

The consumer will block, waiting for something to be added to the queue. As soon as something is added, the consumer will dequeue and process it.

To stop the consumer, call _stats.CompleteAdding(). That will signal that no more items will be added to the queue. The consumer will empty the queue and then exit the loop.

If you really want to use a periodic task, then I would suggest using a timer. But make the timer a one-shot so that you can't have concurrent invocations. You'll reset the timer after every tick. Like this:

// create the timer
System.Timers.Timer timer = new Timer();
timer.Elapsed += TimerTick;
timer.Interval = 10000;
timer.AutoReset = false;  // makes a one-shot timer rather than a periodic timer
timer.Enabled = true;

void TimerTick(object sender, EventArgs e)
{
    // do something with the queue
    // then reset the timer
    timer.Enabled = true;
}

Whichever way you go, I strongly suggest using BlockingCollection instead of ConcurrentQueue. The API for BlockingCollection is a lot easier to work with, and includes nice things like non-busy waits, GetConsumingEnumerable, etc. Much more robust.



回答2:

I think having one background process that sleeps, as you suggested, would work. In my opinion, that's less complex than having a timer raising events and kicking off separate background processes each time because you might run into concurrency issues if a previously-spawned process overlaps a new one and is fighting for access to the resource -- your ConcurrentQueue. Granted, you could always use a mutex to coordinate access that resource, but still, the other way is less complex.