[英]Best practice for task/await in a foreach loop
我在使用task / await的foreach中有一些耗時的代碼。 它包括從數據庫中提取數據,生成html,將其發布到API,以及保存對數據庫的回復。
模擬看起來像這樣
List<label> labels = db.labels.ToList();
foreach (var x in list)
{
var myLabels = labels.Where(q => !db.filter.Where(y => x.userid ==y.userid))
.Select(y => y.ID)
.Contains(q.id))
//Render the HTML
//do some fast stuff with objects
List<response> res = await api.sendMessage(object); //POST
//put all the responses in the db
foreach (var r in res)
{
db.responses.add(r);
}
db.SaveChanges();
}
時間方面,生成Html並將其發布到API似乎占用了大部分時間。
理想情況下,如果我可以為下一個項目生成HTML,並在發布下一個項目之前等待帖子完成,那將會很棒。
其他想法也歡迎。 怎么會這樣呢?
我首先考慮在foreach
之上添加一個Task
並在下一次POST之前等待它完成,但是我如何處理最后一個循環...它感覺很亂......
您可以並行執行此操作,但每個任務中都需要不同的上下文。
實體框架不是線程安全的,因此如果您不能在並行任務中使用一個上下文。
var tasks = myLabels.Select( async label=>{
using(var db = new MyDbContext ()){
// do processing...
var response = await api.getresponse();
db.Responses.Add(response);
await db.SaveChangesAsync();
}
});
await Tasks.WhenAll(tasks);
在這種情況下,所有任務看起來都是並行運行的,每個任務都有自己的上下文。
如果不為每個任務創建新的Context,則會在此問題上提到錯誤Entity Framework是否支持並行異步查詢?
這是一個架構問題,而不是代碼問題,imo。
您可以將您的工作分成兩個獨立的部分:
您可以並行運行它們,並使用隊列來協調:當HTML准備就緒時,它會被添加到隊列中,另一個工作人員從那里繼續,獲取該HTML並發送到API。
這兩個部分也可以以多線程方式完成,例如,您可以通過讓一組工作人員在隊列中查找要處理的項目來同時處理隊列中的多個項目。
這對生產者/消費者模式感到尖叫:一個生產者以不同於消費者消費的速度生產數據。 一旦生產者不再生產任何東西,它就會通知消費者不再需要數據。
MSDN有一個很好的例子,這個模式將幾個數據流塊鏈接在一起:一個塊的輸出是另一個塊的輸入。
這個想法如下:
<T
>類的對象 <T
>。 該類將此公開為get屬性: 編碼:
class MyProducer<T>
{
private System.Threading.Tasks.Dataflow.BufferBlock<T> bufferBlock = new BufferBlock<T>();
public ISourceBlock<T> Output {get {return this.bufferBlock;}
public async ProcessAsync()
{
while (somethingToProduce)
{
T producedData = ProduceOutput(...)
await this.bufferBlock.SendAsync(producedData);
}
// no date to send anymore. Mark the output complete:
this.bufferBlock.Complete()
}
}
編碼:
public class MyConsumer<T>
{
ISourceBlock<T> Source {get; set;}
public async Task ProcessAsync()
{
while (await this.Source.OutputAvailableAsync())
{ // there is input of type T, read it:
var input = await this.Source.ReceiveAsync();
// process input
}
// if here, no more input expected. finish.
}
}
現在把它放在一起:
private async Task ProduceOutput<T>()
{
var producer = new MyProducer<T>();
var consumer = new MyConsumer<T>() {Source = producer.Output};
var producerTask = Task.Run( () => producer.ProcessAsync());
var consumerTask = Task.Run( () => consumer.ProcessAsync());
// while both tasks are working you can do other things.
// wait until both tasks are finished:
await Task.WhenAll(new Task[] {producerTask, consumerTask});
}
為簡單起見,我遺漏了異常處理和取消。 StackOverFlow具有關於異常處理和取消任務的技巧:
這就是我最終使用的:( https://stackoverflow.com/a/25877042/275990 )
List<ToSend> sendToAPI = new List<ToSend>();
List<label> labels = db.labels.ToList();
foreach (var x in list) {
var myLabels = labels.Where(q => !db.filter.Where(y => x.userid ==y.userid))
.Select(y => y.ID)
.Contains(q.id))
//Render the HTML
//do some fast stuff with objects
sendToAPI.add(the object with HTML);
}
int maxParallelPOSTs=5;
await TaskHelper.ForEachAsync(sendToAPI, maxParallelPOSTs, async i => {
using (NasContext db2 = new NasContext()) {
List<response> res = await api.sendMessage(i.object); //POST
//put all the responses in the db
foreach (var r in res)
{
db2.responses.add(r);
}
db2.SaveChanges();
}
});
public static Task ForEachAsync<T>(this IEnumerable<T> source, int dop, Func<T, Task> body) {
return Task.WhenAll(
from partition in Partitioner.Create(source).GetPartitions(dop)
select Task.Run(async delegate {
using (partition)
while (partition.MoveNext()) {
await body(partition.Current).ContinueWith(t => {
if (t.Exception != null) {
string problem = t.Exception.ToString();
}
//observe exceptions
});
}
}));
}
基本上讓我生成HTML同步,這很好,因為生成1000只需要幾秒鍾,但讓我發布並保存到數據庫異步,使用盡可能多的線程和我預定義的。 在這種情況下,我發布到Mandrill API,並行帖子沒有問題。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.