foreach循环中任务/等待的最佳实践

时间:2015-07-30 03:14:17

标签: c# asp.net-mvc optimization async-await

我在使用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,并在发布下一个项目之前等待帖子完成,那就太棒了。

其他想法也欢迎。 怎么会这样呢?

我首先考虑在Task之上添加一个foreach并在下一次POST之前等待它完成,但是我如何处理最后一个循环...它感觉很乱......

4 个答案:

答案 0 :(得分:4)

您可以并行执行此操作,但每个任务中都需要不同的上下文。

实体框架不是线程安全的,所以如果你不能在并行任务中使用一个上下文。

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);

在这种情况下,所有任务看起来都是并行运行的,每个任务都有自己的上下文。

如果您没有为每个任务创建新的上下文,则会在此问题上提及错误Does Entity Framework support parallel async queries?

答案 1 :(得分:2)

这是一个架构问题,而不是代码问题,imo。

您可以将您的工作分成两个独立的部分:

  1. 从数据库获取数据并生成HTML
  2. 发送API请求并保存对数据库的响应
  3. 您可以并行运行它们,并使用队列来协调:当HTML准备就绪时,它会被添加到队列中,另一个工作人员从那里继续,获取该HTML并发送到API。

    这两个部分也可以以多线程方式完成,例如:您可以通过让一组工作人员在队列中查找要处理的项目来同时处理队列中的多个项目。

答案 2 :(得分:2)

这对生产者/消费者模式感到尖叫:一个生产者以不同于消费者消费的速度生产数据。一旦生产者不再生产任何东西,它就会通知消费者不再需要数据。

MSDN有一个很好的例子,这个模式将几个数据流块链接在一起:一个块的输出是另一个块的输入。

Walkthrough: Creating a Dataflow Pipeline

这个想法如下:

  • 创建一个将生成HTML的类。
  • 此类具有类System.Threading.Tasks.Dataflow.BufferBlock <T&gt;
  • 类的对象
  • 异步过程会创建所有HTML输出,并等待SendAsync将数据发送到bufferBlock
  • 缓冲区块实现接口ISourceBlock <T&gt;。该类将此公开为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()
    }
}
  • 第二个类使用此ISourceBlock。它将在此源块处等待,直到数据到达并处理它。
  • 在异步功能中执行此操作
  • 在没有更多数据可用时停止

代码:

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具有关于异常处理和取消任务的注意事项:

答案 3 :(得分:0)

这是我最终使用的:(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,并行帖子没有问题。