3

How to execute "Parallel.ForEach" as background task that will return control immediate to the calling method?

I want to using C#/Linq "Parallel.Foreach" but I don't want to way until all parallel tasks complete before proceeding to the next statement in the call method. I'm looking for a way to handle the completation of "Parallel.Foreach" asynchronously from another thread.

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...
    }
 }
Mojtaba Tajik
  • 1,725
  • 16
  • 34
Bimo
  • 5,987
  • 2
  • 39
  • 61

3 Answers3

4

Well for starters I would recommend an async void EventHandler.

An async eventhandler with an await method will prevent the UI/main thread from being frozen. To benefit from said asynchronous-ness you will need an async/await in the code.

Propose change to this:

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...
}

To This:

    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);
            });
        });
    }

Note: There maybe other considerations to be careful of, for example, someone double clicking. However, to answer the original question - this should be all the change you need. See below for a quick and dirty ThreadSafeBoolean.

For TAP/C# Purists:
If Toub, Cleary, or Skeet, were here they would caution me against the await Task wrapper - however I have found that in the real world to maintain Async/Await patterns - this pattern is occasionally needed. Even though Parallel supports async lambdas too the behavior is woefully unpredictable. You would need something like NitroEx or ForEachAsync extenstions by Yuri. However, if you want to run something like these synchronous fire and forgets in parallel and asynchronously - async Task wrapper is usually the simplest solution.

Below I demonstrate handling double clicks with a thread safe boolean backed by Interlocked. For more readable / generalized code I would also consider if (Monitor.TryEnter(myObjectLock, 0)) or a SemaphoreSlim(1,1). Each one has a different usage style. If I was sticking with a pattern for this case then Monitor.TryEnter maybe the cleanest approach as it will return false and you can exit out similar to the thread safe bool. For efficiency and simplicity I assume we are to just pass through the Event (i.e. do nothing) if its already running.

Sample Busy Check With 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);
        });
    });
}

For other performance optimizations, consider alternatives to Concurrent inserts and if the workload is intense, limit the parallel foreachs processed concurrently to 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);
            });
    });
}
HouseCat
  • 1,559
  • 20
  • 22
  • personally, I'm not a fan of the await-async pattern,,,.better to avoid it and start a new thread for concurrency unless Microsoft is shoveling it down your throats by api requirement... once you add one of those async functions into your code they turn all of your code into async functions... I really hate that... – Bimo Sep 22 '18 at 11:34
  • At the risk of sounding... rude. How you feel about the answer is not important and nor should you `hate` asynchronous code. You are free to choose whatever answer you wish, however, this is the textbook correct approach for the task at hand - and not because I gave it (@mojtaba-tajik gave a similar answer). Thread construction is bulky, expensive, and not recommended in modern .NET. Developers often poorly understand them and abuse them. In general, they are more inefficient than simple asynchronous Tasks. – HouseCat Sep 22 '18 at 11:39
  • 1
    ok..sorry...I was just complaining to Microsoft... what happens if your click on the button again if you are alredy in an await? – Bimo Sep 22 '18 at 11:50
  • It's all good. I wanted to be helpful but clear. After all we have new users browsing every day. You would fire n times for n clicks. You would need a threadsafe bool to prevent this. I will add an example. – HouseCat Sep 22 '18 at 11:53
  • incidentally, you can start a task in background without using await if you just save the task variable returned by task.run. just replace await in second example with var task = DoMyWorkAsync(); not sure why Microsoft has that requirement...I guess they just wanted people to not forget to add await keyword and get background version instead..of course that's kind of quarky as well since you don't necessarily care about saving the task if its in the background and pushes its results to a concurrent queue...hmm..of course if you save the task then I suppose your don't need a concurrent queue – Bimo Sep 22 '18 at 12:28
  • I take that last comment back.. I would swear that works some types... better to use Run.Task(() => { }) – Bimo Sep 22 '18 at 15:45
  • Did you actually try that 2nd sample with the CompletedTask? I think it would block. – H H Sep 22 '18 at 18:47
  • I will double check fen I can, it's called eliding. – HouseCat Sep 22 '18 at 22:33
  • @HenkHolterman Yeah, my mistake in this use case the async method calls the elided Task and runs synchronously but keeping the TAP patterns. I will remove that example. – HouseCat Sep 23 '18 at 14:25
0

You can run Parallel.ForEach in seprate task and call a delegate when all tasks done :

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");
    });
Mojtaba Tajik
  • 1,725
  • 16
  • 34
  • Is the StartNew lamba in the gui thread or just the continueWith? or maybe neither Is in the gui thread? – Bimo Sep 22 '18 at 11:52
  • ContinueWith not run in GUI theread and for the StartNew() see this topic : https://stackoverflow.com/questions/17702226/task-factory-startnew-invoked-on-ui-thread-anyhow – Mojtaba Tajik Sep 22 '18 at 12:06
  • @mjwills https://stackoverflow.com/questions/18312695/does-task-continuewith-capture-the-calling-thread-context-for-continuation So it's better to specify TaskScheduler to code. – Mojtaba Tajik Sep 22 '18 at 13:02
0

Method 1:

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

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

Method 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() });
}

Notes:

(but, I found that the overhead that parallel.foreach inserts for each task it starts is quite high for some reason.. seems like it starts too many worker tasks at the same time and they over compete for limited resouces on the same cpu...ends up being slower then another case where I started 4 tasks in parallel and then waited on them all to complete before getting another batch of 4 parallel tasks...that usage case of limiting parallel batches of 4 task each time when going through a large array was much faster...)

Bimo
  • 5,987
  • 2
  • 39
  • 61
  • `that usage case of limiting parallel batches of 4 task each time` https://stackoverflow.com/questions/9290498/how-can-i-limit-parallel-foreach may be of interest – mjwills Sep 22 '18 at 12:44