Is it possible to control the SynchronizationContext used by a .NET Progress<T> object?

505 views Asked by At

From the Progress<T> Class reference page:

Any handler provided to the constructor or event handlers registered with the ProgressChanged event are invoked through a SynchronizationContext instance captured when the instance is constructed. If there is no current SynchronizationContext at the time of construction, the callbacks will be invoked on the ThreadPool.

I'm creating a Progress<int> on a background thread. I want the callbacks, and any associated cancellations (throwing an OperationCancelledException), to occur on the same thread.

At the time of constructing the Progress<int> object, SynchronizationContext.Current is null.

And so, as the above documentation tells me, the callbacks are being executed on the Thread Pool...

Questions

  1. Is it possible to do anything about the fact that the current SynchronizationContext is null? For example, create one for the current thread?
  2. If that is possible, would the Progress<T> capture that SynchronizationContext?
  3. If that is possible, would that reliably mean that the callbacks are executed on the same thread? As, I've read in other places, such as this SO answer, that:

...a SynchronizationContext does not necessarily represent a specific thread; it can also forward invocation of the delegates passed to it to any of several threads (e.g. to a ThreadPool worker thread)...

Solution/workaround

To make sure that callbacks are happening on the same thread, I'm using my own implementation of the IProgress<T> interface:

public class SynchronousProgress<T> : IProgress<T>
{
    private readonly Action<T> action;

    public SynchronousProgress(Action<T> action)
    {
        this.action = action;
    }

    public void Report(T value)
    {
        action(value);
    }
}

It works. However, I'm still wondering if there is a way to achieve this with the .NET Progress<T> class?

Update: background information

The attempted usage of the Progress<T> class sits inside a custom, cancellable progress dialog, which encapsulates doing some work and reporting progress. In this case, the work (that can be cancelled) is occuring on the other side of a plugin boundary. It's desirable to use a .NET type (e.g. IProgress<T>) in the plugin interface for communicating progress, rather than a custom type (e.g. our own, (older) IProgress type).

The callback being given to the .NET IProgress<T> implementation, is simply an instruction to increment the progress of the custom IProgress implementation. Along the lines of:

public void Export(CulturedStreamWriter writer, IProgress progress) // that's a custom IProgress
{
    progress.Steps = toExport.Count;
    exporter.Export(toExport, writer, new SynchronousProgress<int>(progress.StepTo)); // increment the progress of the custom IProgress
}

Using the .NET Progress<T> in place of the SynchronousProgress<T> does not work, as cancellation exceptions are being thrown on a different thread to this code, which is where they need to be caught.

Seems as the custom implementation of the .NET IProgress<T> is working (SynchronousProgress<T>), perhaps it is in fact the most appropriate approach (given the surrounding code/constraints).

1

There are 1 answers

7
usr On

You can set SynchronizationContext.Current before constructing the instance to a value of your choice. Reset it afterwards (with a finally block to make sure you don't permanently mess up the thread).

This is a bit ugly. WebClient requires the same thing (unrelated to this question - just an example). I find it an API omission in Progress<T> that you can't provide the sync context. You could consider opening an issue on GitHub as a public service.

If you want to can just fork the source code of Progress<T> and add a constructor argument for the sync context. It's a small, self-contained class.

If that is possible, would that reliably mean that the callbacks are executed on the same thread?

They would run wherever that sync context chooses to run them. Depends on the context.

Your own implementation really just runs the callback right now which seems pointless. This IProgress implementation behaves just like an event that does not know anything about threads. It will not target any thread in particular. I doubt this is what you need although I can't be sure.