作为使用this code的库的一部分,有一个SimpleQueue
类可以将生产者与消费者分开:
private class SimpleQueue
{
private readonly Func<ResolvedEvent, CancellationToken, Task> _onResolvedEvent;
private readonly CancellationToken _token;
private readonly ConcurrentQueue<ResolvedEvent> _events;
private readonly InterlockedBoolean _isPushing;
private static readonly ILog s_logger;
static SimpleQueue()
{
s_logger = LogProvider.For<SimpleQueue>();
}
public SimpleQueue(Func<ResolvedEvent, CancellationToken, Task> onResolvedEvent, CancellationToken token)
{
_onResolvedEvent = onResolvedEvent;
_token = token;
_events = new ConcurrentQueue<ResolvedEvent>();
_isPushing = new InterlockedBoolean();
}
public void Enqueue(ResolvedEvent resolvedEvent)
{
_events.Enqueue(resolvedEvent);
Push();
}
private void Push()
{
if(_isPushing.CompareExchange(true, false))
{
return;
}
Task.Run(async () =>
{
ResolvedEvent resolvedEvent;
while (!_token.IsCancellationRequested && _events.TryDequeue(out resolvedEvent))
{
try
{
await _onResolvedEvent(resolvedEvent, _token);
}
catch(Exception ex)
{
s_logger.ErrorException(ex.Message, ex);
}
}
_isPushing.Set(false);
}, _token);
}
}
我想我可以在这里看到一个问题,如果:
events.TryDequeue(out resolvedEvent))
会返回false
Push()
被调用,但在_isPushing
为true
时立即返回_isPushing
设置为false
,任务退出在这种情况下,队列中的事件将被调度,直到下一个入队并在Push()
中循环出队。如果是这样,我认为我不喜欢这个。
所以我改写使用TPL BlockingQueue:
public class SimpleQueue<T>
{
readonly BufferBlock<T> _queue = new BufferBlock<T>();
public SimpleQueue(Func<T, CancellationToken, Task> onItemQueued, CancellationToken token)
{
Task.Run(async () =>
{
while (true)
{
try
{
var item = await _queue.ReceiveAsync(token);
if (token.IsCancellationRequested)
return;
await onItemQueued(item, token);
}
catch (Exception ex)
{
// log
}
}
}, token);
}
public void Enqueue(T item)
{
_queue.Post(item);
}
}
class Program
{
private readonly static SimpleQueue<string> Queue;
private readonly static CancellationToken CancellationToken = new CancellationToken();
static async Task OnEvent(string item, CancellationToken cancellationToken)
{
await Task.Run(() =>
{
Console.WriteLine("Rx from remote {0}", item);
}, cancellationToken);
}
static Program()
{
Queue = new SimpleQueue<string>(OnEvent, CancellationToken);
}
static void Main(string[] args)
{
// wire up code to call ExternalReceive from 3rd party lib
DevLinkImports.DLRegisterType2CallDeltas(0,CallEvent);
Console.ReadLine();
}
// this is called by 3rd party dll on demand
static void CallEvent(uint pbxh, string info)
{
// we must dispatch and return within 50ms or 3rd party lib will go ape
Queue.Enqueue(info);
}
问题:
出于学习目的,我在查看原始SimpleQueue的问题时是否正确,可以根据时间选择保留项目?
如果没有“过早优化”,我觉得有必要问一下,为static async Task OnEvent(string item, CancellationToken cancellationToken)
的每次调用启动新线程的开销是多少?
通过重写,我在休眠时不会保持线程打开,但实际上使用此异步调用有任何好处,或者只是改为使用BlockingCollection
并在出列时阻塞?我不想保存一个线程来牺牲启动新线程所花费的时间。
答案 0 :(得分:2)
- 出于学习目的,我在查看原始SimpleQueue的问题时是否正确,可以根据时间选择项目?
醇>
无法肯定地说,因为此处未提供InterlockedBoolean
的实施。您的关注点似乎有效,但在尝试做出明确的陈述之前,我希望看到实际的代码。
- 如果没有“过早优化”,我觉得唯一明智的做法是,为每次调用静态异步任务OnEvent(字符串项,CancellationToken cancellationToken)调整新线程的开销是多少?
醇>
创建新线程的开销很大。但是您的OnEvent()
方法可能会或可能不会实际执行此操作。您正在创建一个新任务,然后调度程序将决定如何处置它。如果线程池包含用于执行它的可用线程和/或调度程序决定它可以等待现有但很繁忙的线程变为可用,则不会创建新线程。
- 使用重写我睡觉时不会保持线程打开,但实际上使用这个异步调用有什么好处,或者只是旋转一个线程并使用BlockingCollection并阻塞出队?我不想保存一个线程来牺牲启动新线程所花费的时间。
醇>
在您的程序中添加单个线程来为队列提供服务并不是那么糟糕。你只需创建一次,因此开销无关紧要。它确实为堆栈占用了一兆字节(默认情况下),但这通常也不会成为问题。
另一方面,由于使用了线程池,同样调用Task.Run()
也不太可能导致大量开销。
所以对我来说,它归结为美学和可维护性。
我要指出,使用BlockingCollection<T>
vs BufferBlock<T>
的问题与OnEvent()
的实施问题有些不同。前者涉及底层队列的实现,而后者涉及事件实际上已经出列时发生的事情。即使您使用BlockingCollection<T>
,如果您不更改OnEvent()
,您仍然会为每个事件开始新任务。相反,即使使用OnEvent()
,也无法让BufferBlock<T>
同步运行事件处理。
队列代码显然期望异步处理事件,但事件不一定如此。这取决于队列的客户端。