简体   繁体   中英

Using async-await for database queries — how does that save threads?

I'm starting to understand the concept of how await ing a chain of async methods that has "at the bottom" a hardware task like writing to a file (There Is No Thread https://blog.stephencleary.com/2013/11/there-is-no-thread.html ). But, what is the point of await ing a database call, like ExecuteQueryAsync , if the database is a local one? Isn't that technically a CPU-bound task since SQL server needs a thread to execute the query?

Your process has a limited number threads in the thread pool (you can create more threads but they're expensive in terms of resources). When you make a database call, the database server is typically a separate process with its own set of threads. It takes time to prepare the data you're asking for so you have some options as to what to do while that's happening:

  • Go into a blocking wait - Thread.Sleep , Task.Wait , etc. This means that you fire your database request on thread A and then enter a wait on that thread: the thread is allocated but is blocked and not usable for anything else. When your data is ready, you somehow learn of that and exit the wait and your code continues on thread A.
  • You perform an asynchronous wait: you fire your database request and then do an await with a callback function. The call gets sent to the database process but then it just returns (internally it takes note of your callback function (the continuation)). At this point now, thread A is free - you're not using it anymore so it goes back into the pool and it can be used for something else. The database server uses one of its own threads to get your data but that's none of your business - thread A can be used within your own application to do something now.

    A while later, your data is ready and your callback function gets called on some thread - not necessarily thread A and your code can resume.

Async processing thus freed up one of your threads for a bit of time to do something else. You don't get your data faster - it still takes X milliseconds to prepare your data. What you get is more efficiency in using your existing threads and more paralellism for operations that mostly wait, rather than compute (that is, IO operations). In your case you mostly wait - the processing and computing is done by a different process.

As @ErikPhilips pointed out, Stephen Cleary's article is a good read (not just this one): https://blog.stephencleary.com/2013/11/there-is-no-thread.html

But, what is the point of awaiting a database call, like ExecuteQueryAsync, if the database is a local one?

  1. C# Thread(1) -> ExecuteQuery()
  2. C# Thread(1) -> WaitForCallBack()
  3. SQL > Spin up Thread
  4. SQL Thread(2) -> Do Work
  5. SQL Thread(2) -> Return Result
  6. SQL > Spin down Thread
  7. C# Thread(1) -> WaitForCallBack() Signaled
  8. C# Thread(1) -> Materialize Results

In the previous example we have a thread literally doing nothing. This is not an efficient use of a thread.

  1. C# Thread(1) -> ExecuteQueryAsync()
  2. C# Thread(1) -> Framework::StoreMachineState
  3. C# > Spin down Thread
  4. SQL > Spin up Thread
  5. SQL Thread(1) -> Do Work
  6. SQL Thread(1) -> Return Result
  7. SQL > Spin down Thread
  8. C# > Spin up Thread
  9. C# Thread(1) -> Framework::RestoreMachineState
  10. C# Thread(1) -> WaitForCallBack() Signaled
  11. C# Thread(1) -> Materialize Results

In the previous example we a have a bit of overhead to store the machine state of the thread, but theoretically it's possible that the same thread is used between processes (not a process thread).

Isn't that technically a CPU-bound task since SQL server needs a thread to execute the query?

Technically it's 100% maybe . If the SQL optimization engine determines the query and data needed is in memory (cache or otherwise) it has a good chance of being CPU-bound (even in this instance it's not 100% chance). The only other scenario is to read from disk which if implemented propertly ( ReadFileEx ) then it's also async. Excerpt:

If the function succeeds, the calling thread has an asynchronous I/O operation pending: the overlapped read operation from the file. When this I/O operation completes, and the calling thread is blocked in an alertable wait state, the system calls the function pointed to by lpCompletionRoutine, and the wait state completes with a return code of WAIT_IO_COMPLETION.

If the function succeeds, and the file reading operation completes, but the calling thread is not in an alertable wait state, the system queues the completion routine call, holding the call until the calling thread enters an alertable wait state. For information about alertable waits and overlapped input/output operations, see About Synchronization.

Most often then not, there is no reason to not implement async/await using Microsoft's Framework. I can't speak about other 3rd party frameworks who might implement Async because it's the thing to do (fad) regardless of if it's the right thing to do (if someone uses Task.Run to make something async, they've completely failed making it async).

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM