1

Some years ago Remus Rusanu has posted this great answer about ADO async programming: https://stackoverflow.com/a/9434687/23900248

In particular he wrote:

A word of caution: an async SQL command is completing as soon as the first result returns to the client, and info messages count as result. You've lost all benefits of async. The print creates a result that is sent back to the client, which completes the async command and execution on the client resumes and continues with the 'reader.Read()'. Now that will block until the complex query start producing results. You ask 'who puts print in the procedure?' but the print may be disguised in something else, perhaps something as innocent looking as an INSERT that executes without first issuing a SET NOCOUNT ON.

Now we are at .NET 8, and this situation seems to persist. Is there a workaround? What is the correct way to asynchronously call a stored procedure that produces many results, including messages that, for example, inform the client about the execution status?

In example:

declare @K int=0, @Progress varchar(255)
while @K<100
    begin

    set @K=@K+1

    select Flush=1
    set @Progress=concat('Executing ', @K, ' of 100')
    raiserror(@Progress, 9, 1) with nowait
    waitfor delay '00:00:01'
    end

marc.

UPDATE

Following the discussion with Panagiotis, I'm trying to reformulate my question. Consider the TSQL fragment mentioned above; we're not interested in whether this code is good or bad, nor are we concerned about whether it's a proper or improper use of SQL Server. The only fact I ask to focus on is that this code produces a flow of 100 result sets and 100 messages, alternated.

If it weren't for that waitfor, the code execution, transmission to the client, and client-side processing would be almost instantaneous. However, that waitfor is there and causes the processing of that stored procedure to last about 100 seconds. Almost all of this time consists of waiting for the server.

On paper, this is the ideal situation to apply async/await logic. Putting it in simplified terms: "No new thread is needed, with all its complications, we use the waiting time to do something else and not block the flow and therefore, ultimately, the interface."

And indeed, this happens if the messages are removed; ADO.Net performs that call asynchronously. But if there are messages, the mechanism gets jammed. Why?

Theoretically, datasets and messages are essentially the same thing, two chunks of data in the Pipe flowing from the server to the client. Why does a message disrupt ADO.Net?

Remus writes:

The print creates a result that is sent back to the client, which completes the async command, and execution on the client resumes and continues with the 'reader.Read()'. Now that will block until the complex query starts producing results.

And Panagiotis reiterates:

Remus Rusanu warns against bad stored procedure coding, not about issues in asynchronous programming.

However, I don't understand why the arrival of the message demands marking the command as completed. The only reason that comes to my mind is that messages, being handled for historical reasons as events, break the async/await chain.

Why does ADO lock up? And more importantly, is there a purely async/await way to prevent it from locking up? Running the asynchronous call within Task.Run() solves the problem, but it seems if Task.Run is more efficient than the asynchronous method itself, it means that the asynchronous method is widely optimizable, right?

rimarc

9
  • What you posted isn't asynchronous programming at all, it's ugly, synchronously blocking way of "reporting progress". That alone is a bad smell that suggests the stored procedure is doing something wrong, like looping or using a cursor instead of set-based queries. Asynchronous programming means using using var reader=await cmd.ExecuteReaderAsync(). So what does this situation seems to persist mean? Commented Apr 3, 2024 at 14:32
  • SQL Server has a way to send notifications between processes/services since 2005, Service Broker, which provides proper queues, services, conversations. ADO.NET notifications are based on this. Commented Apr 3, 2024 at 14:37
  • The issue I raise is not about how to write a stored procedure but how to call a generic stored procedure in C# without encountering the problem pointed out by Remus. Executing an asynchronous call via .Net to that TSQL code (good or bad as it may be) causes the client to freeze and the benefits of async/await are lost (as observed by Remus). One way to circumvent this is to use Task.Run(), in this way ADO forcibly uses a new thread and the flow works properly. But what's the point? Is it not possible to asynchronously receive those results using exclusively the async/await pattern? Commented Apr 3, 2024 at 15:10
  • 1
    You aren't making an asynchronous call to a generic stored procedure. What you describe is progress reporting, not asynchronous execution. The real problem is using the wrong technique to do something that shouldn't be done this way anyway. raiserror(@Progress, 9, 1) with nowait is NOT a report mechanism Commented Apr 3, 2024 at 15:11
  • causes the client to freeze not if you actually used Execute...Async without trying to read "progress messages". Commented Apr 3, 2024 at 15:12

1 Answer 1

1

After a year, I have realised that the issue does not actually exist — or more precisely, it does not concern ADO itself, but rather async/await.

A typical ADO call contains several constructs of this kind, especially when using a DataReader (OpenAsync, ExecuteReaderAsync, ReadAsync, etc.).

Well, all these await calls need to be followed by ConfigureAwait(false) — otherwise the application is forced to switch back and forth between threads, which ends up blocking everything.

That is really the whole point. In .NET, it was decided to adopt ConfigureAwait(true) as the default, probably to make it easier to adopt this powerful pattern. However, in doing so, the meaning and importance of ConfigureAwait remained somewhat in the background — and it took me a whole year to figure it out. :-)

Hopefully this can save someone else a bit of time!

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.