如何执行“Parallel.ForEach”作为后台任务,将控制立即返回给调用方法?

我想使用 C#/Linq“Parallel.Foreach”,但我不想在所有并行任务完成之前继续调用方法中的下一条语句。我正在寻找一种方法来处理从另一个线程异步完成“Parallel.Foreach”。

using System.Threading.Tasks;
using System.Collections.Concurrent;

public class stuff {
    int[]                jobs;
    ConcurrentQueue<int> done;

    void DoSomething(int job_id) {
       //This takes a long time...
       Sleep(10000);
    }

    void Button_Click() {
        jobs = new int[] {0,20,10,13,12};

        // parallel.foreach...ok great...now all CPUs will
        // be loaded in parallel with tasks...
        // … But, How to get this to return
        // immediately without awaiting completion?
        Parallel.ForEach(jobs, job_i => {
            DoSomething(job_i);
            done.Enqueue(job_i); //tell other thread that job is done
        });
        // now my gui is blocked and unresponsive until this
        // foreach fully completes...instead,
        // I want this running in background and returning immediately to gui
        // so that its not hung...
    }
 }

最佳答案

对于初学者来说,我会推荐一个 async void EventHandler

带有 async eventhandler 方法的 await 将防止 UI/主线程被卡住。为了从上述异步性中受益,您需要在代码中使用 async/await。

建议对此进行更改:

void Button_Click() {
    jobs = new int[] {0,20,10,13,12};

    // parallel.foreach...ok great...now all CPUs will
    // be loaded in parallel with tasks...
    // … But, How to get this to return
    // immediately without awaiting completion?
    Parallel.ForEach(jobs, job_i => {
        DoSomething(job_i);
        done.Enqueue(job_i); //tell other thread that job is done
    });
    // now my gui is blocked and unresponsive until this
    // foreach fully completes...instead,
    // I want this running in background and returning immediately to gui
    // so that its not hung...
}

对此:
    public async void Button_Click()
    {
        await DoMyWorkAsync();
    }

    private async Task DoMyWorkAsync()
    {
        await Task.Run(() =>
        {
            jobs = new int[] { 0, 20, 10, 13, 12 };

            Parallel.ForEach(jobs, job_i =>
            {
                DoSomething(job_i);
                done.Enqueue(job_i);
            });
        });
    }

注意:可能还有其他注意事项需要注意,例如有人双击。但是,要回答最初的问题 - 这应该是您需要的所有更改。请参阅下面的快速和肮脏的 ThreadSafeBoolean。

对于 TAP/C# 纯粹主义者:
如果 Toub、Cleary 或 Skeet 在这里,他们会警告我不要使用 await Task 包装器——但是我发现在现实世界中维护异步/等待模式——偶尔需要这种模式。尽管 Parallel 也支持异步 lambda,但行为是非常不可预测的。你需要像 Yuri 的 NitroEx 或 ForEachAsync 扩展。但是,如果你想并行和异步地运行类似这些同步触发和遗忘的东西 - 异步任务包装器通常是最简单的解决方案。

下面我演示了使用 Interlocked 支持的线程安全 bool 值处理双击。对于更易读/通用的代码,我还会考虑 if (Monitor.TryEnter(myObjectLock, 0))SemaphoreSlim(1,1) 。每一种都有不同的使用风格。如果我坚持这种情况下的模式,那么 Monitor.TryEnter 可能是最干净的方法,因为它会返回 false 并且您可以像线程安全 bool 一样退出。为了效率和简单性,我假设我们只是通过事件(即什么都不做),如果它已经在运行。

使用 Threadsafe Boolean 的繁忙检查示例:
using System.Threading;

// default is false, set 1 for true.
private static int _threadSafeBoolBackValue = 0;

public bool ThreadSafeBusy
{
    get { return (Interlocked.CompareExchange(ref _threadSafeBoolBackValue, 1, 1) == 1); }
    set
    {
        if (value) Interlocked.CompareExchange(ref _threadSafeBoolBackValue, 1, 0);
        else Interlocked.CompareExchange(ref _threadSafeBoolBackValue, 0, 1);
    }
}

public async void Button_Click(object sender, EventArgs e)
{
    if (!ThreadSafeBusy)
    {
        ThreadSafeBusy = true;
        await DoMyWorkAsync();
        ThreadSafeBusy = false;
    }
}

private async Task DoMyWorkAsync()
{
    await Task.Run(() =>
    {
        jobs = new int[] { 0, 20, 10, 13, 12 };

        Parallel.ForEach(jobs, job_i =>
        {
            DoSomething(job_i);
            done.Enqueue(job_i);
        });
    });
}

对于其他性能优化,请考虑并发插入的替代方案,如果工作量很大,请将并行处理的 foreach 限制为 new ParallelOptions {MaxDegreeOfParallelism = Environment.ProcessorCount}

private async Task DoMyWorkAsync()
{
    await Task.Run(() =>
    {
        jobs = new int[] { 0, 20, 10, 13, 12 };

        Parallel.ForEach(
            parallelOptions: new ParallelOptions { MaxDegreeOfParallelism = Environment.ProcessorCount },
            source: jobs,
            body: job_i =>
            {
                DoSomething(job_i);
                done.Enqueue(job_i);
            });
    });
}

关于c# - 如何执行 "Parallel.ForEach"作为后台任务,将控制立即返回给调用方法?,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/52455868/

10-11 22:23
查看更多