1

我正在使用Parallel.Invoke同时运行某些方法并在所有方法完成后收集结果。

问题

正如您在“可怕的代码”部分看到的那样,动作列表被硬编码为三个元素,如果detectedDevicesList.Count != 3.

尝试过的解决方案

我尝试动态创建一个Actions[]数组并将其作为参数传递给Parallel.Invoke,但我无法将现有方法转换为Tasks,然后再转换为Actions。

非工作代码

public async Task<Task<String>> callWithEveryConnectedDevice(ListBox listBoxLog, Boolean sendAlarms)
{        
    String TEST_CALLS_COMPLETED = "All test calls completed.";
    String TEST_CALLS_FAILED = "One or more test cals failed";

    return Task.Run(async () =>
    {
        List<MobileEquipment> detectedDevicesList = await GetConnectedDevices.getAsync();

        if (detectedDevicesList.Count == 0)
        {
            UpdateGui.listboxAddItem(listBoxLog, "No devices are connected.", true);
            return TEST_CALLS_FAILED;
        }

        Console.WriteLine("Executing test calls...");

        List<Task<MobileEquipment>> results = new List<Task<MobileEquipment>>();

        //Horrible code begins...
        Parallel.Invoke(() =>
        {
            results.Add(new MakePhoneCall().call(detectedDevicesList[0], listBoxLog));
        },
        () =>
        {
            results.Add(new MakePhoneCall().call(detectedDevicesList[1], listBoxLog));
        },
        () =>
        {
            results.Add(new MakePhoneCall().call(detectedDevicesList[2], listBoxLog));
        });

        //Horrible code ends...
        foreach (Task<MobileEquipment> mobileEquipment in results)
        {
            UpdateGui.listboxAddItem(listBoxLog, "Test call result for " + mobileEquipment.Result.serial + " " + mobileEquipment.Result.operador + ": " + mobileEquipment.Result.callSuccess, true);

            if (!mobileEquipment.Result.callSuccess && sendAlarms)
            {                      
                await SendEmail.sendAlarmEmailsAsync(libreta, asunto, mensaje);
            }
        }
                      

        UpdateGui.listboxAddItem(listBoxLog, TEST_CALLS_COMPLETED, true);

        return TEST_CALLS_COMPLETED;
    });
}

编辑:给读者的有用信息和经验教训

在收到出色的答案和评论之后,我添加了一些最初缺失的代码,这些代码可以帮助您安全地与并行任务中的 Windows 窗体对象进行交互。

public static void ListboxAddItem(ListBox listBox, String argText, Boolean useTimestamp)
    {
        String timeStamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");

        if (useTimestamp)
        {
            argText = timeStamp + ": " + argText;
        }

        if (Thread.CurrentThread.IsBackground)
        {
            listBox.Invoke(new Action(() =>
            {
                listBox.Items.Add(argText);
                listBox.SelectedIndex = listBox.Items.Count - 1;
            }));
        }
        else
        {
            listBox.Items.Add(argText);
            listBox.SelectedIndex = listBox.Items.Count - 1;
        }
    }

此外,不要盲目遵循 IntelliSense 建议,以防止像 Task<Task> 这样的恶作剧,或在 C# 上使用类似 Java 的大小写。

很难选择最佳建议的答案,因为它们都可以正常工作并且没有任何明显的性能差异(MakePhoneCall().call 使用通过 ADB 连接的 Android 设备进行自动电话呼叫)。检查哪个答案最适合您的特定应用。

4

3 回答 3

1

Parallel.FororParallel.Foreach和一个并发集合。应该更合适:

ConcurrentStack<Task<MobileEquipment>> results = new ();
Parallel.Foreach(detectedDevicesList, d => results.Add(new MakePhoneCall().call(d, listBoxLog));

另一种选择是并行 Linq

var result = detectedDevicesList.AsParallel(
    d => results.Add(new MakePhoneCall().call(d, listBoxLog).ToList();

但是,它看起来像Call返回一个任务,所以你确定这是一个缓慢的阻塞调用吗?如果不是,最好使用常规循环来启动调用,并使用Task.WaitAll来(a)等待它们完成。看起来您当前的解决方案可能会阻止mobileEquipment.Result.

另请注意,它listBoxLog看起来像一个 UI 对象,并且不允许从工作线程访问 UI 对象。如果方法是“纯”且对象是不可变的,则使用后台线程进行处理会容易得多。即避免可能不是线程安全的副作用。作为一般规则,我建议避免使用多线程编程,除非,a) 有充分的理由期待一些改进,b) 你很清楚线程安全的危险。

您还可以考虑使用Dataflow设置一个管道,以并行和异步方式执行处理的每个步骤。

于 2021-10-22T08:05:14.963 回答
1

你应该使用微软的反应框架(又名 Rx)——NuGetSystem.Reactive并添加using System.Reactive.Linq;——然后你所有丑陋的代码变成这样:

IObservable<MobileEquipment> query =
    from detectedDevicesList in Observable.FromAsync(() => GetConnectedDevices.getAsync())
    from detectedDevice in detectedDevicesList.ToObservable()
    from mobileEquipment in Observable.FromAsync(() => new MakePhoneCall().call(detectedDevice, listBoxLog))
    select mobileEquipment;

full 方法现在正确返回Task<String>,而不是Task<Task<String>>.

这里是:

public async Task<String> callWithEveryConnectedDevice(ListBox listBoxLog, Boolean sendAlarms)
{
    String TEST_CALLS_COMPLETED = "All test calls completed.";
    String TEST_CALLS_FAILED = "One or more test cals failed";

    IObservable<MobileEquipment> query =
        from detectedDevicesList in Observable.FromAsync(() => GetConnectedDevices.getAsync())
        from detectedDevice in detectedDevicesList.ToObservable()
        from mobileEquipment in Observable.FromAsync(() => new MakePhoneCall().call(detectedDevice, listBoxLog))
        select mobileEquipment;
        
    IList<MobileEquipment> results = await query.ToList();

    if (results.Count == 0)
    {
        UpdateGui.listboxAddItem(listBoxLog, "No devices are connected.", true);
        return TEST_CALLS_FAILED;
    }

    foreach (MobileEquipment mobileEquipment in results)
    {
        UpdateGui.listboxAddItem(listBoxLog, "Test call result for " + mobileEquipment.serial + " " + mobileEquipment.operador + ": " + mobileEquipment.callSuccess, true);

        if (!mobileEquipment.callSuccess && sendAlarms)
        {
            await SendEmail.sendAlarmEmailsAsync(libreta, asunto, mensaje);
        }
    }

    UpdateGui.listboxAddItem(listBoxLog, TEST_CALLS_COMPLETED, true);

    return TEST_CALLS_COMPLETED;
}
于 2021-10-22T08:21:30.733 回答
0

Parallel.Invoke不是在这种情况下使用的正确工具,因为您的工作负载是异步的,并且不是Parallel.Invoke异步友好的。CallAsync只需一次创建所有任务,然后await使用该Task.WhenAll方法完成所有任务,即可解决您的问题。在await您返回 UI 线程后,您可以使用结果安全地更新 UI。

将检测到的设备投射到任务的一个方便工具是SelectLINQ 运算符。

public static async Task CallWithEveryConnectedDevice(ListBox listBoxLog, Boolean sendAlarms)
{
    List<MobileEquipment> detectedDevicesList = await GetConnectedDevices.GetAsync();

    Task<MobileEquipment>[] tasks = detectedDevicesList
        .Select(device => new MakePhoneCall().CallAsync(device))
        .ToArray();

    MobileEquipment[] results = await Task.WhenAll(tasks);

    foreach (var mobileEquipment in results)
    {
        UpdateGui.ListboxAddItem(listBoxLog,
            $"Test call result for {mobileEquipment.Serial} {mobileEquipment.Operador}: {mobileEquipment.CallSuccess}", true);
    }

    foreach (var mobileEquipment in results)
    {
        if (!mobileEquipment.CallSuccess && sendAlarms)
        {
            await SendEmail.SendAlarmEmailsAsync(libreta, asunto, mensaje);
        }
    }
}
于 2021-10-22T10:57:30.940 回答