Home > database >  Awaiting tasks in order, and not in parallel
Awaiting tasks in order, and not in parallel

Time:10-05

I have a set of asynchronous tests, which run on external hardware.

I can run them in order but because all of these tests have side effects, I'd like to be able to shuffle them and run them over and over.

When I put them in a list and try to await each afterwards, they all run in parallel instead of being run 1 by 1.

I'd like to be able to shuffle the list and then run them all in order.

How can I run the tests (List<Task<bool>) in order while keeping them in a list so that I can shuffle the list?

private async void RunVisca()
{
    Ptz ptz = new Ptz(SelectedComPort, (byte)(1), SelectedBaudRate, Node.DeviceType.PTZ);

    UpdateResults("VISCA TEST START\n\n");

    // Method 1: Correct - Runs in order
    await VTestDriveClosedLoop(ptz, true);
    await VTestDriveClosedLoop(ptz, false);
    await VTestLimitsCl(ptz, 125, 20);
    await VTestLimitsOl(ptz, 125, 20);

    // Method 2: Incorrect - Runs in parallel
    List<Task<bool>> tests = new List<Task<bool>>();
    tests.Add(VTestDriveClosedLoop(ptz, true));
    tests.Add(VTestDriveClosedLoop(ptz, false));
    tests.Add(VTestLimitsCl(ptz, 125, 20));
    tests.Add(VTestLimitsOl(ptz, 125, 20));

    // Tasks all run in parallel here - should they not run in order?
    foreach(Task<bool> test in tests)
    {
        _ = await test.ConfigureAwait(true);
    }

    UpdateResults("\nTEST COMPLETE\n\n");
}

CodePudding user response:

The Task-based Asynchronous Pattern (TAP) pattern means that calling an async method, which returns a Task, will return a hot task.

A Task in a hot state has already been started. This can be proven by checking the Task.Status property, which will never be TaskStatus.Created.

A cold task will have a Task.Status property of TaskStatus.Created & will only start running when Start() is called on the instance.

The only way to create a cold task is to use Task/Task<T>'s respective public constructors.


Since your tasks are returned by an async method, they will already be running when they are returned to you.

To run them in order & not in parallel, you mustn't create them after one another unless the previous task has been completed. To ensure this, you will have to await them separately.

This results in method 1 being the correct way to run hot tasks in order & not in parallel.

await VTestDriveClosedLoop(ptz, true);
await VTestDriveClosedLoop(ptz, false);
await VTestLimitsCl(ptz, 125, 20);
await VTestLimitsOl(ptz, 125, 20);

Method 2/3 is incorrect as it will run your tasks in parallel.

You are adding hot tasks returned from your async methods to your list, without awaiting them. They will have already started running before you have moved on to add the next task to the list.

The 1st task is not awaited so the 2nd task will start immediately after, regardless of if task 1 finished or not.

The 2nd task is not awaited so the 3rd task etc.

With method 2/3, they will always run in parallel.

tests.Add(VTestDriveClosedLoop(ptz, true));
tests.Add(VTestDriveClosedLoop(ptz, false));

The real question is how to run cold tasks in order so that we can store them in a list yet take advantage of deferred execution until we loop over our list.

The solution is to store delegates instead, which will trigger the below Task constructor:

public Task (Func<TResult> function);

This will create cold tasks, letting you run the Task when the Func<Task<bool>> is invoked:

var tests = new List<Func<Task<bool>>>();
tests.Add(() => VTestDriveClosedLoop(ptz, true));
tests.Add(() => VTestDriveClosedLoop(ptz, false));
tests.Add(() => VTestLimitsCl(ptz, 125, 20));
tests.Add(() => VTestLimitsOl(ptz, 125, 20));
    
foreach (var test in tests) {
     await test();
}

This demo program below will clearly show tasks running in order vs. parallel.

class Program
{
    private static async Task Main(string[] args)
    {
        // 1... 2... 3... 4...
        var tasksThatRunInOrder = new List<Func<Task<bool>>>();
        tasksThatRunInOrder.Add(() => Test("1"));
        tasksThatRunInOrder.Add(() => Test("2"));
        tasksThatRunInOrder.Add(() => Test("3"));
        tasksThatRunInOrder.Add(() => Test("4"));

        foreach (var test in tasksThatRunInOrder)
            await test();

        // 1, 2, 3, 4
        var testsThatRunInParallel = new List<Task<bool>>();
        testsThatRunInParallel.Add(Test("1"));
        testsThatRunInParallel.Add(Test("2"));
        testsThatRunInParallel.Add(Test("3"));
        testsThatRunInParallel.Add(Test("4"));

        foreach (var test in testsThatRunInParallel)
            await test;
    }

    private static async Task<bool> Test(string x)
    {
        Console.WriteLine(x);
        await Task.Delay(1000);
        return true;
    }
}

CodePudding user response:

Tasks start when they are created. Since you're creating all of them at the same time without awaiting any of them, they're going to execute in a undetermined parallel manner. I think maybe you wanted to store delegates instead. These aren't executed until invoked.

var tests = new List<Func<Task<bool>>>();
tests.Add( () => VTestDriveClosedLoop(ptz, true) );
tests.Add( () => VTestDriveClosedLoop(ptz, false) );
tests.Add( () => VTestLimitsCl(ptz, 125, 20) );
tests.Add( () => VTestLimitsOl(ptz, 125, 20) );
    
foreach(var test in tests)
{
    _ = await test(); 
}

CodePudding user response:

You could create a list where you randomize the order you want to run things and then call the functions that create the tasks accordingly. For example:

    static async Task RunTasks()
    {
        Random rng = new Random();
        var order = new List<int>() { 1, 2 };
        var shuffledOrder = order.OrderBy(a => rng.Next()).ToList();

        foreach (var i in shuffledOrder)
        {
            switch (i)
            {
            case 1:
                await LoadFile1();
                break;
            case 2:
                await LoadFile2();
                break;
            }
        }
    }

CodePudding user response:

How about call chain of ContinueWith?

Task.Run(async () => 
{
    await VTestDriveClosedLoop(ptz, true);
}).ContinueWith(async (_) => 
{ 
    await VTestDriveClosedLoop(ptz, false);
}).ContinueWith(async (_) => 
{ 
    await VTestLimitsCl(ptz, 125, 20);
}).ContinueWith(async (_) => 
{ 
    await VTestLimitsOl(ptz, 125, 20);
});

CodePudding user response:

Try awaiting without assignment to a throwaway variable.

  • Related