如何使用C#任务并行库和IProducerConsumerCollection实现通用回调?
我有一个组件可以向基于web的API提交请求,但必须限制这些请求,以免违反API的数据限制。这意味着所有请求都必须通过队列来控制提交速率,但它们可以(而且应该)并发执行以实现最大吞吐量。每个请求必须在将来完成时的某个时刻向调用代码返回一些数据 我正在努力创建一个好的模型来处理数据的返回 使用如何使用C#任务并行库和IProducerConsumerCollection实现通用回调?,c#,.net,generics,task-parallel-library,blockingcollection,C#,.net,Generics,Task Parallel Library,Blockingcollection,我有一个组件可以向基于web的API提交请求,但必须限制这些请求,以免违反API的数据限制。这意味着所有请求都必须通过队列来控制提交速率,但它们可以(而且应该)并发执行以实现最大吞吐量。每个请求必须在将来完成时的某个时刻向调用代码返回一些数据 我正在努力创建一个好的模型来处理数据的返回 使用BlockingCollection我不能仅仅从Schedule方法返回Task,因为排队和退队过程都在缓冲区的两端。因此,我创建了一个RequestItem类型,其中包含Action形式的回调 其思想是,一
BlockingCollection
我不能仅仅从Schedule
方法返回Task
,因为排队和退队过程都在缓冲区的两端。因此,我创建了一个RequestItem
类型,其中包含Action
形式的回调
其思想是,一旦从队列中提取了一个项,就可以通过启动的任务调用回调,但到那时,我已经丢失了泛型类型参数,剩下的就是反射和各种各样的肮脏(如果可能的话)
例如:
public class RequestScheduler
{
private readonly BlockingCollection<IRequestItem> _queue = new BlockingCollection<IRequestItem>();
public RequestScheduler()
{
this.Start();
}
// This can't return Task<TResult>, so returns void.
// Instead RequestItem is generic but this poses problems when adding to the queue
public void Schedule<TResult>(RequestItem<TResult> request)
{
_queue.Add(request);
}
private void Start()
{
Task.Factory.StartNew(() =>
{
foreach (var item in _queue.GetConsumingEnumerable())
{
// I want to be able to use the original type parameters here
// is there a nice way without reflection?
// ProcessItem submits an HttpWebRequest
Task.Factory.StartNew(() => ProcessItem(item))
.ContinueWith(t => { item.Callback(t); });
}
});
}
public void Stop()
{
_queue.CompleteAdding();
}
}
public class RequestItem<TResult> : IRequestItem
{
public IOperation<TResult> Operation { get; set; }
public Action<Task<TResult>> Callback { get; set; }
}
公共类请求调度器
{
私有只读BlockingCollection _queue=new BlockingCollection();
公共请求调度程序()
{
这个。Start();
}
//这无法返回任务,因此返回void。
//相反,RequestItem是通用的,但在添加到队列时会出现问题
公共作废时间表(请求项请求)
{
_添加(请求);
}
私有void Start()
{
Task.Factory.StartNew(()=>
{
foreach(变量项在_queue.GetConsumingEnumerable()中)
{
//我希望能够在这里使用原始类型参数
//有没有一种不经思考的好方法?
//ProcessItem提交HttpWebRequest
Task.Factory.StartNew(()=>ProcessItem(项目))
.ContinueWith(t=>{item.Callback(t);});
}
});
}
公共停车场()
{
_queue.CompleteAdding();
}
}
公共类请求项:IRequestItem
{
公共操作操作{get;set;}
公共操作回调{get;set;}
}
当请求从缓冲区中提取并提交到API时,我如何继续缓冲请求,但将任务返回给客户端?首先,您可以从Schedule()
返回任务,您只需要使用它
其次,为了避开泛型问题,您可以将其全部隐藏在(非泛型)Action
s中。在Schedule()
中,使用lambda创建一个操作,该操作完全满足您的需要。然后,消费循环将执行该操作,它不需要知道内部是什么
第三,我不明白为什么在循环的每次迭代中都要开始一个新的任务。首先,这意味着你实际上不会得到任何节流
通过这些修改,代码可能如下所示:
public class RequestScheduler
{
private readonly BlockingCollection<Action> m_queue = new BlockingCollection<Action>();
public RequestScheduler()
{
this.Start();
}
private void Start()
{
Task.Factory.StartNew(() =>
{
foreach (var action in m_queue.GetConsumingEnumerable())
{
action();
}
}, TaskCreationOptions.LongRunning);
}
public Task<TResult> Schedule<TResult>(IOperation<TResult> operation)
{
var tcs = new TaskCompletionSource<TResult>();
Action action = () =>
{
try
{
tcs.SetResult(ProcessItem(operation));
}
catch (Exception e)
{
tcs.SetException(e);
}
};
m_queue.Add(action);
return tcs.Task;
}
private T ProcessItem<T>(IOperation<T> operation)
{
// whatever
}
}
公共类请求调度器
{
private readonly BlockingCollection m_queue=新建BlockingCollection();
公共请求调度程序()
{
这个。Start();
}
私有void Start()
{
Task.Factory.StartNew(()=>
{
foreach(m_queue.GetConsumingEnumerable()中的var操作)
{
动作();
}
},TaskCreationOptions.LongRunning);
}
公共任务计划(IOOperation操作)
{
var tcs=new TaskCompletionSource();
动作动作=()=>
{
尝试
{
tcs.SetResult(ProcessItem(操作));
}
捕获(例外e)
{
tcs.SetException(e);
}
};
m_queue.Add(操作);
返回tcs.Task;
}
私有T ProcessItem(IOOperation操作)
{
//随便
}
}
是否有任何方法可以传递回调操作,并使用lambda在具有所有可用类型的位置创建该操作?这是什么意思?动作委托不接受参数,如何返回数据?为什么调度程序调用回调?是否在ProcessItem
的定义中不调用回调?是的,我同意,上面的代码不起作用,只是一个草图,所以它当然可以在ProcessItem中调用。这将如何解决键入问题?@MalcomTucker,因为RequestScheduler
不需要知道结果的类型,因为它不调用回调。在ProcessItem
的定义中,您当然知道正确调用回调所需的结果类型(并且实际有结果)。非常感谢,我昨晚发现了TaskCompletionSource,并刚刚完成了一些工作,所以我很高兴我的思路是正确的。节流发生在BlockingCollection上的foreach
中,为了清楚起见,我省略了它。最后一个问题-为什么要使用LongRunning
选项?我的API调用大约为70毫秒-是否足够长的运行时间来使用此选项?@MalcomTucker每次API调用需要多长时间并不重要。重要的是整个任务需要多长时间。在我看来,这可能是一段很长的时间,其中大部分时间可能会花在等待getConsumineumerable()
返回另一项上。明白了,好的,是的,特定任务只是无限期地运行,将消息传输到API,只有在用户断开连接时才会关闭。。谢谢你的帮助,聪明的家伙(像往常一样!)