netwjx

混乱与有序

WinForms开发中SynchronizationContext和Invoke的使用注意事项

| 评论

WinForms 开发中Control.Invoke是用于非UI线程中请求修改UI元素的方法, 一般配合Control.InvokeRequired使用:

Control.Invoke and Control.InvokeRequired
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public partial class Form1 : Form
{
    private void Foo(string text)
    {
        if (InvokeRequired)
        {
            Invoke((Action<string>)Foo, text);
        }
        else
        {
            textBox1.Text = text;
        }
    }
}

类似Control.Invoke的还有Control.BeginInvokeControl.EndInvoke, 它们是异步调用.

这些方法和属性都依赖于IsHandleCreatedtrue时, IsHandleCreated表示窗口句柄是否已创建, 它并不是指是否new Form1()过, 而是指是否Show()过, 包括Application.Run, Show, ShowDialog这些调用都会使IsHandleCreatedtrue.

而在IsHandleCreatedfalse时, 比如刚刚new Form1(), Control.InvokeRequired返回false, 调用Control.Invoke会抛出异常:

System.InvalidOperationException: 在创建窗口句柄之前,不能在控件上调用 Invoke 或 BeginInvoke。

当在非UI线程和多个窗口之间操作时, 可能会有一些麻烦的情况发生, 这种情况可能会考虑使用SynchronizationContext.

SynchronizationContext可以在当前线程第一次new Form1()之后通过SynchronizationContext.Current取得, 之后使用PostSend实现在UI线程执行指定的委托, 下面使用的WindowsFormsSynchronizationContext.Current在WinForms程序中等价于SynchronizationContext.Current:

SynchronizationContext.Post
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public partial class Form1 : Form
{
    public static SynchronizationContext SyncContext { get; set; }

    public Form1()
    {
        InitializeComponent();
        SyncContext = WindowsFormsSynchronizationContext.Current;
    }

    private void Foo(string text)
    {
      SyncContext.Post(delegate(object obj)
      {
          textBox1.Text = text;
      }, null);
    }
}

SynchronizationContext.Current的文档可知它只会返回当前线程的同步上下文, 要在别的线程中访问需要自行保存它的引用, 即这里属性SyncContext, 使用时确保在访问SyncContext之前new Form1()过一次, 且只能一次, 否则后续的会覆盖之前的, 在符合需求的情况下会很自然想到单例模式:

线程安全的单例模式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static Form1[] _Instance = { null };

public static Form1 Instance
{
    get
    {
        if (_Instance[0] == null)
        {
            lock (_Instance)
            {
                if (_Instance[0] == null)
                {
                    _Instance[0] = new Form1();
                }
            }
        }
        return _Instance[0];
    }
}

目前看起来是没什么问题了, 现实总是会出点问题, 比如SynchronizationContext.Current总是返回当前线程的, 结合上述的单例模式, 如果第一次访问Instance属性是在别的线程中, 测试代码如下:

在不同的线程中访问Form1.Instance
1
2
3
4
5
6
7
8
9
10
11
new Thread(delegate()
{
    Form1.Instance.ToString();
    Debug.Assert(SynchronizationContext.Current != null);
}).Start();

Thread.Sleep(3000);

var f = Form1.Instance;
Debug.Assert(SynchronizationContext.Current == null);
Application.Run(f);

上面代码的两处断言都通过了, 这种情况下Form1.SyncContext.Post仍旧可以调用, 但是将不产生任何效果, 也不抛出异常, 因为new Form1()的那个线程已经结束了, 以及那个线程并没有执行消息循环Application.Run.

如果需要在Application.Run之后, 相关的UI元素变得可用时再执行相关代码, 可以自行定义事件, 实现相关的触发和绑定, 确保new Form1Application.Run在同一个线程中调用, 在具体的多线程环境中解决办法会表现的完全不同.

评论

Fork me on GitHub