Как выполнить Parallel.ForEach в качестве фоновой задачи, которая немедленно вернет управление вызывающему методу? - PullRequest
0 голосов
/ 22 сентября 2018

Как выполнить «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...
    }
 }

Ответы [ 3 ]

0 голосов
/ 22 сентября 2018

Хорошо, для начала я бы порекомендовал async void EventHandler.

async eventhandler с методом await, чтобы предотвратить заморозку UI / основного потока.Чтобы воспользоваться этой асинхронностью, вам понадобится асинхронное / ожидание в коде.

Предложите изменение в этом:

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 #:
Если бы здесь были Тауб, Клири или Скит, они бы предостерегли меня от оболочки ожидающего задания - однако я обнаружил, что вреальный мир для поддержки шаблонов Async / Await - этот шаблон иногда необходим.Хотя Parallel также поддерживает асинхронные лямбды, их поведение крайне непредсказуемо.Вам понадобится что-то вроде расширений NitroEx или ForEachAsync от Юрия.Однако, если вы хотите запустить что-то вроде этого синхронного запуска и забудет параллельно и асинхронно - асинхронная оболочка задач обычно является самым простым решением.

Ниже я продемонстрирую обработку двойных щелчков с помощью потокового логического значения, поддерживаемого Interlocked.Для более читабельного / обобщенного кода я бы также рассмотрел 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);
        });
    });
}

Для других оптимизаций производительностирассмотрите альтернативы параллельным вставкам, и, если рабочая нагрузка интенсивна, ограничьте параллельные foreachs, обрабатываемые одновременно, 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);
            });
    });
}
0 голосов
/ 22 сентября 2018

Метод 1:

var thread = new Thread(() => {
    //JobInit();
    Parallel.ForEach(jobs, job_i => {
        DoSomething(job_i);
        done.Enqueue(job_i);
    });
    //stop = true;
});

thread.Start();
//stop = false;

Метод 2:

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

void Button_click() {

    // save task variable to run in background
    Task.Run(() => { run_task() });
}

Примечания:

(но я обнаружил, что накладные расходы, которые параллельные .foreach вставляют для каждогоЗадача, которую он запускает, довольно высока по какой-то причине ... кажется, что она запускает слишком много рабочих задач одновременно, и они чрезмерно конкурируют за ограниченные ресурсы на одном и том же процессоре ... в конечном итоге это происходит медленнее, чем в другом случае, когда я запускал 4 задачи вПараллельно, а затем ждал завершения их всех, прежде чем получить еще один пакет из 4 параллельных задач ... этот сценарий использования ограничения параллельных пакетов из 4 задач каждый раз при прохождении большого массива был намного быстрее ...)

0 голосов
/ 22 сентября 2018

Вы можете запустить Parallel.ForEach в отдельной задаче и вызвать делегата, когда все задачи выполнены:

int[] jobs;
ConcurrentQueue<int> done = new  ConcurrentQueue<int>();

jobs = new int[] {0, 20, 10, 13, 12};

//How to get this to return immediately without awaiting completion?
Task tsk = new TaskFactory().StartNew(() =>
    {
        Parallel.ForEach(jobs, jobI =>
        {
            DoSomething(jobI);

            done.Enqueue(jobI);
        });
    })
    .ContinueWith(task =>
    {
        MessageBox.Show("Parallel.ForEach complete");
    });
...