简体   繁体   English

给定一个可以停止和启动的外部生产者 API,当本地缓冲区已满时有效地停止生产者

[英]Given an external producer API that can be stopped and started, efficiently stop the producer when local buffer is full

Suppose I am provided with an event producer API consisting of Start() , Pause() , and Resume() methods, and an ItemAvailable event.假设我获得了一个事件生成器 API,它由Start()Pause()Resume()方法以及一个ItemAvailable事件组成。 The producer itself is external code, and I have no control over its threading.生产者本身是外部代码,我无法控制它的线程。 A few items may still come through after Pause() is called (the producer is actually remote, so items may already be in flight over the.network).在调用Pause()后,一些项目可能仍会通过(生产者实际上是远程的,因此项目可能已经在 .network 上运行)。

Suppose also that I am writing consumer code, where consumption may be slower than production.还假设我正在编写消费者代码,其中消费可能比生产慢。

Critical requirements are关键要求是

  1. The consumer event handler must not block the producer thread, and消费者事件处理程序不得阻塞生产者线程,并且
  2. All events must be processed (no data can be dropped).必须处理所有事件(不能丢弃任何数据)。

I introduce a buffer into the consumer to smooth out some burstiness.我在消费者中引入了一个缓冲区来消除一些突发性。 But in the case of extended burstiness, I want to call Producer.Pause() , and then Resume() at an appropriate time, to avoid running out of memory at the consumer side.但是在扩展突发的情况下,我想调用Producer.Pause() ,然后在适当的时候调用Resume() ,以避免在消费者端用完 memory 。

I have a solution making use of Interlocked to increment and decrement a counter, which is compared to a threshold to decide whether it is time to Pause or Resume .我有一个使用Interlocked来递增和递减计数器的解决方案,该计数器与阈值进行比较以决定是时候Pause还是Resume

Question: Is there a better solution than the Interlocked counter ( int current in the code below), in terms of efficiency (and elegance)?问题:在效率(和优雅)方面,是否有比Interlocked计数器(下面代码中的int current )更好的解决方案?

Updated MVP (no longer bounces off the limiter):更新的 MVP(不再反弹限制器):

namespace Experiments
{
    internal class Program
    {
        // simple external producer API for demo purposes
        private class Producer
        {
            public void Pause(int i) { _blocker.Reset(); Console.WriteLine($"paused at {i}"); }
            public void Resume(int i) { _blocker.Set(); Console.WriteLine($"resumed  at {i}"); }
            public async Task Start()
            {
                await Task.Run
                (
                    () =>
                    {
                        for (int i = 0; i < 10000; i++)
                        {
                            _blocker.Wait();
                            ItemAvailable?.Invoke(this, i);
                        }
                    }
                );
            }

            public event EventHandler<int> ItemAvailable;
            private ManualResetEventSlim _blocker = new(true);
        }

        private static async Task Main(string[] args)
        {
            var p = new Producer();
            var buffer = Channel.CreateUnbounded<int>(new UnboundedChannelOptions { SingleWriter = true });
            int threshold = 1000;
            int resumeAt = 10;
            int current = 0;
            int paused = 0;

            p.ItemAvailable += (_, i) =>
            {
                if (Interlocked.Increment(ref current) >= threshold
                    && Interlocked.CompareExchange(ref paused, 0, 1) == 0
                ) p.Pause(i);

                buffer.Writer.TryWrite(i);
            };

            var processor = Task.Run
            (
                async () =>
                {
                    await foreach (int i in buffer.Reader.ReadAllAsync())
                    {
                        Console.WriteLine($"processing {i}");
                        await Task.Delay(10);
                        if
                        (
                            Interlocked.Decrement(ref current) < resumeAt
                            && Interlocked.CompareExchange(ref paused, 1, 0) == 1
                        ) p.Resume(i);
                    }
                }
            );

            p.Start();
            await processor;
        }
    }
}

If you are aiming at elegance, you could consider baking the pressure-awareness functionality inside a custom Channel<T> .如果您的目标是优雅,您可以考虑在自定义Channel<T>中烘焙压力感知功能。 Below is a PressureAwareUnboundedChannel<T> class that derives from the Channel<T> .下面是派生自Channel<T>PressureAwareUnboundedChannel<T> class。 It offers all the functionality of the base class, plus it emits notifications when the channel becomes under pressure, and when the pressure is relieved.它提供基本 class 的所有功能,此外,它还会在通道承受压力和释放压力时发出通知。 The notifications are pushed through anIProgress<bool> instance, that emits a true value when the pressure surpasses a specific high-threshold, and a false value when the pressure drops under a specific low-threshold.通知通过IProgress<bool>实例推送,当压力超过特定的高阈值时发出true值,当压力下降到特定的低阈值以下时发出false值。

public sealed class PressureAwareUnboundedChannel<T> : Channel<T>
{
    private readonly Channel<T> _channel;
    private readonly int _highPressureThreshold;
    private readonly int _lowPressureThreshold;
    private readonly IProgress<bool> _pressureProgress;
    private int _pressureState = 0; // 0: no pressure, 1: under pressure

    public PressureAwareUnboundedChannel(int lowPressureThreshold,
        int highPressureThreshold, IProgress<bool> pressureProgress)
    {
        if (lowPressureThreshold < 0)
            throw new ArgumentOutOfRangeException(nameof(lowPressureThreshold));
        if (highPressureThreshold < lowPressureThreshold)
            throw new ArgumentOutOfRangeException(nameof(highPressureThreshold));
        if (pressureProgress == null)
            throw new ArgumentNullException(nameof(pressureProgress));
        _highPressureThreshold = highPressureThreshold;
        _lowPressureThreshold = lowPressureThreshold;
        _pressureProgress = pressureProgress;
        _channel = Channel.CreateBounded<T>(Int32.MaxValue);
        this.Writer = new ChannelWriter(this);
        this.Reader = new ChannelReader(this);
    }

    private class ChannelWriter : ChannelWriter<T>
    {
        private readonly PressureAwareUnboundedChannel<T> _parent;

        public ChannelWriter(PressureAwareUnboundedChannel<T> parent)
            => _parent = parent;
        public override bool TryComplete(Exception error = null)
            => _parent._channel.Writer.TryComplete(error);
        public override bool TryWrite(T item)
        {
            bool success = _parent._channel.Writer.TryWrite(item);
            if (success) _parent.SignalWriteOrRead();
            return success;
        }
        public override ValueTask<bool> WaitToWriteAsync(
            CancellationToken cancellationToken = default)
                => _parent._channel.Writer.WaitToWriteAsync(cancellationToken);
    }

    private class ChannelReader : ChannelReader<T>
    {
        private readonly PressureAwareUnboundedChannel<T> _parent;

        public ChannelReader(PressureAwareUnboundedChannel<T> parent)
            => _parent = parent;
        public override Task Completion => _parent._channel.Reader.Completion;
        public override bool CanCount => _parent._channel.Reader.CanCount;
        public override int Count => _parent._channel.Reader.Count;
        public override bool TryRead(out T item)
        {
            bool success = _parent._channel.Reader.TryRead(out item);
            if (success) _parent.SignalWriteOrRead();
            return success;
        }
        public override ValueTask<bool> WaitToReadAsync(
            CancellationToken cancellationToken = default)
                => _parent._channel.Reader.WaitToReadAsync(cancellationToken);
    }

    private void SignalWriteOrRead()
    {
        var currentCount = _channel.Reader.Count;
        bool underPressure;
        if (currentCount > _highPressureThreshold)
            underPressure = true;
        else if (currentCount <= _lowPressureThreshold)
            underPressure = false;
        else
            return;
        int newState = underPressure ? 1 : 0;
        int oldState = underPressure ? 0 : 1;
        if (Interlocked.CompareExchange(
            ref _pressureState, newState, oldState) != oldState) return;
        _pressureProgress.Report(underPressure);
    }
}

The encapsulated Channel<T> is actually a bounded channel, having capacity equal to the maximum Int32 value,封装的Channel<T>实际上是一个有界通道,容量等于最大Int32值, because only bounded channels implement the Reader.Count property.因为只有有界通道会实现Reader.Count属性。 ¹ ¹

Usage example:使用示例:

var progress = new Progress<bool>(underPressure =>
{
    if (underPressure) Producer.Pause(); else Producer.Resume();
});
var channel = new PressureAwareUnboundedChannel<Item>(500, 1000, progress);

In this example the Producer will be paused when the items stored inside the channel become more than 1000, and it will be resumed when the number of items drops to 500 or less.在这个例子中,当频道内存储的项目超过 1000 时, Producer将暂停,当项目数量下降到 500 或更少时,它将恢复。

The Progress<bool> action is invoked on the context that was captured at the time of the Progress<bool> 's creation. Progress<bool>操作在创建Progress<bool>时捕获的上下文中调用。 So if you create it on the UI thread of a GUI application, the action will be invoked on the UI thread, otherwise in will be invoked on the ThreadPool .因此,如果您在 GUI 应用程序的 UI 线程上创建它,该操作将在 UI 线程上调用,否则将在ThreadPool上调用。 In the later case there will be no protection against overlapping invocations of the Action<bool> .在后一种情况下,将无法防止Action<bool>的重叠调用。 If the Producer class is not thread-safe, you'll have to add synchronization inside the handler.如果Producer class 不是线程安全的,则必须在处理程序中添加同步。 Example:例子:

var progress = new Progress<bool>(underPressure =>
{
    lock (Producer) if (underPressure) Producer.Pause(); else Producer.Resume();
});

¹ Actually unbounded channels also support the Count property, unless they are configured with the SingleReader option. ¹实际上,无限通道也支持Count属性,除非它们配置了SingleReader选项。

This is relatively straightforward if you realize there are three "steps" in this problem.如果您意识到这个问题有三个“步骤”,那么这就相对简单了。

  1. The first step ToChannel(Producer) receives messages from the producer.第一步ToChannel(Producer)从生产者那里接收消息。
  2. The next step, PauseAt signals pause() if there are too many pending items in the out panel.下一步,如果输出面板中有太多未决项目, PauseAt发出pause()信号。
  3. The third step, ResumeAt signals resume() if its input channel has a count less than a threshold.第三步,如果ResumeAt的输入通道的计数小于阈值,ResumeAt 会向resume()发出信号。

It's easy to combine all three steps using typical Channel patterns.使用典型的通道模式很容易将所有三个步骤结合起来。


producer.ToChannel(token)
    .PauseAt(1000,()=>producer.PauseAsync(),token)
    .ResumeAt(10,()=>producer.ResumeAsync(),token)
    ....

Or a single, generic TrafficJam method:或者一个单一的、通用的TrafficJam方法:

static ChannelReader<T> TrafficJam(this ChannelReader<T> source,
    int pauseAt,int resumeAt,
    Func<Task> pause,Func<Task> resume,
    CancellationToken token=default)
{
    return source
             .PauseAt(pauseAt,pause,token)
             .ResumeAt(resumeAt,resume,token);
}

ToChannel到频道

The first step is relatively straightforward, an unbounded Channel source based from the producer's events.第一步相对简单,基于生产者事件的无界通道源。

static ChannelReader<int> ToChannel(this Producer producer,
                                    CancellationToken token=default)
{
    Channel<int> channel=Channel.CreateUnbounded();
    var writer=channel.Writer;
    producer.ItemAvailable += OnItem;
    return channel;

    void OnItem(object sender, int item)
    {
        writer.TryWriteAsync(item);
        if(token.IsCancellationRequested)
        {
            producer.ItemAvailable-=OnItem;
            writer.Complete();
            
        }
    }
}

The only unusual part is using a local function to allow disabling the event handler and completing the output channel when cancellation is requested唯一不寻常的部分是使用本地 function 来允许禁用事件处理程序并在请求取消时完成 output 通道

That's enough to queue all the incoming items.这足以让所有传入的项目排队。 ToChannel doesn't bother with starting, pausing etc, that's not its job. ToChannel不会为启动、暂停等而烦恼,这不是它的工作。

PauseAt暂停时间

The next function, PauseAt , uses a BoundedChannel to implement the threshold.接下来的 function, PauseAt ,使用 BoundedChannel 来实现阈值。 It forwards incoming messages if it can.如果可以,它会转发传入的消息。 If the channel can't accept any more messages it calls the pause callback and a waits until it can resume forwarding:如果通道不能再接受任何消息,它会调用pause回调等待直到它可以恢复转发:

static ChannelReader<T> PauseAt(this ChannelReader<T> source, 
        int threshold, Func<Task> pause,
        CancellationToken token=default)
{
    Channel<T> channel=Channel.CreateBounded(threshold);
    var writer=channel.Writer;

    _ = Task.Run(async ()=>
        await foreach(var msg in source.ReadAllAsync(token))
        {
            if(writer.CanWrite())
            {
               await writer.WriteAsync(msg);
            }
            else
            {
               await pause();
               //Wait until we can post again
               await writer.WriteAsync(msg);
            }
        }
    },token)
    .ContinueWith(t=>writer.TryComplete(t.Exception));

    return channel;
}

ResumeAt恢复时间

The final step, ResumeAt , calls resume() if its input was previously above the threshold and now has fewer items.最后一步ResumeAt调用resume()如果它的输入先前高于阈值并且现在有更少的项目。

If the input isn't bounded, it just forwards all messages.如果输入不受限制,它只会转发所有消息。

static ChannelReader<T> ResumeAt(this ChannelReader<T> source, 
        int resumeAt, Func<Task> resume,
        CancellationToken token=default)
{
    Channel<T> channel=Channel.CreateUnbounded();
    var writer=channel.Writer;

    _ = Task.Run(async ()=>{
        bool above=false;
        await foreach(var msg in source.ReadAllAsync(token))
        {
            await writer.WriteAsync(msg);
            //Do nothing if the source isn't bounded
            if(source.CanCount)
            {
                if(above && source.Count<=resumeAt)
                {
                    await resume();
                    above=false;
                }       
                above=source.Count>resumeAt;  
            }
       }
    },token)
    .ContinueWith(t=>writer.TryComplete(t.Exception));

    return channel;
}

Since only a single thread is used, we can keep count of the previous count.由于只使用了一个线程,我们可以保留之前的计数。 and whether it was above or below the threshold.以及它是高于还是低于阈值。

Combining Pause and Resume结合暂停和恢复

Since Pause and Resume work with just channels, they can be combined into a single method:由于PauseResume仅适用于通道,因此可以将它们组合成一个方法:

static ChannelReader<T> TrafficJam(this ChannelReader<T> source,
    int pauseAt,int resumeAt,
    Func<Task> pause,Func<Task> resume,
    CancellationToken token=default)
{
    return source.PauseAt(pauseAt,pause,token)
             .ResumeAt(resumeAt,resume,token);
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

相关问题 生产者/消费者,流缓冲问题 - Producer/Consumer, Stream buffer problem 这对于生产者/消费者唯一的键控缓冲区有很好的暗示吗? - Is this a good impl for a Producer/Consumer unique keyed buffer? 如何知道何时停止并行foreach,其中消费者也是C#中的生产者 - How to know when to stop a parallel foreach where the consumer is also the producer in C# 本地计算机上的服务启动然后停止 - The service on Local Computer started then stopped 使用Queue c#.NET为Consumer和Producer线程创建缓冲区 - Creating a buffer for Consumer and Producer threads using Queue c# .NET 本地计算机上的此服务已启动,然后停止。 如果某些服务未被其他服务或程序使用,则会自动停止 - This service on local computer started and then stopped. some services stop automatically if then are not in use by other servces or programs 本地计算机上的服务启动,然后停止。 如果某些服务未被其他服务或程序使用,则会自动停止 - The service on Local Computer started and then stopped. Some services stop automatically if they are not in use by other services or programs 生产者/消费者模式与批量生产者 - Producer/Consumer pattern with a batched producer 生产者 - 消费者在队列为空时等待? - Producer-Consumer waiting when queue is empty? 生产者消费模式时产品很多 - Producer consumer pattern when many products
 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM