Home > OS >  It's possible to give priority to a certain Task when executing more than one?
It's possible to give priority to a certain Task when executing more than one?

Time:03-24

I'm executing an async function (ExampleFunction) that blocks the thread for the following calls if the previous one hasn't finished. I'm printing messages in the console to check the order, but the order is not the expected.

So, this is the code:

private async Task TestTest()
    {
        int idtmp = id;
        var task1 = Test(idtmp);
        id  ;

        int idtmp2 = id;
        var task2 = Test(idtmp2);
        id  ;

        await Task.WhenAll(task1, task2).ConfigureAwait(false);


        Console.ReadLine();

    }

    private async Task Test(int id)
    {
        Console.WriteLine($"Before calling {id} {DateTime.Now.ToString("HH:mm:ss.ffff")}");
        Task<int> task = ExampleFunction();
        Console.WriteLine($"After calling {id} {DateTime.Now.ToString("HH: mm:ss.ffff")}");

        await task.ConfigureAwait(false);

        Console.WriteLine($"Function finished {id} {DateTime.Now.ToString("HH: mm:ss.ffff")}");

    }

This is the output:

Before calling 1 17:23:42.5953
After calling 1 17:23:42.6356
Before calling 2 17:23:42.6371
After calling 2 17:24:07.0415
Function finished 1 17:24:07.0453
Function Finished 2 17:31.2036

The expected output should be:

Before calling 1
After calling 1
Before calling 2
Function finished 1
After calling 2
Function Finished 2

But it seems that when the first Task finishes, the second one continues until it hits the await and that's why it shows the After calling 2 message first.

Is there a way to specify that the Task1 has priority when the first call finishes?

EDIT:

The ExampleFunction has two behaviors in the server side:

  1. The second call will block the thread if there is another call in progress
  2. The second call won't block the thread, it will be queued in the background.

So, for the second behavior the output should be this:

Before calling 1
After calling 1
Before calling 2
After calling 2
Function finished 1
Function Finished 2

But that's the output that I'm receiving with the first behavior. I need to check the correct behavior in each configuration. That's why the current method doesn't work for me.

CodePudding user response:

Async methods will run synchronously until they hit the first await of an incomplete task. If you think about how much of your code might run synchronously at each await it would be equivalent to;

int idtmp = id;

Console.WriteLine($"Before calling {idtmp} {DateTime.Now.ToString("HH:mm:ss.ffff")}");
var task1 = ExampleFunction();
Console.WriteLine($"After calling {idtmp} {DateTime.Now.ToString("HH: mm:ss.ffff")}");

if (task1.IsCompleted){
    Console.WriteLine($"Function finished {idtmp} {DateTime.Now.ToString("HH: mm:ss.ffff")}");
}else{
   // async magic
}

id  ;
int idtmp2 = id;

Console.WriteLine($"Before calling {idtmp2} {DateTime.Now.ToString("HH:mm:ss.ffff")}");
var task2 = ExampleFunction();
Console.WriteLine($"After calling {idtmp2} {DateTime.Now.ToString("HH: mm:ss.ffff")}");

if (task2.IsCompleted){
    Console.WriteLine($"Function finished {idtmp2} {DateTime.Now.ToString("HH: mm:ss.ffff")}");
}else{
   // async magic
}

Then you also need to consider what ExampleFunction will do before calling it's first await. If it's a CPU heavy task, it might take a long time to return. Does the second call block the current thread on a lock until the first call returns? If there is some lock synchronisation between tasks, does it matter which thread / task resumes first?

While it is possible for the first async method to complete in the order you are hoping for. Is it likely?

CodePudding user response:

I hope this modified example makes things a bit more clear.

Notes:

  1. The tasks are started before calling WhenAll
  2. A task runs synchronously until the first await so it will not yield control before that (in the case of a fake task with no awaits it will never yield control)
  3. All work of is done by the same thread until the awaits 'come back'.
var task1 = Test("A");
var task2 = Test("B");

Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId}: Sleeping");
await Task.Delay(TimeSpan.FromMilliseconds(300));
Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId}: Before WhenAll");
await Task.WhenAll(task1,task2).ConfigureAwait(false);
Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId}: After WhenAll");

Console.ReadLine();

async Task Test(string s)
{
    Console.WriteLine($"  {Thread.CurrentThread.ManagedThreadId}: Started {s}");
    Task task = Task.Delay(TimeSpan.FromMilliseconds(100));
    Console.WriteLine($"  {Thread.CurrentThread.ManagedThreadId}: After starting work {s}");

    await task.ConfigureAwait(false);

    Console.WriteLine($"  {Thread.CurrentThread.ManagedThreadId}: Finished {s}");
}

This prints

  1: Started A
  1: After starting work A
  1: Started B
  1: After starting work B
1: Sleeping
  5: Finished B
  7: Finished A
5: Before WhenAll
5: After WhenAll

CodePudding user response:

If I've understood you correctly, the key thing here is that you want to start the second task at the same time as the first to let service calls begin, but you can't finish the second task until you have the results of the first.

If so, your second task needs to look like this:

public async Task Task2(int id, Task<Task1State> task1)
{
    /* Start Service calls */
    await Task.Delay(TimeSpan.FromSeconds(1.0));        
    var state = await task1;
    /* Complete `Task2` with the `state` from `Task1` */
    await Task.Delay(TimeSpan.FromSeconds(1.0));
}

The await Task.Delay calls are there to simulate work.

Now it's easy to harness up a test:

async Task Main()
{
    var id = 0;
    var task1_id =   id;
    var task2_id =   id;
    
    Console.WriteLine($"Before calling {task1_id} {DateTime.Now.ToString("HH:mm:ss.ffff")}");
    var task1 = Task1(task1_id);
    Console.WriteLine($"After calling {task1_id} {DateTime.Now.ToString("HH: mm:ss.ffff")}");
    Console.WriteLine($"Before calling {task2_id} {DateTime.Now.ToString("HH:mm:ss.ffff")}");
    var task2 = Task2(task2_id, task1);
    Console.WriteLine($"After calling {task2_id} {DateTime.Now.ToString("HH: mm:ss.ffff")}");
    
    await task2;
    Console.WriteLine($"Function finished {task2_id} {DateTime.Now.ToString("HH: mm:ss.ffff")}");
}

public class Task1State { }
public async Task<Task1State> Task1(int id)
{
    await Task.Delay(TimeSpan.FromSeconds(2.0));
    Console.WriteLine($"Function finished {id} {DateTime.Now.ToString("HH: mm:ss.ffff")}");
    return new Task1State();
}

When I run that I get the following expected results:

Before calling 1 14:16:13.0724
After calling 1 14: 16:13.0736
Before calling 2 14:16:13.0736
After calling 2 14: 16:13.0743
Function finished 1 14: 16:15.0889
Function finished 2 14: 16:16.1039

The combined wait time is 4 seconds, but because two of those seconds happen in parallel then the total time is 3 seconds as seen in the output.

CodePudding user response:

You want to have two Tasks that are Mutually Exclusive.

In the async programming world we have a synchronization primitive called the Mutex.

You want the server side to have an Async Mutex.

<PackageReference Include="Nito.AsyncEx.Coordination" Version="5.1.2" />

public class Server
{
   private readonly AsyncLock _asyncMutext = new();

   public async Task Foo()
   {
      using var _ = await _asyncMutext.LockAsync();
      Console.WriteLine("Starting Foo");
      await Task.Delay(1000);
      Console.WriteLine("Finishing Foo");
   }

   public async Task Bar()
   {
      using var _ = await _asyncMutext.LockAsync();
      Console.WriteLine("Starting Bar");
      await Task.Delay(1000);
      Console.WriteLine("Finishing Bar");
   }
}

Just scope your Mutex appropriately for your use case.

  • Related