4

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).

Community
  • 1
  • 1
Gavin Hope
  • 2,173
  • 24
  • 38

1 Answers1

2

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.

usr
  • 168,620
  • 35
  • 240
  • 369
  • I tried creating a context with `new SynchronizationContext();` to set `SynchronizationContext.Current`; I still get crashes resulting from cancellation exceptions being thrown on the ThreadPool. From the code in the `Progress` implementation, if there is not context at the time of construction, it sets a default context, also using `new SynchronizationContext();` with the comment: `// A default synchronization context that targets the ThreadPool`. Assuming that the `Progress` object captures the context I make, it seems a sync context is the wrong mechanism for the communication I need. – Gavin Hope Dec 20 '16 at 15:20
  • About the `SynchronousProgress` implementation I posted, you're right that it behaves just like an event that does not know anything about threads... but it doesn't run the callback right away. It's a much simpler implementation than the .NET `Progress`, but it is still constructed with `Action` that's only invoked when a client decides to use `Report()`. – Gavin Hope Dec 20 '16 at 15:34
  • 1
    The `SynchronizationContext` class does nothing interesting, yes. You need to pass a context that targets the thread you want targeted. Is this not a UI thread (which is an easy case)? For a thread to be targeted it must cooperate. It must run some kind of message loop. Stephen Cleary's "Nito" library has something for that I believe. Is it this maybe? http://blog.stephencleary.com/2012/02/async-console-programs.html I'm not familiar with installing a custom context. I also wonder why you want everything to happen on the same thread. What higher goal are you trying to achieve? – usr Dec 20 '16 at 15:39
  • I've update the question to provide some background information on usage. Cheers! – Gavin Hope Dec 20 '16 at 15:56
  • @GavinHope so do I understand you correctly that what you want is to make the progress calls occur on the UI thread of the dialog so that it is legal to touch controls? Your current implementation is not legal because it touches the UI from some other thread. This often appears to work, I would not do this. – usr Dec 20 '16 at 16:29
  • No, I wasn't clear ;) the "custom, cancellable progress dialog" that I mentioned uses a few classes, one of which is responsible for marshalling updates from a worker thread back to the UI thread (using `Task`s and a `TaskScheduler` for the UI thread). That design does not expect the background work to be moved to another thread, because it needs to handle cancellation exceptions. Here, I've done that, as using the .NET `Progress` class introduces the Thread Pool. My goal was to work nice with an application-plugin boundary; the .NET interface, `IProgress` is enough though, so I'm ok :) – Gavin Hope Dec 21 '16 at 08:41
  • OK, I guess this is resolved for you. I wonder, though, what role cancellation plays here. Normally, cancellation propagates up into the surrounding task (or up the stack in general) which is where you can find out that it occurred. It should not interact with progress reporting. One more comment: If you create the progress object on the UI thread everything should work automatically. – usr Dec 21 '16 at 10:54