问题
I'm moving some code from a winforms control object to a separate object for better modularity. However, there some calls to an external object issuing callbacks, which I have no control of and which can be fired from different threads as the main UI thread. To avoid this I use the well known BeginInvoke scheme to check, whether a call should be transfered to the main UI thread.
When I now move this code to my separated object, I have not necessary a Winforms reference anymore. I could handle over a Control object to still ensure that everything is running in the same thread. But I would rather like to have a generic mechanism which does exactly the same like ensuring, that the Threadconext in which the e.g. the object was created or a specific entry function was called is also used for subsequent calls issued e.g. by external callbacks.
How could this achieved most easily ?
Example:
public class Example
{
ThreadedComponent _Cmp = new ThreadedComponent();
public Example()
{
_Cmp.ThreadedCallback += new ThreadedComponent.CB(Callback);
}
public void StartFunction()
{
// called in ThreadContextA
_Cmp.Start();
}
void Callback(Status s)
{
// is called in ThreadContextB
if(s == SomeStatus)
_Cmp.ContinueFunction(); // must be called in ThreadContextA
}
}
For clarification
ContinueFunction
must be called from the same ThreadContext like StartFunction
was called. This is not necessarily a UI thread, but at the moment it is of course a button handler.
回答1:
There is no 'generic' scheme, your class cannot make a lot of assumptions about what thread it is used on and what object can provide the BeginInvoke() method you need. Choose from one of the following options:
Do not help at all, simply document that the event can be raised on a worker thread. Whatever code exists in the GUI layer can of course always figure out how to use BeginInvoke() when needed.
Allow the client code to pass a Control object through your class constructor. You can store it and call its BeginInvoke() method. That works, it isn't terribly pretty because your class now is only usable in a Winforms project.
Expose a property called "SynchronizingObject" of type ISynchronizeInvoke. The GUI layer now has the option to ask you to call ISynchronizeInvoke.BeginInvoke(). Which you do if the property was set, just fire the event directly otherwise. Several .NET Framework classes do this, like Process, FileSystemWatcher, EventLog, etc. It however has the same problem as the previous solution, the interface isn't readily available in a non-Winforms application.
Demand that the client code creates your object on the UI thread. And copy SynchronizationContext.Current in your constructor. You can, later, use its Post() method to invoke. This is the most compatible option, all GUI class libraries in .NET provide a value for this property.
Do keep the trouble in mind when you choose one of the latter bullets. The client code will get the event completely unsynchronized from your thread's code execution. A concrete event handler is somewhat likely to want to access properties on your class to find out more about the state of your class. That state is unlikely to still be valid since your thread has progressed well past the BeginInvoke() call. The client code has no option at all to insert a lock to prevent that from causing trouble. You should strongly consider to not help at all if that's a real issue, it often is.
回答2:
In C# you cannot assign a thread context to an object, like in Qt for example (C++).
A thread is running in itself, it does not "collect" objects or methods to call them if they were marked somehow.
However synchronizing to a GUI thread in C# is very easy. Instead of the BeginInvoke/Invoke
pattern, you can create a System.Windows.Forms.Timer
instance, which can call the methods on the non-WinForms objects.
Example:
public interface IMyExternalTask
{
void DoSomething();
}
// ...
List<IMyExternalTask> myTasks = new List<IMyExternalTask>();
System.Windows.Forms.Timer t = new System.Windows.Forms.Timer();
t.Interval = 1000; // Call it every second
t.Tick += delegate(object sender, EventArgs e) {
foreach (var myTask in myTasks)
myTask.DoSomething();
};
t.Start();
In the example your "external" objects must implement the interface, and they can do their tasks from the DoSomething()
method, which will be synchronized to the GUI thread.
These external objects don't have to have any reference to any Windows.Forms
object.
回答3:
I solve the problem using a separate queue which runs its own thread. Function Calls are added to the Queue with a Proxyinterface. It's probably not the most elegant way, but it ensures, that everything added to the queue is executed in the queue's threadcontext. This is a very primitive implementation example just to show the basic idea:
public class Example
{
ThreadQueue _QA = new ThreadQueue();
ThreadedComponent _Cmp = new ThreadedComponent();
public Example()
{
_Cmp.ThreadedCallback += new ThreadedComponent.CB(Callback);
_QA.Start();
}
public void StartFunction()
{
_QA.Enqueue(AT.Start, _Cmp);
}
void Callback(Status s)
{
// is called in ThreadContextB
if(s == SomeStatus)
_QA.Enqueue(new ThreadCompAction(AT.Continue, _Cmp);
}
}
public class ThreadQueue
{
public Queue<IThreadAction> _qActions = new Queue<IThreadAction>();
public Enqueue(IThreadAction a)
{
lock(_qActions)
_qActions.Enqueue(a);
}
public void Start()
{
_thWatchLoop = new Thread(new ThreadStart(ThreadWatchLoop));
_thWatchLoop.Start();
}
void ThreadWatchLoop()
{
// ThreadContext C
while(!bExitLoop)
{
lock (_qActions)
{
while(_qActions.Count > 0)
{
IThreadAction a = _qActions.Dequeue();
a.Execute();
}
}
}
}
}
public class ThreadCmpAction : IThreadAction
{
ThreadedComponent _Inst;
ActionType _AT;
ThreadCmpAction(ActionType AT, ThreadedComponent _Inst)
{
_Inst = Inst;
_AT = AT;
}
void Do()
{
switch(AT)
{
case AT.Start:
_Inst.Start();
case AT.Continue:
_Inst.ContinueFunction;
}
}
}
来源:https://stackoverflow.com/questions/24382735/generic-begininvoke-scheme-to-ensure-function-calls-in-same-threading-context