很多winform开发的新人,在子线程(非UI线程,线程id不为1)中更新控件的text属性时经常会遇到一个不允许跨线程访问控件的异常:Cross-thread operation not valid. Control “” accessed from a thread other than the thread it was created on

那么在这篇文章里就总结下如何该正确的跨进程访问控件:

如果你想从子线程访问Control(Form也属于一种Control),则必须调用Control的这四个方法InvokeBeginInvokeEndInovkeCreateGraphics,将操作封送到正确的线程上。

但是调这四个方法的前提是该控件或窗体的句柄(Handle)必须已经被创建,否则就会抛出异常。

那这个句柄啥时候创建呢?
msdn解释是当这个Control第一次被展示的时候,或者Control.Handle属性被调用的时候。

所以开启子线程之前,如果你没法确认句柄是否被创建,就先调用一次Control.Handle属性,或者在窗体的Load事件里再启动你的线程,此时CLR会保证句柄一定被创建

一般来说代码就可以这么写:

    private void WriteTextSafe(string text)
    {
        if (textBox1.InvokeRequired)
        {
            var d = new SafeCallDelegate(WriteTextSafe);
            textBox1.Invoke(d, new object[] { text });
        }
        else
        {
            textBox1.Text = text;//严格来说有风险
        }
    }

    private void SetText()
    {
        WriteTextSafe("This text was set safely.");
    }

InvokeRequired用来判断是否需要进行封送。如果当前线程是主线程或者textBox1是在非主线程上创建的或找不到它的句柄,那InvokeRequired返回false。如果当前线程是子线程且能找到textBox1的句柄,那就返回true。
所以上述代码的textBox1.Text=text严格来说是有风险的,在textBox1的句柄没被创建的情况下直接设置Text属性会抛出异常。

所以可以做如下优化:

    private void WriteTextSafe(string text)
    {
        if (textBox1.InvokeRequired)
        {
            var d = new SafeCallDelegate(WriteTextSafe);
            textBox1.Invoke(d, new object[] { text });
        }
        else
        {
        	if(textBox1.IsHandleCreated)
        	{
        		textBox1.Text = text;
        	}
        	else
        	{
        		//大多数情况下,都不应该走到这里。如果真的走到了,正如前面所说 请在线程开始之前先调用下textBox1.Handle属性,手动创建句柄。
        	}
        }
    }

如果每次都这么规规矩矩的写,是不是很麻烦?

我一般做法是等Form显示出来之后才开启子线程,然后调用Form的Invoke方法而不是具体某个Control的Invoke。在这种情况下我可以确保,当前操作不是在主线程而且Form的句柄也一定被创建了。就直接简写为:

private void WriteTextSafe(string text)
{
	this.Invoke(new Action(() =>
	{
    	textEdit1.Text = text;
	}));
}

其中this是当前Form的实例。

仔细阅读以下内容,可以加深你的理解:

Controls in Windows Forms are bound to a specific thread and are not thread safe. Therefore, if you are calling a control’s method from a different thread, you must use one of the control’s invoke methods to marshal the call to the proper thread. This property can be used to determine if you must call an invoke method, which can be useful if you do not know what thread owns a control.
In addition to the InvokeRequired property, there are four methods on a control that are thread safe to call: Invoke,BeginInvoke, EndInvoke and CreateGraphics if the handle for the control has already been created. Calling CreateGraphics before the control’s handle has been created on a background thread can cause illegal cross thread calls. For all other method calls, you should use one of these invoke methods when calling from a different thread.
If the control’s handle does not yet exist, InvokeRequired searches up the control’s parent chain until it finds a control or form that does have a window handle. If no appropriate handle can be found, the InvokeRequired method returns false.
This means that InvokeRequired can return false if Invoke is not required (the call occurs on the same thread), or if the control was created on a different thread but the control’s handle has not yet been created.
In the case where the control’s handle has not yet been created, you should not simply call properties, methods, or events on the control. This might cause the control’s handle to be created on the background thread, isolating the control on a thread without a message pump and making the application unstable.
You can protect against this case by also checking the value of IsHandleCreated when InvokeRequired returns false on a background thread. If the control handle has not yet been created, you must wait until it has been created before calling Invoke or BeginInvoke. Typically, this happens only if a background thread is created in the constructor of the primary form for the application (as in Application.Run(new MainForm()), before the form has been shown or Application.Run has been called.
One solution is to wait until the form’s handle has been created before starting the background thread. Either force handle creation by calling the Handle property, or wait until the Load event to start the background process.
An even better solution is to use the SynchronizationContext returned by SynchronizationContext rather than a control for cross-thread marshaling.

如果想有更深一步的了解可以参考:

Control.InvokeRequired Property
How to: Make thread-safe calls to Windows Forms controls
Control.Handle Property