非阻塞和重复发生的生产者/消费者通知程序实现

时间:2014-08-11 20:25:46

标签: c# .net task-parallel-library async-await producer-consumer

努力搜索一段符合我想要的代码,我很满意。阅读thisthis有很多帮助。

我有一个场景,我需要单个消费者在新数据可用时通知单个生产者,但也希望定期通知消费者,无论新数据是否可用。 如果通知消费者的时间超过了重复发生的时间,那就没有问题,但不应该频繁通知消费者。

当消费者已经通知并正在工作时,可能会发生“新数据”的多个通知。 (所以SemaphoreSlim并不合适。 因此,比生产者通知速度慢的消费者不会排队后续通知,他们只会“重新发出信号”相同的“数据可用”标志而不受影响。

我还希望消费者异步等待通知(不阻塞线程)。

我将下面的类包裹在TaskCompletionSource并使用内部Timer。

public class PeriodicalNotifier : IDisposable
{
    // Need some dummy type since TaskCompletionSource has only the generic version
    internal struct VoidTypeStruct { }
    // Always reuse this allocation
    private static VoidTypeStruct dummyStruct;

    private TaskCompletionSource<VoidTypeStruct> internalCompletionSource;
    private Timer reSendTimer;

    public PeriodicalNotifier(int autoNotifyIntervalMs)
    {
        internalCompletionSource = new TaskCompletionSource<VoidTypeStruct>();
        reSendTimer = new Timer(_ => Notify(), null, 0, autoNotifyIntervalMs);
    }

    public async Task WaitForNotifictionAsync(CancellationToken cancellationToken)
    {
        using (cancellationToken.Register(() => internalCompletionSource.TrySetCanceled()))
        {
            await internalCompletionSource.Task;
            // Recreate - to be able to set again upon the next wait
            internalCompletionSource = new TaskCompletionSource<VoidTypeStruct>();
        }
    }

    public void Notify()
    {
        internalCompletionSource.TrySetResult(dummyStruct);
    }

    public void Dispose()
    {
        reSendTimer.Dispose();
        internalCompletionSource.TrySetCanceled();
    }
}

此类的用户可以执行以下操作:

private PeriodicalNotifier notifier = new PeriodicalNotifier(100);

// ... In some task - which should be non-blocking
while (some condition)
{
    await notifier.WaitForNotifictionAsync(_tokenSource.Token);
    // Do some work...
}

// ... In some thread, producer added new data
notifier.Notify();

效率对我很重要,场景是高频数据流,所以我想到了:

  • 等待的非阻塞性质。
  • 我认为Timer比重新创建Task.Delay更有效,如果不是要通知的话,取消它。
  • 重新考虑TaskCompletionSource

我的问题是:

  1. 我的代码是否正确解决了问题?任何隐藏的陷阱?
  2. 我是否错过了这个用例的一些简单解决方案/现有块?
  3. 更新

    我得出的结论是,除了重新实现更精益的任务完成结构(如herehere)之外,我没有更多的优化要做。希望能帮助任何人看到类似的场景。

2 个答案:

答案 0 :(得分:0)

  1. 是的,您的实施有意义但TaskCompletionSource娱乐应该在使用范围之外,否则“旧”取消令牌可能取消“新”TaskCompletionSource
  2. 我认为使用某种AsyncManualResetEvent结合Timer会更简单,更不容易出错。在Visual Studio SDK by Microsoft中有一个非常好的名称空间和异步工具。您需要install the SDK然后引用Microsoft.VisualStudio.Threading程序集。以下是使用AsyncManualResetEvent使用相同API的实现:
  3. public class PeriodicalNotifier : IDisposable
    {
        private readonly Timer _timer;
        private readonly AsyncManualResetEvent _asyncManualResetEvent;
    
        public PeriodicalNotifier(TimeSpan autoNotifyInterval)
        {
            _asyncManualResetEvent = new AsyncManualResetEvent();
            _timer = new Timer(_ => Notify(), null, TimeSpan.Zero, autoNotifyInterval);
        }
    
        public async Task WaitForNotifictionAsync(CancellationToken cancellationToken)
        {
            await _asyncManualResetEvent.WaitAsync().WithCancellation(cancellationToken);
            _asyncManualResetEvent.Reset();
        }
    
        public void Notify()
        {
            _asyncManualResetEvent.Set();
        }
    
        public void Dispose()
        {
            _timer.Dispose();
        }
    }
    

    通过设置重置事件进行通知,使用WaitAsync异步等待,使用WithCancellation扩展方法启用取消,然后重置事件。通过设置相同的重置事件来“合并”多个通知。

答案 1 :(得分:0)

Subject<Result> notifier = new Subject<Result)();

notifier 
    .Select(value => Observable.Interval(TimeSpan.FromMilliSeconds(100))
                                            .Select(_ => value)).Switch()
    .Subscribe(value => DoSomething(value));

//Some other thread...
notifier.OnNext(...);

此Rx查询将每隔100毫秒继续发送值,直到新值出现。然后我们每100毫秒通知一次该值。

如果我们每100毫秒收到一次超过一次的值,那么我们的输出基本上与输入相同。

相关问题