[英]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关键要求是
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 an
IProgress<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.如果您意识到这个问题有三个“步骤”,那么这就相对简单了。
ToChannel(Producer)
receives messages from the producer.ToChannel(Producer)
从生产者那里接收消息。PauseAt
signals pause()
if there are too many pending items in the out panel.PauseAt
发出pause()
信号。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:由于
Pause
和Resume
仅适用于通道,因此可以将它们组合成一个方法:
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.