Capturing the main thread SynchronizationContext o

2019-03-26 04:17发布

I have a C# library that would like to have the ability to Send/Post work to the "main" ui thread (if one exists). This library may be used by:

  • A winforms application
  • A native application (with UI)
  • A console application (with no UI)

In the library I'd like to capture something (A SynchronizationContext, a Dispatcher, a Task Scheduler, or something else) during initialization, that will allow me to (at a later time) Send/Post work to the main thread (if the main thread has that ability--i.e. it has a message pump). For example, the library would like to put up some Winforms UI on the main thread if and only if the main application has the ability for me to get to the main thread.

Things I've tried:

  1. A SynchronizationContext: Capturing this works fine for a Winforms application (a WindowsFormsSynchronizationContext will be installed as the Current SynchronizationContext. This also works fine for the console app--since I can detect that the Current SynchronizationContext is null (and thus, know that I don't have the ability to send/post work to the main thread). The problem here is the native UI application: It has the ability (i.e. it has a message pump), but the Current Synchronization context is null and thus I can't differentiate it from the Console app case. If I could differentiate, then I could simply install a WindowsFormsSynchronizationContext on the main thread, and I'm good to go.
  2. A Dispatcher: Capturing this using Current creates a new SynchronizationContext. Thus, in all situations I will get back a Dispatcher. However, for a Console app, using Dispatcher.Invoke from a background thread will hang (as expected). I could use Dispatcher.FromThread (which doesn't create a Dispatcher for the thread if one doesn't exist). But the native UI application will return a null Dispatcher using this method, and so then I'm, again, stuck not being able to distinguish the UI application from the console application.
  3. A TaskScheduler: I could use FromCurrentSynchronizationContext. This has the same problems as the SynchronizationContext. I.e. Before calling FromCurrentSyncronizationContext, I'd have to check if the Current SynchronizationContext is null (which will be the case for the Console app and the native ui application). So, again I can't distinguish the native ui application from the console application.

I, of course, could have the user of my library specify whether or not it is a UI application when they call my Initialize method, but I was hoping to avoid that complication for the user of the library if possible.

4条回答
我只想做你的唯一
2楼-- · 2019-03-26 04:54

This is not in general possible, a library that's apt to be used in threads cannot make any assumptions about which particular thread is the UI thread. You can capture Synchronization.Current but that will only work correctly if your initialization method is called from the UI thread. That's not terribly unusual to work out well, like TaskScheduler.FromCurrentSynchronizationContext() tends to work by accident, but not a guarantee. You can add a check, if Thread.CurrentThread.GetApartmentState() doesn't return STA then the odds that you are not being called from the UI thread are very high. SynchronizationContext.Current will also often be null in that case, another way to check.

The (arguably) better ways are to just not worry about it and let the client code figure it out, it won't have any trouble marshaling the callback. Or to expose a property of type SynchronizationContext so that the client code can assign it. Or add it as a constructor argument. Throw an InvalidOperationException if you are ready to Post but find out that it is still null, that's an oversight that the client programmer only makes once.

查看更多
欢心
3楼-- · 2019-03-26 05:01

Change the library initialization to have a SyncronizationContext parameter. If the parameter is null then the library doesn't need to do anything special, if not null Post/Send GUI updates there.

查看更多
趁早两清
4楼-- · 2019-03-26 05:06

I think you should make this an option to your Initialize method (or somehow allow your caller to request UI interaction), to me that just makes more sense. I don't know the specifics but these seems to me to be a "courteous" thing to do, let your caller decide if they want you to or want to support your UI. I would take it one step further and even as the caller to supply a synchronization context. But that's my opinion.

To answer your question, there are a few "hacks" you can use to determine if you're running in a console applicaiton. This SO question has some information on that: C#/.NET: Detect whether program is being run as a service or a console application

查看更多
女痞
5楼-- · 2019-03-26 05:08

I think this is exactly what AsyncOperationManager.CreateOperation() is for.“Implementing the Event-based Asynchronous Pattern” states:

The Event-based Asynchronous Pattern provides a standardized way to package a class that has asynchronous features. If implemented with helper classes like AsyncOperationManager, your class will work correctly under any application model, including ASP.NET, Console applications, and Windows Forms applications.

It is up to the caller to decide if they want to call your API on the UI thread or not. If they do, this will capture the context and events will go through the message pump in order. In a Console application you can get the same behavior if you install a SynchronizationContext such you get for free by using AsyncContext.Run() from the Nito.AsyncEx nuget package. No need for an extra property or to have to write the conditional code yourself. If no serializing synchronization context is available, AsyncOperation.Post() will use the fake synchronization context available to Console apps which just queue the event to the threadpool instead (meaning that the posts may not execute in order). Just remember to call AsyncOperation.OperationCompleted() or AsyncOperation.PostOperationCompleted() when done.

In the library I'd like to capture something (A SynchronizationContext, a Dispatcher, a Task Scheduler, or something else) during initialization

This is exactly what AsyncOperationManager.CreateOperation() does, and in an environment-agnostic way. But you should try to pair this with a call to OperationCompleted() which maybe would be more difficult given the API you want to expose. The easiest way to use AsyncOperation would be to start an operation when your library actually starts an operation instead of during initialization. Or by having the initialization routine return an IDisposable context object handle which would signal to the consumer that they need to manage its lifetime.

查看更多
登录 后发表回答