Home > Blockchain >  How grunty or expensive is C# async/await?
How grunty or expensive is C# async/await?

Time:08-04

I want to connect 50 USB serial port devices to a single computer and communicate asynchronously (using polling) with each of them.

AFAIK, using C# async/await does not cause the creation of additional threads. However, even though only a single thread is used, the rest of the program maintains responsiveness for non-CPU bound tasks.

For each USB serial port async/await method call, I understand that a new state machine is created by the compiler/CLR.

How many USB serial port methods can be async/awaited before a single thread becomes overloaded? I'm guessing that dynamically creating state machines is CPU heavy. Is the CLR smart enough to offload some of the workload / state machine overhead to other threads or CPU cores?

Example issue: Async/await code development takes place on a new fast computer, but the customer has an old slow computer. Initiating many state machines may work well during development, but perhaps stall upon deployment.

Edit: The following dummy code creates 500 async/awaits, thus causing the creation of 500 state-machines. Compiling and running the code from the command line shows no evidence of increased CPU usage (according to Windows Resource monitor plots).


MyClass[] myClass = new MyClass[numInstances];

for (int i = 0; i < numInstances; i  )
{
    myClass[i] = new MyClass();
    _ = myClass[i].MyMethod(i);
}

Console.ReadLine();

public class MyClass
{
    public async Task MyMethod(int i)
    {
        Console.WriteLine("Opening port"   i);
        await Task.Delay(3000);  // Do USB serial port poll
        Console.WriteLine("Done"   i);
    }
}

CodePudding user response:

I want to connect 50 USB serial port devices to a single computer and communicate asynchronously (using polling) with each of them.

Serial ports are one of those awkward APIs in .NET because they just don't get the love and attention that other APIs do. For one thing, there's a restriction on naming (they must start with COM) that .NET insists on even though it's not necessary (and many devices - including utilities such as com0com - don't have names starting with COM). For another, even though practically all APIs have been updated to have ReadAsync / WriteAsync methods, SerialPort has not. SerialPort does have a BaseStream property, but I'm not sure if it actually supports asynchronous operations, or if they're always synchronous.

TL;DR: Be sure to do some testing before you assume things will work. With SerialPort, always verify with manual testing.

For each USB serial port async/await method call, I understand that a new state machine is created by the compiler/CLR.

State machines are created as needed for each async method, yes.

I'm guessing that dynamically creating state machines is CPU heavy.

It's heavier on memory. But what's the alternative: creating a thread for each connection? Allocating a state machine (usually a few dozen or maybe hundred bytes) is going to be far more efficient than creating a thread, both in terms of CPU and memory.

Is the CLR smart enough to offload some of the workload / state machine overhead to other threads or CPU cores?

No; they're always created on the current thread. But the offloading would cause more CPU overhead than it would save. Creating the state machine is literally just an allocate and then a shallow-copy of all the local variables.

How many USB serial port methods can be async/awaited before a single thread becomes overloaded?

More generally: how many async methods can be in progress? And the answer is "a lot". Waaaay more than a few hundred; I'd guess probably around 50 million or so even on 10-15-year-old machines, though I haven't tested it. To put it another way: I've never seen anyone run into this limit; some other scalability limit is always hit first.

CodePudding user response:

As Stephen said, a state machine is more an indication of where your code stopped so it can pick back up where it left to do the rest of the work. In your case I would worry more about the synchronization context.

When the code encounters an await, it will also record the current synchronization context so it can schedule the continuation on that. There are 3 big ones that you have to take care of. There is a default one where every continuation will take a free thread out of the thread pool and run it's continuation on. There is also one for when you are running in a form or in an ASP.NET framework page. These ones limit all the work to a single thread to run the continuations on. New continuations that are scheduled will wait until the thread is free with it's work.

Assume that after the await, you have some very intensive work that keeps the CPU busy for a while. In the case of the default synchronization context, you will see that all the continuations are working in parallel (multithreaded). If you have more continuations than there are threads in the thread pool, then those will need to wait.

If the synchronization context only allows a single thread, then the next only run if the previous continuation is done (really done, or it has hit another await that isn't completed yet). You can avoid this by using the .ConfigureAwait(false) by telling that you want to use the default synchronization context. Alert: this only works in combination with await because the synchronization context is stored in the state machine, calling _ = myClass[i].MyMethod(i).ConfigureAwait(false); will have no effect as there is no state machine created at this point to save this context in. And this method doesn't change the context of the state machine that it has received.

  • Related