1

I am using System.Threading.Timer to asynchronously repeat a certain task. When the task is ready, a method on the original thread (where Start() was called) should be executed.

public event EventHandler StatusChanged;

public void Start()
{
    StatusChanged += new EventHandler(SomethingChangedMethod);
    new Timer(Pending, null, 0, 1000);
}

private void Pending(object state)
{
    //Do Something
    StatusChanged?.Invoke(this, EventArgs.Empty);
}

Since I am not on a control or something, I cannot call Invoke() or BeginInvoke().

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
rkgghz
  • 456
  • 8
  • 19
  • What kind of thread is the original thread? Is it the UI thread, a `ThreadPool` thread, or it was started manually using the `Thread` constructor? – Theodor Zoulias Oct 18 '21 at 09:12
  • Since Start() can be called from outside the library I have no real control about that. – rkgghz Oct 18 '21 at 10:02
  • Or is it something the user has to care about in his 'SomethingChangedMethod' (e.g. with 'BeginInvoke()' on a Control) – rkgghz Oct 18 '21 at 10:09
  • 1
    As a side note, if I were in your shoes I would be very worried about allowing unknown code to run on the callback of a periodic `Timer`. This component offers no protection against overlapping invocations of the callback. Not only the unknown code may not be prepared for concurrency, but also you may end up with an ever increasing number of concurrent executions, resulting to depletion of resources and `ThreadPool` starvation. You could check [this](https://stackoverflow.com/questions/684200/synchronizing-a-timer-to-prevent-overlap) question for solutions. – Theodor Zoulias Oct 18 '21 at 23:41
  • Thanks for the info. In my code am am using `SemaphoreSlim` to prevent concurrent executions. (I just reduced it in the simplified example). But regarding the issue with the caller (not beeing prepared for concurrency), that was the exact reason why I published this question. – rkgghz Oct 19 '21 at 06:56
  • Yeah, the `SemaphoreSlim` will solve the concurrency issue, but not the depletion of resources issue. After running for a few hours you may end up with hundreds or even thousands of `ThreadPool` threads blocked, waiting for their turn to acquire the semaphore, with each one of them occupying [1 MB](https://stackoverflow.com/questions/28656872/why-is-stack-size-in-c-sharp-exactly-1-mb) of memory or more. – Theodor Zoulias Oct 19 '21 at 07:14
  • You might also find [this](https://stackoverflow.com/questions/30462079/run-async-method-regularly-with-specified-interval "Run async method regularly with specified interval") question interesting. It might not be applicable in your case, but using an asynchronous loop instead of a `Timer` makes it easy to enforce a non-overlapping policy in a periodic execution. Handling exceptions of unknown code can be tricky though. – Theodor Zoulias Oct 19 '21 at 07:24
  • That is a good point. The work done in Pending() normally should not take this long, but who knows. Would you again use an EventHandler inside the asynchronous loop, to inform the user about the changed status, or should I use a return value of the awaited method? – rkgghz Oct 19 '21 at 08:07
  • 1
    Yes that works. If I use the return of the awaited method(just a bool in my case) I can call the `Invoke()` directly from `Start()` if needed. This solves also the initial problem of the correct thread. Again thanks a lot for your help. – rkgghz Oct 19 '21 at 08:46
  • Btw not many components, intended for multithreaded usage, expose events in general. The event pattern is more suitable for single-thread scenarios. An alternative way to push StatusChanged notifications would be via an `IProgress` object, that the caller would provide in the constructor of your component. The `T` can be anything (a `bool`, a value tuple, a custom class etc). The built-in implementation of this interface, the `Progress` class, captures the `SynchronizationContext` in its constructor, making it convenient for GUI applications to receive the notifications on the UI thread. – Theodor Zoulias Oct 19 '21 at 09:10

2 Answers2

3

Since Start() can be called from outside the library I have no real control about that.

As a library author, you don't know what the right thing to do is. For all you know the original thread was just spun up to call Start and exited a long time ago. You don't know that returning to the "same" "thread" is the right behaviour, so of course, yes, you leave it up to your consumers.

In the same way that, as a library author, you shouldn't be fixing choices about what logging framework to use, if & how to show error messages to users (if there even are users), etc.

Do the simple thing, raise your event, let the person picking the framework(s) for the application make the right choices. Because you should not (without necessarily impairing the potential consumers of your library)

Damien_The_Unbeliever
  • 234,701
  • 27
  • 340
  • 448
  • Thank you for the info. So have to tell the consumer in the documentation, that 'StatusChanged' event can be called from a different thread. I will accept the other answer, because it handles mor the orginal question. – rkgghz Oct 18 '21 at 10:27
  • 1
    I agree in general, but it's worth noting that there are specific cases in the runtime which *do* capture the caller's SynchronizationContext, and post back to it, e.g. `BackgroundWorker` and `Progress`. Generally though make this clear in your documentation. It's common practice for asynchronous events to be raised on ThreadPool threads so callers are probably expecting this, but there's no harm in calling it out. If you do capture the SynchronizationContext, then document this also. – canton7 Oct 18 '21 at 10:27
2

First off, you may or may not be able to post a message to the thread which called Start: you'll only be able to do this if the thread has a message queue associated with it, and is checking that queue. UI threads do this, but e.g. threadpool threads, or threads which you've created with new Thread(), won't.

If a thread has a message queue associated with it, it's common practice to install a SynchronizationContext on that thread. You can fetch a thread's SynchronizationContext using SynchronizationContext.Current, store it away, and then post messages to that thread's message queue using that SynchronizationContext. If a thread doesn't have a SynchronizationContext installed on it, SynchronizationContext.Current returns null.

public event EventHandler StatusChanged;
private Timer timer;
private SynchronizationContext synchronizationContext;

public void Start()
{
    synchronizationContext = SynchronizationContext.Current;
    StatusChanged += new EventHandler(SomethingChangedMethod);
    timer = new Timer(Pending, null, 0, 1000);
}

private void Pending(object state)
{
    // Do Something
    if (synchronizationContext != null)
    {
        synchronizationContext.Post(_ => StatusChanged?.Invoke(this, EventArgs.Empty), null);
    } 
    else
    {
        StatusChanged?.Invoke(this, EventArgs.Empty);
    }
}
canton7
  • 37,633
  • 3
  • 64
  • 77
  • Thanks I will try out. Does this also mean this is generally not possible if the thread has no message queue – rkgghz Oct 18 '21 at 10:10
  • 1
    Correct. If a thread is not checking a message queue, there's no way to tell it to do something. Not least, because if it's not checking a message queue it's by definition busy doing something else, and there's no way to just jump into a thread and force it to switch to doing something else – canton7 Oct 18 '21 at 10:12