sapito
sapito

Reputation: 1522

Async tasks and locks

I have a list of elements that should be updated by two processes. First one is the UI thread (controlled by the user), second one is a background process that retrieves information from a web service.

Since this second process is I/O bound, it seems suitable for async tasks. This leads me to a few questions:

  1. Since async tasks don't run on separate threads, it seems I don't need any kind of lock when updating this list, right?

  2. On the other hand, can we assume that async tasks will never run on separate threads?

  3. I'm talking about a Windows Forms application. Maybe in the future I want it to run it as a console application. AFAIK, in console applications Async tasks run on separate threads. What's the preferred idiom to ask a task if it's running on a separate thread? This way I can establish a lock when necessary.

  4. The fact that I don't know if I really need a lock makes me wonder wether this is the best design or not. Would it make sense to stick to Task.Run() even for this kind of IO bound code?

Upvotes: 10

Views: 17717

Answers (3)

Yuval Itzchakov
Yuval Itzchakov

Reputation: 149518

Since async tasks don't run on separate threads, it seems I don't need any kind of lock when updating this list, right?

There is no guarantee that one isn't using Task.Run and marking his method async. IO bound async tasks are most likely not using some sort of thread behind the scenes, but that isn't always the case. You shouldn't rely on that for the correctness of your code. You could ensure that your code runs on the UI thread by wrapping it with another async method which doesn't use ConfigureAwait(false). You can always use the concurrent collections given in the framework. Perhaps a ConcurrentBag or a BlockingCollection can suite you needs.

AFAIK, in console applications Async tasks run on separate threads. What's the preferred idiom to ask a task if it's running on a separate thread?

That is incorrect. async operations by themselfs dont run on seperate threads only because they're in a console app. Simply put, the default TaskScheduler in a console app is the default ThreadPoolTaskScheduler, which will queue any continuation on a threadpool thread, as a console has no such entity called a ui thread. Generally, it's all about SynchronizationContext

The fact that I don't know if I really need a lock makes me wonder wether this is the best design or not. Would it make sense to stick to Task.Run() even for this kind of IO bound code?

Definitely not. The fact that you don't know is the reason you posted this question, and the reason we're trying to help.

There is no need to use a threadpool thread to be doing async IO. The whole point of async in IO is the fact that you can free the calling thread doing IO to process more work while the request is being processed.

Upvotes: 10

Stephen Cleary
Stephen Cleary

Reputation: 456322

Since async tasks don't run on separate threads, it seems I don't need any kind of lock when updating this list, right?

True enough. This approach works well if you follow a functional pattern (i.e., each background operation will return its result, rather than update shared data). So, something like this will work fine:

async Task BackgroundWorkAsync() // Called from UI thread
{
  while (moreToProcess)
  {
    var item = await GetItemAsync();
    Items.Add(item);
  }
}

In this case, it doesn't matter how GetItemAsync is implemented. It can use Task.Run or ConfigureAwait(false) all it wants - BackgroundWorkAsync will always sync up with the UI thread before adding the item to the collection.

Maybe in the future I want it to run it as a console application. AFAIK, in console applications Async tasks run on separate threads.

"Async tasks" don't run at all. If this is confusing, I have an async intro that may be helpful.

Every asynchronous method starts out being executed synchronously. When it hits an await, it (by default) captures the current context and later uses that to resume executing the method. So, what happens when it's called from a UI thread is that the async method resumes on the captured UI context. Console apps do not provide a context, so the async method resumes on a thread pool thread.

What's the preferred idiom to ask a task if it's running on a separate thread? This way I can establish a lock when necessary.

I'd recommend a design that doesn't ask such threading questions. First off, you could just use a plain lock - they're extremely fast when there's no contention:

async Task BackgroundWorkAsync() // Called from any thread
{
  while (moreToProcess)
  {
    var item = await GetItemAsync();
    lock (_mutex)
        Items.Add(item);
  }
}

Alternatively, you could document that the component depends on a one-at-a-time context provided, and use something like AsyncContext from my AsyncEx library for the Console app.

Upvotes: 6

NeddySpaghetti
NeddySpaghetti

Reputation: 13495

Asyc-await captures the synchronization context before the await statement and then by default runs the continuation on the same context after the await statement. Synchronization context for UI thread are only associated with one thread, so in that sort of scenario you are you could end up always updating the list from the UI thread.

But if someone changes the code to call ConfigureAwait(false) after on of the awaits the continuation will not run on the original synchronization context and you can end up updating the list on one of the thread pool threads.

Also note that you cannot use await inside a lock statement but you can use a SemapahoreSlim to do an asynchronous wait instead.

  1. IMHO it's much better to just use a synchronized collection rather than relying on the list being updated from the same thread.

  2. You cannot assume that, the current synchronization context will be captured but continuations may not always run on it.

  3. I would use a synchronized collection or SempahoreSlim in this case. For console app the thread pool synchronizationo context is used and continuations could end up running on any of the thread pool threads.

  4. It makes sense to use async-await for IO bound code as it doesn't consume a thread.

I'd stick with using async-await and change to use a thread safe collection or synchronize using SemaphoreSlim

Upvotes: 2

Related Questions