【问题标题】:Generic BeginInvoke Scheme to ensure function calls in same threading context通用 BeginInvoke 方案以确保在同一线程上下文中调用函数
【发布时间】:2014-06-24 09:14:42
【问题描述】:

我将一些代码从一个 winforms 控件对象移动到一个单独的对象以实现更好的模块化。但是,有一些对发出回调的外部对象的调用,我无法控制这些调用,并且可以作为主 UI 线程从不同的线程中触发。为了避免这种情况,我使用众所周知的 BeginInvoke 方案来检查是否应该将调用转移到主 UI 线程。

当我现在将此代码移动到我的分离对象时,我不再需要 Winforms 引用。我可以处理一个 Control 对象,以确保一切都在同一个线程中运行。但我宁愿有一个通用机制,它的作用与确保 Threadconext 完全相同,例如创建对象或调用特定入口函数也用于随后发出的调用,例如通过外部回调。

这如何最容易实现?

例子:

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

   } 
}

澄清

ContinueFunction 必须从调用 StartFunction 的同一个 ThreadContext 调用。这不一定是 UI 线程,但目前它当然是按钮处理程序。

【问题讨论】:

  • 您的单独组件可能不应该引用 UI。所以它不应该知道必须以同步方式访问 UI 的事实。只要它知道 UI 存在,它就不会真正分离。
  • 在你的外部组件和 UI 之间放置一些东西。委托、事件或接口。然后这个中间人可以在外部组件不知道的情况下与 UI 同步。
  • @usr:正如我在第一个答案下方的评论中所写,我想我正在使用自己的线程事件队列,然后在定义的上下文中执行任务。
  • 您不需要任何消息队列。你只需要在你的组件和Control.Invoke 之间有一个很小的层,这样你的组件甚至不需要知道有控件这样的东西。发布一些代码,我可以看看这种模式是否适用于您的情况。
  • 将UI同步逻辑添加到CallbackThreadedComponent 不需要知道。

标签: c# .net multithreading thread-safety


【解决方案1】:

没有“通用”方案,您的类不能对它在哪个线程上使用以及哪个对象可以提供您需要的 BeginInvoke() 方法做出很多假设。从以下选项之一中选择:

  • 根本没有帮助,只需记录可以在工作线程上引发的事件。 GUI 层中存在的任何代码当然总能在需要时弄清楚如何使用 BeginInvoke()。

  • 允许客户端代码通过您的类构造函数传递一个 Control 对象。您可以存储它并调用它的 BeginInvoke() 方法。这行得通,它不是很漂亮,因为你的类现在只能在 Winforms 项目中使用。

  • 公开一个名为“SynchronizingObject”的 ISynchronizeInvoke 类型的属性。 GUI 层现在有 选项 来要求您调用 ISynchronizeInvoke.BeginInvoke()。如果设置了属性,你会做什么,否则直接触发事件。有几个 .NET Framework 类可以做到这一点,例如 Process、FileSystemWatcher、EventLog 等。但是它与之前的解决方案存在相同的问题,该接口在非 Winforms 应用程序中不可用。

  • 要求客户端代码在 UI 线程上创建您的对象。并在您的构造函数中复制 SynchronizationContext.Current。稍后,您可以使用它的 Post() 方法来调用。这是最兼容的选项,.NET 中的所有 GUI 类库都为此属性提供了一个值。

当您选择后一种项目符号时,请记住问题所在。客户端代码将获得与线程代码执行完全不同步的事件。具体的事件处理程序可能希望访问您的类的属性以了解有关您的类状态的更多信息。该状态不太可能仍然有效,因为您的线程已经远远超过了 BeginInvoke() 调用。客户端代码根本没有选择插入锁以防止造成麻烦。如果这是一个真正的问题,你应该强烈考虑提供帮助,通常是。

【讨论】:

  • 我将坚持我提出的解决方案,将所有内容放入一个自己的线程队列中,这样我就可以完全控制正在运行的线程。稍后我会发布一个示例。
  • 请注意,对于选项 2-4,如果未提供同步对象/同步上下文,您可以默认使用线程池线程,从而允许该类在非 UI 上下文中使用,而仍会在需要时尝试使用适当的 UI 线程。
【解决方案2】:

在 C# 中,您不能将 线程上下文 分配给对象,例如在 Qt 中 (C++)。

线程本身正在运行,它不会“收集”对象或方法来调用它们,如果它们以某种方式被标记。

但是,在 C# 中与 GUI 线程同步非常容易。您可以创建一个System.Windows.Forms.Timer 实例来代替BeginInvoke/Invoke 模式,该实例可以调用非WinForms 对象上的方法。

例子:

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();

在示例中,您的“外部”对象必须实现接口,并且它们可以通过DoSomething() 方法执行任务,该方法将同步到 GUI 线程。

这些外部对象不必引用任何Windows.Forms对象。

【讨论】:

  • 我认为这不是我要寻找的:当我调用一个函数时,我可以使用 Thread.CurrentThread 来查看我正在运行的上下文。如果一个回调被触发并且我想确保它在我的起始线程上下文中,我想我构建了一个在这个上下文中运行的消息队列,我可以在其中将回调排队,然后在起始线程上下文中执行。这应该是 BeginInvoke 工作的基本方式,因为它使用 UI 消息循环 afaik。
  • 我以某种方式使用您的示例使用我想避免的 Windows 窗体来执行此操作。所以我想,方法可能是使用我自己的线程队列,它是在定义的线程上下文中执行任务。
  • 如果你想从你的消息队列同步到 GUI 线程,你也可以使用 ISynchronizeInvoke 作为属性,如果实例是由 GUI 线程创建/工作,则可以设置该属性。 Control 实现了这个接口,你的类根本不需要知道 WinForms,因为它是来自 ComponentModel 的接口
【解决方案3】:

我使用一个单独的队列来解决这个问题,该队列运行自己的线程。函数调用通过代理接口添加到队列中。这可能不是最优雅的方式,但它确保添加到队列的所有内容都在队列的线程上下文中执行。这是一个非常原始的实现示例,只是为了展示基本思想:

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;
        }
     }
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2015-07-02
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-10-30
    相关资源
    最近更新 更多