-1

I have a method that is "partially" async, meaning that one code path runs async and the other runs synchronously. I can't currently make the synchronous part async, although I may be able to in the future.

public async Task UpdateSomethingAsync(){

    if (ConditionIsMet){
    
      await DoSomethingAsync;
   
    }else{

      DoSomethingSynchronous;
    }
}

Both DoSomethingAsync and DoSomethingSynchronous are I/O bound. Calling this method from the Winforms UI thread with "await" causes it to block the UI thread if the Synchronous path is taken, which is to be expected.

private async void MyDropDownBox_DropDownClosed(object sender, EventArgs e)
{
    //This blocks if the DoSomethingSynchronous path is taken, causing UI to 
   //become unresponsive.
    await UpdateSomethingAsync();  

}

So off to Stephen Cleary's blog I go. His suggestion (although for CPU bound code instead of I/O bound) is to run the method with Task.Run, as if it were completely synchronous, while documenting that the method is "partially" async. However, events raised by DoSomethingSynchronous now cause an exception, I believe due to the fact that they are now on a different thread from the UI.

private async void MyDropDownBox_DropDownClosed(object sender, EventArgs e)
{
    //This no longer blocks, but events will not marshal back to UI Thread 
   //causing an exception.
    await Task.Run(()=> UpdateSomethingAsync());  
}

How can this be fixed?

Casey Wilkins
  • 2,555
  • 2
  • 23
  • 31

6 Answers6

3

Don't update the UI, or any model bound to the UI inside of UpdateSomethingAsync or any of the methods that it calls. Create a class that will hold the data required to update your UI, and return an instance of that class from UpdateSomethingAsync.

DoSomethingAsync will return a Task<ThatClassYouCreated> and DoSomethingSynchronous just returns an instance of ThatClassYouCreated. Then, back in MyDropDownBox_DropDownClosed after you await UpdateSomethingAsync, use the instance returned by UpdateSomethingAsync to update your UI or your model.

public class UpdatedInformation
{
    public int UpdateId { get; set; }
    public string UpdatedName { get; set; }
    public DateTimeOffset Stamp { get; set; }
    // etc, etc...
}

public class YourForm : Form
{
    private async Task<UpdatedInformation> DoSomethingAsync()
    {
        var result = new UpdatedInformation();
        // Something is awaited...
        // Populate the properties of result.
        // Do not modify your UI controls. Do not modify the model bound to those controls.
        return result;
    }
    
    private UpdatedInformation DoSomethingSynchronous()
    {
        var result UpdatedInformation();
        // Populate the properties of result.
        // Do not modify your UI controls. Do not modify the model bound to those controls.
        return result;
    }

    private async Task<UpdatedInformation> UpdateSomethingAsync()
    {
        if (ConditionIsMet)
        {
            return await DoSomethingAsync();
        }
        else
        {
            return await Task.Run(DoSomethingSynchronous);
        }
    }

    private async void MyDropDownBox_DropDownClosed(object sender, EventArgs e)
    {
        var updatedInformation = await UpdateSomethingAsync();
        // Now use updatedInformation to update your UI controls, or the model bound to
        // your UI controls.
        model.Id = updatedInformation.UpdateId;
        // etc...
    }
}
Johnathan Barclay
  • 18,599
  • 1
  • 22
  • 35
Joshua Robinson
  • 3,399
  • 7
  • 22
2

In your event handler, you can use Invoke() to update the UI like this:

private void someEventHandler() // <- it might have params
{

    // ... possibly some other code that does NOT update the UI ...

    this.Invoke((MethodInvoker)delegate {

        // ... it's safe to update the UI from in here ...

    });

    // ... possibly some other code that does NOT update the UI ...

}

I don't know who keeps doing it, but my comments below this post keep getting deleted.

This answers the TITLE of the question, which was:

How do I marshal an event from Task.Run back to the UI thread?

When you receive an event from a different thread, this is a perfectly valid way of updating the UI.

E_net4
  • 27,810
  • 13
  • 101
  • 139
Idle_Mind
  • 38,363
  • 3
  • 29
  • 40
  • "when you receive an event from a different thread, this is a perfectly valid way of updating the UI." This event is *not* firing from a non-UI thread, and as such, that quote of mine most certainly does not say what you're claiming it does. Please do not misrepresent my statements in your answer. – Servy Jul 28 '20 at 20:44
  • Huh?...the author explicitly stated in their original description: "However, events raised by DoSomethingSynchronous now cause an exception" So if `DoSomethingSynchronous()` is being run via Task.Run(), then the event would be raised by a different thread. What am I missing here? – Idle_Mind Jul 28 '20 at 20:51
  • @Servy, where would the event in my original code be firing from, if not the Thread Pool thread that is started with Thread.Run? My original assumption was that the exceptions I was getting was a result of attempted UI access from another (thread pool) thread, via the event. – Casey Wilkins Jul 28 '20 at 20:53
  • Yes, *if you call `Task.Run`* then that code you pass to it will run in a thread pool thread. That's radically different from saying the event is fired from a non-UI thread. The event fires in the UI thread. **If you intentionally ask for some code to run in a non-UI thread**, then *just that code* will run in a non-UI thread. That doesn't mean the event is firing from a non-UI thread. – Servy Jul 28 '20 at 20:54
  • I think this solution is valid, although maybe not as elegant. Unfortunately I don't think I can use it as I'm not handling the event myself, but rather a Winforms BindingSource is handling a PropertyChanged event I'm raising from DoSomethingSynchronous. – Casey Wilkins Jul 28 '20 at 20:56
  • @CaseyWilkins The event is being fired from the UI thread. That's why you can update the UI from the event handler so long as you aren't explicitly marshaling to a thread pool thread first. – Servy Jul 28 '20 at 20:56
  • @CaseyWilkins You've shown the event handlers in the code in the question, so it would seem you *are* handling the event yourself. – Servy Jul 28 '20 at 20:58
  • @Servy, I think in this cases the event is firing from a non-UI thread. The confusion may be from my question wording. In my original question, the UpdateSomethingAsync method is called via Task.Run. Within UpdateSomethingAsync, two other methods (DoSomethingAsync and DoSomethingSynchronous) can be called, both of which can raise a PropertyChanged event, presumably on the Thread Pool thread started by Task.Run... which is where I'm assuming the problem lies. – Casey Wilkins Jul 28 '20 at 20:59
  • 1
    @Servy the event in question is a PropertyChanged event, raised by DoSomethingAsync or DoSomethingSynchronous. I'm not handling those. The handlers present are for UI elements, maybe I should have left them out for clarity. – Casey Wilkins Jul 28 '20 at 21:01
  • @CaseyWilkins All winforms controls fire their events in the UI thread. The event handler most certainly is running in the UI thread. You're just intentionally marshaling to a thread pool thread, and showed the code that does it. – Servy Jul 28 '20 at 21:01
  • @CaseyWilkins If you want to be manipulating the UI then *don't explicitly run the code in a thread pool thread*. You're going out of your way to leave the UI thread. If you want to update the UI, then don't do that. Do your non-UI work in thread pool theads, not your UI work. – Servy Jul 28 '20 at 21:03
  • @Servy I'm not referencing any events from Winform controls in my question (trying not to at least), but an event I'm manually firing myself, from within DoSomethingAsync or DoSomethingSynchronous. It is a PropertyChanged event that lets a BindingSource (on the UI) know that a specific property on a bound item has changed, and that it should update. The PropertyChanged event gets fired after DoSomethingAsync or DoSomethingSynchronous updates some properties. – Casey Wilkins Jul 28 '20 at 21:09
  • @CaseyWilkins So it sounds like your program is designed such that updates to your models are expected to be done from the UI thread (which makes sense, I wouldn't expect that they're written to behave properly in the face of multithreaded accesses) so updates to your model *are UI updates* from the perspective of your code, and should be treated accordingly – Servy Jul 28 '20 at 21:11
  • @Servy, it depends on if Async can be used, which kinda comes full circle. I I have a need to keep the UI responsive. If I can use Async, great, I can essentially stay on the UI thread and all model updating is done there, async takes care of keeping the UI responsive. If I can't, then I have to offload the model updates to a thread pool thread via Task.Run to keep the UI responsive. Those model updates end up firing a PropertyChanged event... which may be on a thread pool thread if started via Task.Run. Make sense? I'm probably just not explaining it too well. – Casey Wilkins Jul 28 '20 at 21:14
  • @CaseyWilkins You should use `Task.Run` for long running *non-UI* operations. Not for UI updates (because then it obviously doesn't work). Put your *non-UI* code in a `Task.Run` call, return the results that you need, then update the UI with those results. – Servy Jul 28 '20 at 21:16
1

Sicne you state that "[..] DoSomethingSynchronous [is] I/O bound" you could also make it async by wrapping the IO bound operation within DoSomethingSynchronous in a Task.Run.

So if DoSomethingSynchronous is something like

public void DoSomethingSynchronous(...) 
{
    // some UI work

    // blocking sysnchornous IO operation
    var res = IoOperation();

    // some more UI work
}

you could rewrite it to.

public async Task DoSomethingSynchronous(...) 
{
    // some UI work

    // no-UI-Thread blocking IO operation
    var res = await Task.Run(() => IoOperation()).ConfigureAwait(true);

    // some more UI work
}

the .ConfigureAwait(true) could maybe omited but ensures that the code after the await will be scheduled in the orignal sync-context i.e. the UI-Thread.


You then obviously need to rename the method and such, but this will make the code more maintainable if you someday can use a true asycn IO in DoSomethingSynchronous

Ackdari
  • 3,222
  • 1
  • 16
  • 33
0

Since UpdateSomethingAsync needs to access the UI context, it shouldn't be wrapped in a Task.Run call. (You should very rarely, need to call an async method from Task.Run, usually only if the method is implemented incorrectly and you can't fix it.)

Instead DoSomethingSynchronous should be the thing you call from Task.Run. After all, the purpose of that method is to asynchronously run a synchronous method in a thread pool thread. So only use it for the synchronous method you want run in a thread pool thread, not the (supposedly) asynchronous method that needs to access the UI context.

Servy
  • 202,030
  • 26
  • 332
  • 449
  • This is where I was initially leaning, I'm just trying to make sure I follow best practices as well as I can. I typically try to only call Task.Run at the UI level when the intent of the call is to keep the UI responsive, but this may be the best compromise in this case. – Casey Wilkins Jul 28 '20 at 19:53
  • @CaseyWilkins You should call `Task.Run` when you have a long running synchronous method that you want to run in a thread pool thread. That sounds like exactly the case here. – Servy Jul 28 '20 at 19:54
  • I'm with you, but Stephen Cleary seems to be against that type of thing unless the synchronous method is CPU bound. My original usage of Task.Run at the UI level came from the question linked below, where he discusses a somewhat similar scenario at the end of his answer. The slight difference in my case is that it isn't a mixture of CPU Bound and I/O bound, but a mixture of Async and Sync I/O bound, if that makes sense. https://stackoverflow.com/questions/18013523/when-correctly-use-task-run-and-when-just-async-await – Casey Wilkins Jul 28 '20 at 20:00
  • 1
    @CaseyWilkins If your synchronous method is just blocking on IO work then ideally you'd change it to call an inherently asynchronous version of that IO operation so you can `await` it. But if there is no inherently asynchronous version provided, you have no alternative but to do the sad thing and have a thread pool thread just sit there and wait for it. It's not like you can have the UI thread sit there and wait for it instead. So your only real solutions here are wrap the synchronous method in a `Task.Run` call or make it fundamentally asynchronous by not using synchronous IO anywhere. – Servy Jul 28 '20 at 20:03
0

WinUI 3 respects the below method.

DispatcherQueue.TryEnqueue(() =>
{
     //Code to Update the UI
});
-1

Figured I'd answer this myself after some more research. Most of the other answers are correct in some way, but don't necessarily explain the whole deal in one go, so I'll try to sum up here.

This first snippet from the question works event wise, but blocks if the Synchronous path in UpdateSomethingAsync is taken. Events work because "await" automatically captures the SynchronizationContext (this is key) for the UI thread, such that any events raised from UpdateSomethingAsync are marshalled back to the UI, via the SynchronizationContext. This is just the normal way of using async/await:

private async void MyDropDownBox_DropDownClosed(object sender, EventArgs e)
  {
     //This blocks if the DoSomethingSynchronous path is taken, causing UI to 
     //become unresponsive, but events propagate back to the UI correctly.
     await UpdateSomethingAsync();  

  }

Task.Run works in much the same way, if you aren't using it to run an async method. In other words, this works without blocking and will still send events to the UI thread, because UpdateSomethingAsync is replaced with a Synchronous method. This is just the normal usage of Task.Run:

private async void MyDropDownBox_DropDownClosed(object sender, EventArgs e)
  {
     //UpdateSomethingAsync is replaced with a Synchronous version, and run with
     // Task.Run.

     await Task.Run(UpdateSomethingSynchronously());  

  }

However, the original code in question is Async, so the above doesn't apply. The question poses the following snippet as a possible solution, but it errors out with an Illegal Cross Thread call to the UI when an event is raised, because we are using Task.Run to call an Async method, and for some reason this does not set the SynchronizationContext:

private async void MyDropDownBox_DropDownClosed(object sender, EventArgs e)
  {
     //This no longer blocks, but events raised from UpdateSomethingAsync
     //will cause an Illegal Cross Thread Exception to the UI, because the
     //SyncrhonizationContext is not correct.  Without the SynchronizationContext,
     //events are not marshalled back to the UI thread.
     await Task.Run(()=> UpdateSomethingAsync());  
  }

What does seem to work is to use Task.Factory.StartNew to assign the UI SynchronizationContext to the Task using TaskScheduler.FromCurrentSynchronizationContext, like so:

private async void MyDropDownBox_DropDownClosed(object sender, EventArgs e)
{
    //This doesn't block and will return events to the UI thread sucessfully,
    //because we are explicitly telling it the correct SynchronizationContext to use.
    await Task.Factory.StartNew(()=> UpdateSomethingAsync(),
                              System.Threading.CancellationToken.None,
                              TaskCreationOptions.None,
                              TaskScheduler.FromCurrentSynchronizationContext);  
}

What also works, and is very simple but "lies" a little to the caller, is to simply wrap DoSomethingSynchronous in Task.Run:

public async Task UpdateSomethingAsync(){

    if (ConditionIsMet){

      await DoSomethingAsync;

    }else{

      await Task.Run(DoSomethingSynchronous);
    }
}

I consider this a little bit of a lie, because the method is not really fully Async in the sense that it spins off a Thread Pool thread, but may never pose an issue to a caller.

Hopefully this makes sense. If any of this is proven incorrect please let me know, but this is what my testing has uncovered.

Casey Wilkins
  • 2,555
  • 2
  • 23
  • 31