【问题标题】:C# Cross-Thread communicationC# 跨线程通信
【发布时间】:2012-06-30 11:04:58
【问题描述】:

在 C#.NET 中,我编写了以下简单的后台工作线程:

public class MyBackgrounder
{
    public delegate void dlgAlert();
    public dlgAlert Alert;
    public event EventHandler eventAlert;
    Thread trd;

    public void Start()
    {
        if (trd == null || trd.ThreadState == ThreadState.Aborted)
        {
            trd = new Thread(new ThreadStart(Do));
        }
        trd.IsBackground = true;
        trd.Priority = ThreadPriority.BelowNormal;
        trd.Start();
    }

    void Do()
    {

        Thread.Sleep(3000);
        Done();
    }

    void Done()
    {
        if (Alert != null)
            Alert();
        if (eventAlert != null)
            eventAlert(this, new EventArgs());
        Kill();
    }

    public void Kill()
    {
        if (trd != null)
            trd.Abort();
        trd = null;
    }
}


static class Program
{

    [STAThread]
    static void Main()
    {
        MyBackgrounder bg = new MyBackgrounder();
        bg.eventAlert += new EventHandler(bg_eventAlert);
        bg.Alert = jobDone;
        bg.Start();
    }

    static void bg_eventAlert(object sender, EventArgs e)
    {
        // here, current thread's id has been changed
    }

    static void jobDone()
    { 
        // here, current thread's id has been changed
    }

}

它只等待 3 秒(完成它的工作),然后引发指定事件或调用委托。直到这里没有问题,一切正常。但是当我观看 'Thread.CurrentThread.ManagedThreadId' 时,我发现它是后台线程!也许这很正常,但我怎样才能防止这种行为?如果您测试 'System.Windows.Forms.Timer' 组件并处理其 'Tick' 事件,您可以看到 'Thread.CurrentThread.ManagedThreadId ' 没有从主线程 ID 更改为其他任何内容。

我能做什么?

【问题讨论】:

  • 您的代码没有问题,您只是想了解,对吧?后台工作者 is 在另一个线程中。你在哪里看线程ID?
  • System.Windows.Forms.Timer 使用内部 Windows WM_TIMER 事件并简单地计算滴答数,然后引发 .NET 事件。因此它可以在进程的主线程中工作。您编写的后台工作人员在后台线程中工作(显式通过创建 new Thread()),否则 Thread.Sleep(3000) 会暂停您的所有应用程序。这个在后台线程运行到底有什么问题?
  • @MareInfinitus ,当后台工作对象引发其事件或调用指定的方法时,我看到更改的线程 ID。这意味着我的后台工作人员调用了这些方法。
  • 没错,被调用的方法不在后台工作线程中,而是在提供这些回调的线程中。
  • @KubaWyrostek,谢谢。我想解决(更改)后台工作人员的行为,所以以后交互 UI 元素时我会很安全。看来我必须使用本机 Win API 编写后台程序,就像 .NET 的计时器一样,不是吗?但是它如何在不使用线程的独立类中工作!?

标签: c# .net multithreading delegates


【解决方案1】:

如果您使用的是 Windows 窗体,您可以执行以下操作:

  1. 在您的表单中添加属性

    private readonly System.Threading.SynchronizationContext context;
    public System.Threading.SynchronizationContext Context
    {
        get{ return this.context;}
    }
    
  2. 在您的表单构造函数中设置属性

    this.context= WindowsFormsSynchronizationContext.Current;
    
  3. 使用此属性将其作为构造函数参数传递给您的后台工作人员。这样,您的工作人员将了解您的 GUI 上下文。在您的后台工作人员中创建类似的属性。

    private readonly System.Threading.SynchronizationContext context;
    public System.Threading.SynchronizationContext Context
    {
        get{ return this.context;}
    }
    
    public MyWorker(SynchronizationContext context)
    {
        this.context = context;
    }
    
  4. 更改您的Done() 方法:

    void Done()
    {
        this.Context.Post(new SendOrPostCallback(DoneSynchronized), null);
    }
    
    void DoneSynchronized(object state)
    {
        //place here code You now have in Done method.
    }
    
  5. 在 DoneSynchronized 中您应该始终在您的 GUI 线程中。

【讨论】:

  • 不错的解决方案。请注意,BackgroundWorker 的 Context 属性应该是只读的或者它的 setter 是同步的。
  • @KubaWyrostek 没错!我更改了属性。
  • @GrzegorzWilczura,谢谢你的帮助,但如果我决定在我的底层引用“System.Windows.Forms”,你不认为最好使用“定时器”组件而不是“WindowsFormsSynchronizationContext”?我已经对其进行了测试,它工作正常,因为最常运行的应用程序是 Windows 窗体。我认为它更容易使用。
  • @GrzegorzWilczura 请看看我自己的答案。非常感谢你帮助我的朋友。
  • @losingsleeep SynchronizationContext 在 mscorlib.dll 中,所以我认为如果以这种方式实现,您不需要在后台工作程序项目中引用 System.Windows.Forms.dll。
【解决方案2】:

定时器在主线程中运行(如果主线程真的很忙,它会延迟关闭)。如果出于某种原因您希望从主线程调用 jobDone(),例如,您可以使用 BackGroundWorker 对象,该对象在内部线程化并具有一些特定事件,这些事件被称为主线程线程安全(允许 UI 更新等)

【讨论】:

    【解决方案3】:

    究竟是什么让您对后台线程感到厌烦?到目前为止,您所描述的对我来说看起来还不错。

    您检查比较的System.Windows.Forms.Timer 可能在UI 线程 上运行,这确实会使其成为非后台线程。

    来自 Thread.IsBackground 属性的 MSDN 文档(我的亮点):

    线程要么是后台线程,要么是前台线程。 后台线程与前台线程相同,除了 后台线程不会阻止进程终止。一旦所有 属于一个进程的前台线程已经终止,常见的 语言运行时结束该过程。任何剩余的后台线程 已停止且未完成。

    【讨论】:

    • 任何剩余的线程都被停止并且没有完成 - 我猜这些都会抛出 ThreadAbortException
    【解决方案4】:

    这个设计怎么样:

    1. 在您的表单中创建一个 EventWaitHandle 对象。

      private EventWaitHandle layer2changed =
          new EventWaitHandle(false, EventResetMode.ManualReset);
      
    2. 在 Timer 事件处理程序中执行以下操作:

      if (this.layer2changed.WaitOne(0, false))
      {
          // perform UI updates according to some public properties of layer1/layer2
          this.layer2changed.Reset();
      }
      
    3. 在它们的构造函数中传递对 layer2 的引用更改为 layer1/layer2 等。

    4. 每次您的 Done() 方法执行需要更新 UI 的操作时都会发出一个事件信号:

       this.referencedLayer2Changed.Set();
      

    但请注意,layer2 和 UI 的变化会有时间差异(Form.Timer 间隔越长 - 差异越长)。

    请注意:这是一个单独的解决方案,它来自于 cmets 中的 losssleep 对他的问题指定的附加要求。请在投票之前阅读它们。 :-)

    【讨论】:

      【解决方案5】:

      我认为还有另一种解决方案不需要在底层(layer2)中引用“System.Windows.Forms”:
      layer1(UI 表单)提供 layer2 可以使用的超时实现。

      有一个接口:

      namespace Layer2
      {
          public delegate void TimeoutAlert();
      
          public interface IApplicationContext
          {
              void SetTimeout(TimeoutAlert timeoutAlerthandler, int milliseconds);
          }
      }
      

      和Layer2中的工人阶级:

      namespace Layer2
      {
          public class Worker
          {
              IApplicationContext _context;
              public void DoJob(IApplicationContext context)
              {
                  _context = context;
                  _context.SetTimeout(JobTimedOut,3000);
                  // nothing ... (wait for other devices or user events)
              }
      
              void JobTimedOut()
              {
                  // do something suitable for timeout error with _context or anything else
              }
      
          }
      }  
      

      在 UI 中,我们有这个:

      using Layer2;
      
      namespace Layer1
      {
          public partial class frmTest : Form, IApplicationContext
          {
      
              int timeout;
              TimeoutAlert _timeoutAlerthandler;
      
              public frmTest()
              {
                  InitializeComponent();
              }
      
              private void frmTest_Load(object sender, EventArgs e)
              {
                  Worker w = new Worker();
                  w.DoJob(this);
              }
      
              private void timer1_Tick(object sender, EventArgs e)
              {
                  // it's the handler of 'Timer' component named 'timer1'
                  timer1.Enabled = false;
      
                  if (_timeoutAlerthandler != null)
                      _timeoutAlerthandler();
              }
      
              #region IApplicationContext Members
      
              void IApplicationContext.SetTimeout(TimeoutAlert timeoutAlerthandler, int milliseconds)
              {
                  _timeoutAlerthandler = timeoutAlerthandler;
                  timeout = milliseconds;
      
                  timer1.Interval = milliseconds;
                  timer1.Enabled = true;
              }
      
              #endregion
      
          }
      }  
      

      我再次注意到我的场景:
      我有一个 Windows 应用程序项目(layer1),它使用一个核心项目(layer2)来做一些工作。核心需要在其某些方法期间进行非阻塞超时检查,而无需创建不同的线程,因为超时后它需要再次与 UI 交互。对不起,如果我不能描述更多,因为它有点复杂(对我来说!)。

      【讨论】:

      • 定时器滴答正在阻塞主线程。在一个应用程序域中没有不同线程的情况下,没有像非阻塞代码执行这样的东西。
      • 是的,格热戈兹。在这个例子中,除了 Timer 组件之外,所有东西都是阻塞的,这对我来说不是问题。我只是想使用超时而不创建另一个线程。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-05-12
      相关资源
      最近更新 更多