10

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?

Yuval Itzchakov
  • 146,575
  • 32
  • 257
  • 321
sapito
  • 1,472
  • 3
  • 17
  • 26
  • 1
    And by *process* you mean actual separate windows process, or what? If yes, how do you share your list between processes? I would guess it's not actually a process, but you should confirm that... Is it something that user starts by interaction with UI, is it something that runs based on a timer? That is quite important information. – MarcinJuraszek Jan 02 '15 at 05:20
  • It doesn't run on a timer. It's just a function that updates a very long list. Since updating each element involves connecting to a web service, updating the full list may take some seconds or even minutes. – sapito Jan 02 '15 at 12:59

3 Answers3

9

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.

Community
  • 1
  • 1
Yuval Itzchakov
  • 146,575
  • 32
  • 257
  • 321
  • 2
    I disagree with the point on Concurrency and collections. I agree there is no guarantee that you run on the UI thread. However, it is good practice to only access collections from the UI thread due to data-binding. This suggests that the `async` I/O should be pure (thus thread safe), and the `Presenter/Controller/whatever` should be thread-unsafe, and demand the callback to run on the UI thread. However with regards to `lock`ing. The point of `async` programming is to make it easier to write single threaded blocking (lock) free programs. – Aron Jan 02 '15 at 09:58
  • I like this answer, but I think Aron made a good point. @Aron: Do you suggest using `Control.Invoke` in order to update both the list and the UI? – sapito Jan 02 '15 at 13:01
  • @Aron - If the collection is data-bound to UI elements, then i agree and actually the OP doesn't have a choice in that case. Otherwise, i see no reason to be limiting yourself to the UI thread and having to use the UI message loop to access a collection. – Yuval Itzchakov Jan 02 '15 at 13:03
  • @Yuval Itzchakov: actually the collection is "partly bound". Everytime I update one of its elements, I should check if it belongs to the set that is being displayed. If it is, I must also update the UI. I think it makes sense to run all updates on UI thread, even if not all elements are bound. – sapito Jan 02 '15 at 14:21
  • @sapito If it makes sense in this particular case, go for it :) Though i would argue this might not be the best solution for cases where UI elements aren't involved. – Yuval Itzchakov Jan 02 '15 at 14:25
  • @YuvalItzchakov: Yes, I fully agree with you, and obviously I will consider a different approach for the console app (but the console app is not my main concer right now). – sapito Jan 02 '15 at 14:40
6

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.

Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
  • Thanks, yes, obviously at least for the Windows Forms app, it seems I can ignore all sync mechanisms because the updates will run on the UI thread. But as stated in the comments of the accepted answer, wouldn't it be better to call `Control.Invoke` to both update the list and the UI? I am aware that it isn't necessary, but I feel like it will make the code more "robust". Maybe in the future somebody may turn this async task to run on the thread pool (`Task.Run`), or when porting it to a console application, as we don't have a `Control.Invoke`, compiler will make us aware of the issue. – sapito Jan 02 '15 at 17:07
  • @sapito: No. I recommend everyone to **never** use `Control.Invoke`. A simple `lock` should suffice. – Stephen Cleary Jan 02 '15 at 17:10
  • I see your point for the console app. But on Windows Forms, either the lock isn't needed (actually it isn't) or I need to use `Control.Invoke`, because after updating the list I also need to update the UI. I.e, for the Windows Forms app, usign the lock but not using `Control.Invoke` would be incoherent. But I see your advice is not to use anything at all for the WinForms app. – sapito Jan 02 '15 at 17:16
  • @sapito: I recommend you design for the use cases you have. If your use cases change in the future, change the code at that time. – Stephen Cleary Jan 02 '15 at 17:46
  • yes, thanks. I think I'm falling into the design paralysis. I already got a few solutions that should work. Time to implement it... – sapito Jan 02 '15 at 18:36
2

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

NeddySpaghetti
  • 13,187
  • 5
  • 32
  • 61
  • Good points, thanks. I will consider them for the console app. Right now my main concern is the Windows forms one. – sapito Jan 02 '15 at 14:34