.NET: How do I invoke a delegate on a specific thread? (ISynchronizeInvoke, Dispatcher, AsyncOperation, SynchronizationContext, etc.)

試著忘記壹切 提交于 2019-12-02 20:49:32

Sorry for posting such a long answer. But I thought it worth explaining what exactly is going on.

A-ha! I think I've got it figured out. The most generic way of invoking a delegate on a specific thread indeed seems to be the SynchronizationContext class.

First, the .NET framework does not provide a default means to simply "send" a delegate to any thread such that it'll get executed there immediately. Obviously, this cannot work, because it would mean "interrupting" whatever work that thread would be doing at the time. Therefore, the target thread itself decides how, and when, it will "receive" delegates; that is, this functionality has to be provided by the programmer.

So a target thread needs some way of "receiving" delegates. This can be done in many different ways. One easy mechanism is for the thread to always return to some loop (let's call it the "message loop") where it will look at a queue. It'll work off whatever is in the queue. Windows natively works like this when it comes to UI-related stuff.

In the following, I'll demonstrate how to implement a message queue and a SynchronizationContext for it, as well as a thread with a message loop. Finally, I'll demonstrate how to invoke a delegate on that thread.


Example:

Step 1. Let's first create a SynchronizationContext class that'll be used together with the target thread's message queue:

class QueueSyncContext : SynchronizationContext
{
    private readonly ConcurrentQueue<SendOrPostCallback> queue;

    public QueueSyncContext(ConcurrentQueue<SendOrPostCallback> queue)
    {
        this.queue = queue;
    }

    public override void Post(SendOrPostCallback d, object state)
    {
        queue.Enqueue(d);
    }

    // implementation for Send() omitted in this example for simplicity's sake.
}

Basically, this doesn't do more than adding all delegates that are passed in via Post to a user-provided queue. (Post is the method for asynchronous invocations. Send would be for synchronous invocations. I'm omitting the latter for now.)

Step 2. Let's now write the code for a thread Z that waits for delegates d to arrive:

SynchronizationContext syncContextForThreadZ = null;

void MainMethodOfThreadZ()
{
    // this will be used as the thread's message queue:
    var queue = new ConcurrentQueue<PostOrCallDelegate>();

    // set up a synchronization context for our message processing:
    syncContextForThreadZ = new QueueSyncContext(queue);
    SynchronizationContext.SetSynchronizationContext(syncContextForThreadZ);

    // here's the message loop (not efficient, this is for demo purposes only:)
    while (true)
    {
        PostOrCallDelegate d = null;
        if (queue.TryDequeue(out d))
        {
            d.Invoke(null);
        }
    }
}

Step 3. Thread Z needs to be started somewhere:

new Thread(new ThreadStart(MainMethodOfThreadZ)).Start();

Step 4. Finally, back on some other thread A, we want to send a delegate to thread Z:

void SomeMethodOnThreadA()
{
    // thread Z must be up and running before we can send delegates to it:
    while (syncContextForThreadZ == null) ;

    syncContextForThreadZ.Post(_ =>
        {
            Console.WriteLine("This will run on thread Z!");
        },
        null);
}

The nice thing about this is that SynchronizationContext works, no matter whether you're in a Windows Forms application, in a WPF application, or in a multi-threaded console application of your own devising. Both Winforms and WPF provide and install suitable SynchronizationContexts for their main/UI thread.

The general procedure for invoking a delegate on a specific thread is the following:

  • You must capture the target thread's (Z's) SynchronizationContext, so that you can Send (synchronously) or Post (asynchronously) a delegate to that thread. The way how to do this is to store the synchronization context returned by SynchronizationContext.Current while you're on the target thread Z. (This synchronization context must have previously been registered on/by thread Z.) Then store that reference somewhere where it's accessible by thread A.

  • While on thread A, you can use the captured synchronization context to send or post any delegate to thread Z: zSyncContext.Post(_ => { ... }, null);

If you want to support calling a delegate on a thread which doesn't otherwise have a message loop, you have to implement your own, basically.

There's nothing particularly magic about a message loop: it's just like a consumer in a normal producer/consumer pattern. It keeps a queue of things to do (typically events to react to), and it goes through the queue acting accordingly. When there's nothing left to do, it waits until something is placed in the queue.

To put it another way: you can think of a thread with a message loop as a single-thread thread pool.

You can implement this yourself easily enough, including in a console app. Just remember that if the thread is looping round the work queue, it can't be doing something else as well - whereas typically the main thread of execution in a console app is meant to perform a sequence of tasks and then finish.

If you're using .NET 4, it's very easy to implement a producer/consumer queue using the BlockingCollection class.

I recently came across this article and found it to be a lifesaver. The use of a blocking, concurrent queue is the secret sauce, as pointed out by Jon Skeet above. The best 'how-to' that I found on making all of this work is this article on CodeProject by Mike Peretz. The article is part of a three-part series on the SynchronizationContext that provides code examples that can be easily be turned into production code. Note only does Peretz fill in all the details, but he also reminds us that the base SynchronizationContext has essentially worthless implementations of Post() and Send() and thus really should be viewed as an abstract base class. A casual user of the base class could be surprised to find that it does not solve the real-world problems.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!