使用 Async/Await 正确处理多个任务到数据库的方法

3
我有一个简单的场景,但我想知道我的方法是否正确。在连接到数据库并保存实体时,选择保存失败订单的单个任务是否更好,还是可以启动并触发多个任务,并等待它们全部完成。针对这种情况,应该采取什么正确的方法? 我已经有一个基于单个任务的版本,可以将一个实体保存到数据库中。
    public async static Task SaveOrdersAsync(OrderService oService, OrderItemService oiService, IEnumerable<OrderTemplate> toSaveList, IUnitOfWork uow, IProgress<string> progress)
    {
        var toSave = toSaveList as IList<OrderTemplate> ?? toSaveList.ToList();
        var tasks = new Task[toSave.Count()];

        for (var i = 0; i < tasks.Length; i++)
        {
            var i1 = i;

            tasks[i] = new Task(() => SaveToDb(oService, oiService, toSave.ElementAt(i1), uow), TaskCreationOptions.PreferFairness);

            var message = string.Format("- Order: {0} has been resaved.\n", toSave.ElementAt(i1).Order.FriendlyId);

            if (progress != null)
                progress.Report(message);
        }

        await Task.WhenAll(tasks);
    }

目前,我已经测试了以上内容,并且认为任务尚未启动,因为进度条仍在循环。我的假设是Task.WhenAll应该为我启动任务 - 这是我所想的吗?

还是应该在循环中使用它:

      tasks[i] = Task.Run(() => SaveToDb(oService, oiService, toSave.ElementAt(i1), uow));

我认为我已经接近正确了,只是想要有人告诉我是否做得正确。

反馈已整合版本:

    public async static Task SaveOrdersAsync(OrderService oService, OrderItemService oiService, IEnumerable<OrderTemplate> toSaveList, IUnitOfWork uow, IProgress<string> progress)
    {   
        var saveList = toSaveList as IList<OrderTemplate> ?? toSaveList.ToList();
        var saveTask = Task.Run(() =>
        {
            foreach (var ot in saveList)
            {
                SaveToDbBatch(oService, oiService, ot);

                var message = string.Format("- Order: {0} has been resaved.\n", ot.Order.FriendlyId);
                if (progress != null)
                    progress.Report(message);
            }
        });

        await saveTask;
        await Cache.UoW.SaveAsync();
    }
4个回答

6
这种情况下连接数据库并保存实体的正确方法是什么?
一般来说,您应该:
1. 如果可能的话,批量保存。换句话说,调用单个方法同时更新多条记录。例如EF具有SaveChangesAsync。
2. 使用数据库的自然异步API而不是Task.Run(甚至更糟的任务构造函数)。例如EF具有SaveChangesAsync。

谢谢,Stephen希望你能回复。好的,看起来我有一些改变要做。=) - IbrarMumtaz

2

是的,你说得对,创建一个任务并不会启动它。调用 Task.Run(...) 是更好的选择。

然而,更好的选择是使用从 ExecuteAsync(...) 返回的任务,并等待它完成。这是因为 ExecuteAsync 任务是一个 I/O 任务,而不是线程,所以它的执行方式不同,不会使用线程池线程。

顺便提一下:根据“保存”的复杂性,可能更可靠的做法是逐个进行“保存”。这是因为如果并行任务引起任何数据库错误(如约束冲突),则在并行执行时(即在随机时间)将极难重现。


太好了,我会将所有内容合并为一个任务,我没有想到这一点。 - IbrarMumtaz

1

new Task(...) 不会启动任务。启动任务不是 Task.WhenAll 的职责。几乎不应该使用 Task 构造函数。

请使用 Task.Run


0

看起来将其合并为我在更新中发布的一个任务是有效的,它还解决了一个我认为应该在这里提出的附带问题,以防其他人有兴趣追求我的原始方法。但我同意@jaytre的观点,根据您的保存复杂性和要保存的对象,逐个保存可能更好,以进行错误处理-但这取决于您。

因此,如果您追求我的原始方法,您可能会遇到以下错误:

无法将EdmType映射到CLR类多次。 EdmType“FrootPipe.Data.Order”被映射多次。

这基本上是由于锁定/同步问题-因此,不同的任务正在更多或更少同时访问模型,所有任务都试图将失败的订单重新添加到数据模型中。因此,对于我的情况,错误有点难以说明,但一些搜索导致了以下结果。

欲知详情,请参见:Entity framework MappingException: The type 'XXX has been mapped more than once


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接