0

I have read an answer to a similar question https://stackoverflow.com/a/43841624/11478903 but it doesn't explain the yielding of execution.

I have a thread that consumes events using GetConsumingEnumerable from a BlockingCollection<Event> _eventQueue property.

public async Task HadleEventsBlocking() { foreach (var event in _eventsQueue.GetConsumingEnumerable()) { switch (event) { case Event.BtnAddClicked: HandleBtnAddClickedAsync(_cts.Token); break; case Event.BtnRemoveClicked: HandleBtnRemoveClickedAsync(_cts.Token); break; case Event.BtnDisableClicked: _cts.Cancel(); break; case Event.BtnEnableClicked: _cts.Dispose(); _cts = new CancellationTokenSource(); break; } Console.WriteLine("Event loop execution complete."); } } public async Task HandleBtnAddClickedAsync(CancellationToken token) { try { await Task.Run(async () => { token.ThrowIfCancellationRequested(); await Task.Delay(2000); token.ThrowIfCancellationRequested(); Console.WriteLine("BtnAddClicked event complete"); }); } catch (OperationCanceledException) { Console.WriteLine("HandleBtnAddClicked Cancelled"); } } public async Task HandleBtnRemoveClickedAsync(CancellationToken token) { try { await Task.Run(async () => { token.ThrowIfCancellationRequested(); await Task.Delay(2000); token.ThrowIfCancellationRequested(); Console.WriteLine("BtnRemoveClicked event complete"); }); } catch (OperationCanceledException) { Console.WriteLine("HandleBtnRemoveClicked Cancelled"); } } 

And this does exactly what I want, the foreach loop executes each Event as fast as possible and does not get blocked. The methods that correspond to each Event also get the convenience of try/catch with the await Task.Run but why does this work? Because if I simply rearrange it won't work as I want it to.

public async Task HadleEventsBlocking() { foreach (var event in _eventsQueue.GetConsumingEnumerable()) { switch (event) { case Event.BtnAddClicked: try { await Task.Run(async () => { _cts.Token.ThrowIfCancellationRequested(); await Task.Delay(2000); _cts.Token.ThrowIfCancellationRequested(); Console.WriteLine("BtnAddClicked event complete"); }); } catch (OperationCanceledException) { Console.WriteLine("HandleBtnAddClicked Cancelled"); } break; case Event.BtnRemoveClicked: try { await Task.Run(async () => { _cts.Token.ThrowIfCancellationRequested(); await Task.Delay(2000); _cts.Token.ThrowIfCancellationRequested(); Console.WriteLine("BtnRemoveClicked event complete"); }); } catch (OperationCanceledException) { Console.WriteLine("HandleBtnRemoveClicked Cancelled"); } break; case Event.BtnDisableClicked: _cts.Cancel(); break; case Event.BtnEnableClicked: _cts.Dispose(); _cts = new CancellationTokenSource(); break; } Console.WriteLine("Event loop execution complete."); } } 

Now each time an event is executed the foreach loop is blocked by the await inside the try/catch and I understand why, because of the await on the Task.Run.

However I don't understand why I get desired behavior when I pack it into a method that I don't await. Is it because the await inside yields execution back to HandleEventsBlocking and it resumes the foreach loop? I'd also appreciate a comment on whether this is good practice, it got me far but I just don't understand the tool I'm using it and it makes me worried.

5
  • However I don't understand why I get desired behavior when I pack it into a method that I don't await. Because you don't await. You'd have the same result if you didn't await the Task.Run. Commented Aug 19, 2023 at 20:28
  • @tkausl But to me what is strange is that I can use a try/catch on the await Task.Run inside of my work methods with working error handling. I can't do that if I don't wrap it inside of a method. Commented Aug 19, 2023 at 20:30
  • Try to add await to method calls in your first implementation - you will get the same result. First approach is called async loop pattern (mine naming), second one - just simple sequential loop. Commented Aug 19, 2023 at 20:31
  • @The_Matrix; await means asynchronous wait : you will not get next work item until complete current one. Commented Aug 19, 2023 at 20:33
  • The first version of HadleEventsBlocking should give you a compiler warning about an async method that lacks await. The BlockingCollection<T> is intended for blocking threads. If instead of threads you prefer asynchronous flows (async methods) that lack thread affinity and identity, the correct queue to use is the Channel<T>. For more details see this question: Is there anything like asynchronous BlockingCollection<T>? Commented Aug 19, 2023 at 22:58

1 Answer 1

5

However I don't understand why I get desired behavior when I pack it into a method that I don't await.

Because it's not awaited. You can think of await as "asynchronous wait". It pauses the method until the returned task completes. More info on my blog: https://blog.stephencleary.com/2012/02/async-and-await.html

I'd also appreciate a comment on whether this is good practice, it got me far but I just don't understand the tool I'm using it and it makes me worried.

Absolutely not. Ignoring tasks instead of awaiting them is dangerous: it means any exceptions are silently swallowed and your code cannot know when the processing is complete.

A better approach would be something like TPL Dataflow. Alternatively, you could create multiple consumers for your queue of work.

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

11 Comments

Absolutely not. - i believe we can use this approach, it is async loop design pattern, the only thing to care - to handle all its outcomes (complete/cancelled/failured) via Task.ContinueWith.
@Ryan Isn't that handled by the try/catch block? Anytime I cancel the await Task.Run it is handled by the try/catch and I can do clean up inside of it. Only issue is if it's potentially handled on a different thread.
You got the answer - because it's not awaited. Look for details in the article attached.
@The_Matrix (2/2) Therefore, an async method (the state machine created by the compiler) basically captures any such unhandled exceptions in the returned task object. If you don't await such a task object (or otherwise handle the task object to a similar effect), the task object disappears with any such possible captured exception. Your code catches only OperationCanceledException. Think about what will happen when the code within the try-catch block throws some other exception type...
@The_Matrix it is expected and normal that the asynchronous part of a method (the continuation of a method that happens after awaiting a task) to run in a thread different from the calling thread. In async operations, it doesn't really make sense to use thread id's for tracking control flow or stages of concurrent data processing. If you are looking for alternative approaches, i guess it's best to ask a new question in which you could elaborate the usage scenario and logging requirements...
|

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.