0

While studying asynchronous programming, I had the following question: Will the code be executed in parallel after the await keyword in an asynchronous function?

An example of such code:

FuncAsync("123", "123");
FuncAsync("123", "123");

public async Task FuncAsync(string login, string password)
{
    try
    {
        var account = await DB.Accounts
            .Find(a => a.Login == login)
            .FirstOrDefaultAsync();
        // Is GetHash() will be executed in parallel, when two methods FuncAsync called?
        password = GetHash(password);
    }
    catch (Exception e)
    {
        Console.WriteLine(e);
    }
}

public static string GetHash(string password)
{
    var hashed = BCrypt.Net.BCrypt.HashPassword(password, 12);
    return hashed;
}
    

    
    
public static class DB
{
    public static MongoClient Client = new MongoClient("url");
    public static IMongoCollection<Account> Accounts = Client.GetDatabase("s").GetCollection<Account>("accounts");
}

Essentially, the first call to the database query function will be executed first, followed by the second call to the database query function. And then the code of each function will be executed sequentially or in parallel, each on its own thread?

  • 1
    `async void` is almost always a bad idea, see https://stackoverflow.com/questions/12144077/async-await-when-to-return-a-task-vs-void – Klaus Gütter Jul 23 '23 at 21:36
  • 2
    It's awful to see `catch (Exception e) { Console.WriteLine(e); }` in code. It's such an anti-pattern. – Enigmativity Jul 23 '23 at 23:40
  • @Enigmativity Maybe you will write what I should do instead, and not just criticize? – Ростислав Романець Jul 24 '23 at 11:42
  • @РостиславРоманець - You should only have one high-level try catch that provides logging for your code. The rest of your code should be written to avoid throwing exceptions in the first place. If you have some code that you cannot control then you should only ever catch specific exceptions that you can meaningfully handle. Please read [Vexing Exceptions](https://ericlippert.com/2008/09/10/vexing-exceptions/). I hope this is helpful. – Enigmativity Jul 24 '23 at 23:14

3 Answers3

3

Note that you don't seem to await on the calls to FuncAsync:

FuncAsync("123", "123"); // line #1
FuncAsync("123", "123"); // line #2
// line #3

The first call line #1 will result in calling the database methods:

DB.Accounts.Find(a => a.Login == login)
           .FirstOrDefaultAsync();

At this point it has initiated an asynchronous I/O bound task and I/O will be handled by the database driver. As this is an asynchronous operation, it returns immediately and the CPU does not have to block. Next there is an await on this asynchronous task, which will also return immediately to the caller and we end up on line #2. Same behavior repeats on line #2 and we end up on line #3.

At this point we have initiated two asynchronous I/O operations and CPU is free to execute line #3 and #4 and so on.

How the database queries will be executed will depend on database driver and database query scheduler which you don't control from your c# app. However, eventually these two asynchronous tasks will complete and call back into your C# code.

This will result in .net runtime task scheduler to schedule the remaining part of your async method ( which calls GetHash) on some thread. Now it may happen on one thread or more than one threads based on some parameters.

  • Presence of SynchronizationContext: In Winforms or WPF applications, for example, the main UI thread is the synchronization context. So the completion will be scheduled on main UI thread if FuncAsync was originally called on UI thread. This will result in the completions (Gethash) to be called serially one after the other because there is only one thread to execute. However, the order is not guaranteed, the task initiated by line #2 may complete first.

    • if you don't want to capture Synchronization context, look at usage of ConfigureAwait.
  • Without Synchronization context, the task scheduler will schedule the completion on any available thread. So, they may complete on same or different thread, serially or parallelly, there is no guarantee.

This is all default behavior and of course task scheduler is configurable.

Had you awaited the initial calls, for example:

await FuncAsync("123", "123"); // line #1
await FuncAsync("123", "123"); // line #2

Then the line #2 would be called only after completion of the asynchronous task initiated by line #1. In this case, even though they are asynchronous (CPU is never blocked) they will be executed serially for sure.

YK1
  • 7,327
  • 1
  • 21
  • 28
2

You are launching two asynchronous operations (FuncAsync) consecutively, without waiting for their completion. Your question is if the two operations will run concurrently, or if the second will start after the completion of the first.

In general, the two operations will run concurrently. More precisely, the second operation will start as soon as the first operation returns a Task. In general asynchronous methods return an incomplete Task very quickly. In other words the time required to create the task is minuscule compared to the time required for that task to complete. That's how asynchronous methods should behave according to Microsoft's guidelines. You can measure separately the two time intervals like this:

Stopwatch stopwatch = Stopwatch.StartNew();
Task task = FuncAsync("123", "123");
TimeSpan elapsed1 = stopwatch.Elapsed;
bool isCompleted = task.IsCompleted;
stopwatch.Restart();
task.Wait();
TimeSpan elapsed2 = stopwatch.Elapsed;
Console.WriteLine($"Create: {elapsed1}, Completed: {isCompleted}, Wait: {elapsed2}");

There are exceptions though. Some asynchronous methods are implemented synchronously, for various reasons, meaning that the asynchronous method is asynchronous only in theory, and in practice does all the work during the creation of the Task and returns a Task that is already completed upon creation (the property IsCompleted is true). Whether the FirstOrDefaultAsync is such a method, depends on the implementation of your database driver. In case it is, the two FuncAsync operations will not run concurrently. The second operation will start after the completion of the first.

And then the code of each function will be executed sequentially or in parallel, each on its own thread?

Asynchronous operations typically don't run on threads. There is nothing for the CPU to do while, for example, the network card is sending and receiving data through the cable. And when we talk about threads, we talk about the CPU. Work done on other hardware devices don't count as threads.

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
1

It's important to note that async/await does not create or switch threads. You have to explicitly call a method that does.

Try this example to see what I mean:

void Main()
{
    FuncAsync("123", "123");
    FuncAsync("123", "123");
}

public async Task FuncAsync(string login, string password)
{
    int meaning = await Task.FromResult(42);
    Console.WriteLine(meaning);
    Thread.Sleep(TimeSpan.FromSeconds(1.0));
    int reverse = await Task.FromResult(-1);
    Console.WriteLine(reverse);
    Thread.Sleep(TimeSpan.FromSeconds(1.0));
    Console.WriteLine(meaning * reverse);
}

When run it gets this output:

42
-1
-42
42
-1
-42

There is nothing in this code that switches to an existing or invokes a new thread.

Compare to these two alternatives:

public async Task FuncAsync(string login, string password)
{
    int meaning = await Task.Run(() => 42);
    Console.WriteLine(meaning);
    Thread.Sleep(TimeSpan.FromSeconds(1.0));
    int reverse = await Task.Run(() => -1);
    Console.WriteLine(reverse);
    Thread.Sleep(TimeSpan.FromSeconds(1.0));
    Console.WriteLine(meaning * reverse);
}

public async Task FuncAsync(string login, string password)
{
    int meaning = await Task.FromResult(42);
    Console.WriteLine(meaning);
    await Task.Delay(TimeSpan.FromSeconds(1.0));
    int reverse = await Task.FromResult(-1);
    Console.WriteLine(reverse);
    await Task.Delay(TimeSpan.FromSeconds(1.0));
    Console.WriteLine(meaning * reverse);
}

Both do call methods that switch threads. The output is more like you're expecting.

42
42
-1
-1
-42
-42

So, in your code, the await DB.Accounts line is switching to a different thread and hence GetHash will run on different threads.

Enigmativity
  • 113,464
  • 11
  • 89
  • 172