简体   繁体   中英

Changing SynchronizationContext Within Async Method

I am trying to post a lean question here, without code, because my question is so specific: Is it possible/acceptable to modify the SynchronizationContext within an Async method? If I don't set SynchronizationContext when the Async method begins, it seems that code within it--including events I raise and methods within the same class module I call--run on the same worker thread. However, when it comes time to interact with the UI, I find that the SynchronizationContext has to be set to the UI thread.

Is it okay to keep the SynchronizationContext set to the worker thread until such time that I want to invoke a call to a UI-based function?

EDIT: To further clarify my above question, I find myself in a no-win situation with respect to the SynchronizationContext setting. If don't set the SynchronizationContext then my async operations run on a separate thread (as desired) but then I can't return data to the UI thread without encountering a cross-thread operation exception; if I set the SynchronizationContext to my UI thread then the operations I want to run on a separate thread end up running on the UI thread--and then (of course) I avoid the cross-thread exception and everything works. Clearly, I'm missing something.

If you care to read more, I've tried to provide a very clear explanation of what I'm trying to do and I realize you're investing your time to understand so thank you for that!

What I'm trying to do is shown in this flow diagram:

在此处输入图片说明

I have a Winforms application running on the UI thread (in black); I have a Socket object I'd like to run on its own thread. The job of the socket class is to read data from a communication socket and raise events back to the UI whenever data arrives.

Note that I start a message loop from the UI thread so that the socket is continually polled for data on its own thread; if data is received, I want to process that data synchronously on the same non-UI thread before grabbing any more data from the socket. (Yes, if something goes awry with any particular socket read, the socket might be left with unread data in it.)

The code to start the message loop looks like this:

if (Socket.IsConnected)
{
   SetUpEventListeners();
   // IMPORTANT: next line seems to be required in order to avoid a cross-thread error
   SynchronizationContext.SetSynchronizationContext(new WindowsFormsSynchronizationContext());
   Socket.StartMessageLoopAsync();
}

When I start the message loop from the UI thread, I call an async method on the Socket object:

 public async void StartMessageLoopAsync()
  {
     while (true)
     {
        // read socket data asynchronously and populate the global DataBuffer
        await ReadDataAsync();

        if (DataBuffer.Count == 0)
        {
           OnDataReceived();
        }
     }
  }

The Socket object also has the OnDataReceived() method defined as:

  protected void OnDataReceived()
  {
     var dataEventArgs = new DataEventArgs();
     dataEventArgs.DataBuffer = DataBuffer;

     // *** cross-thread risk here!
     DataReceived?.Invoke(this, dataEventArgs);
  }

I have highlighted two areas of the diagram with "1" and "2" blue stars.

In "1" (on the diagram), I am using the async/await pattern. I am using a third-party socket tool that doesn't support Async, so I have wrapped that in my own ReadDataAsync() function that looks like this:

  public override async Task ReadDataAsync()
  {
     // read a data asynchronously
     var task = Task.Run(() => ReadData());
     await task;
  }

ReadData() wraps the third-party component's read method: it populates a global data buffer, so no return value is needed.

In "2" in the diagram, I am encountering the cross-thread risk described in my OnDataReceived() method cited above.

Bottom Line: If I set SynchronizationContext as shown in my first code clip above, then everything in the Socket object runs on its own thread until I try to invoke the DataReceived event handler; if I comment-out the SynchronizationContext then the only part of the code that runs on its own thread is the brief third-party socket-read operation wrapped in my DataReadAsync() method.

So my thought was whether I can set the SynchronizationContext just ahead of trying to invoke the DataReceived event handler. And even if I "can", the better question is whether this is a good idea. If I do modify SynchronizationContext within the non-UI thread then I'd have to set it back to its original value after invoking the DataReceived method, and that has a Durian-like code-smell to me.

Is there an elegant tweak to my design or does it need an overhaul? My goal is to have all red items in the diagram running on a non-UI thread and the black items running on the UI thread. "2" is the point at which the non-UI thread my cross over to the UI thread...

Thank you.

Setting context directly isn't the best idea since other functionality being occasionally executed in this thread can be affected. The most natural way to control synchronization context for async/await flows is using ConfigureAwait . So in your case I see two options to achieve what you want:

1) Use ConfigureAwait(false) with ReadDataAsync

public async void StartMessageLoopAsync()
{
    while (true)
    {
        // read socket data asynchronously and populate the global DataBuffer
        await ReadDataAsync().ConfigureAwait(false);

        if (DataBuffer.Count == 0)
        {
            OnDataReceived();
        }
    }
}

which will make resuming everything after await in background thread. And then use Dispatcher Invoke to marshal DataReceived?.Invoke into UI thread:

protected void OnDataReceived()
{
   var dataEventArgs = new DataEventArgs();
   dataEventArgs.DataBuffer = DataBuffer;

   // *** cross-thread risk here!
   Dispatcher.CurrentDispathcer.Invoke(() => { DataReceived?.Invoke(this, dataEventArgs ); });
}

Or 2) Make some logic decomposition like as follows:

    public async void StartMessageLoopAsync()
    {
        while (true)
        {
            // read socket data asynchronously and populate the global DataBuffer
            await ProcessDataAsync();

            // this is going to run in UI thread but there is only DataReceived invocation
            if (DataBuffer.Count == 0)
            {
                OnDataReceived();
            }
        }
    }

OnDataReceived is thin now and does only event triggering

    protected void OnDataReceived()
    {
        // *** cross-thread risk here!
        DataReceived?.Invoke(this, dataEventArgs);
    }

This consolidates the functionality supposed to run in background thread

    private async Task ProcessDataAsync()
    {
        await ReadDataAsync().ConfigureAwait(false);

        // this is going to run in background thread
        var dataEventArgs = new DataEventArgs();
        dataEventArgs.DataBuffer = DataBuffer;
    }

    public override async Task ReadDataAsync()
    {
        // read a data asynchronously
        var task = Task.Run(() => ReadData());
        await task;
    }

This seems like a scenario better served with reactive extensions:

Reactive Extensions for .NET

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM