首页 > 解决方案 > 如何线程安全地将并行进程中的数据收集到单个对象中?

问题描述

我有以下发送多条短信的功能:

BulkSMSSenderResult bulkResult = new BulkSMSSenderResult();

if (BulkRequest.Requests.Any()) {
    IEnumerable<(SMSSenderRequest, Task<Nito.Try<SMSSenderResult>>)> sendSmsTasks 
        = BulkRequest.Requests.Select(request => (request, SendSingleSmsAsync(request)));
    await Task.WhenAll(sendSmsTasks.Select(task => task.Item2));

    sendSmsTasks.ToList()
        .ForEach(task => {
            (SMSSenderRequest request, Task<Nito.Try<SMSSenderResult>> tryResult) = task;
            _ = tryResult.Result.Match<Either<ErrorMessage, SMSSenderResult>>(
                exception => new ErrorMessage(exception, request),
                value => value
            )
            .Match(
                result => bulkResult.Add(result),
                error => bulkResult.Add(error)
            );
        });
}

if (BulkRequest.BadRequests.Any()) {
    bulkResult.InvalidRequests = BulkRequest.BadRequests;
}

WriteResponseAsync(context, StatusCodes.Status207MultiStatus, bulkResult);

几乎可以按预期工作,只是似乎所有 SMS 都被发送了两次。

我认为问题可能出在这一行:

await Task.WhenAll(sendSmsTasks.Select(task => task.Item2));

我的期望是这条线应该检查 SMS 是否已发送,以便后面的代码可以安全执行。

但是,似乎这行和后面的代码导致SendSingleSmsAsync(request)执行......或者其他东西(我无法推测)导致工作两次触发(我确信SendSingleSmsAsync(request)它本身工作正常)。

任何想法如何解决这一问题?

标签: c#multithreadingcollectionsthread-safety

解决方案


您必须将可枚举的 Linq 操作更多地视为设置要执行的小迷你程序,而不是实际运行它们。

在您的情况下,您在此处设置计算:

IEnumerable<(SMSSenderRequest, Task<Nito.Try<SMSSenderResult>>)> sendSmsTasks 
                    = BulkRequest.Requests.Select(request => (request, SendSingleSmsAsync(request)))

但是你实际上经历并执行了两次 - 这里:

await Task.WhenAll(sendSmsTasks.Select(task => task.Item2));

和这里:

sendSmsTasks.ToList()

解决方法是尽快“实现”可枚举,以便从那时起您处理的是实际数据,而不是可链接的、懒惰的、可能的东西。

尝试坚持.ToArray()在可枚举的声明的末尾。


推荐阅读