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
- Is it possible to do anything about the fact that the current
SynchronizationContext
isnull
? For example, create one for the current thread? - If that is possible, would the
Progress<T>
capture thatSynchronizationContext
? - 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).
You can set
SynchronizationContext.Current
before constructing the instance to a value of your choice. Reset it afterwards (with afinally
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 inProgress<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.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.