C# winform 窗体控件跨线程访问

        在写计算机网络课设的时候,遇到一个需求:建立的服务器需要一直监听来自某个端口的连接请求。一开始看到我想这不是很简单吗,正好刚刚学习了线程有关的知识,直接开个新线程挂在后台执行不就行了,抱着这样简单的想法,一堆错误接踵而至。

        首先出现的问题就是窗体控件的跨线程访问,会出现这样的一个报错信息:System.InvalidOperationException:“线程间操作无效: 从不是创建控件“textBox1”的线程访问它。”

        去找了找解决方案,其中最简单的是这样的一行代码:

Control.CheckForIllegalCrossThreadCalls = false;

        这行代码只需要加到需要进行控制的控件那里,就会让程序忽略线程的危险操作。

        这样确实就解决了问题,但是这是一个全局的静态变量,如果在其他地方不小心修改了这一变量,每个控件就又会崩掉。所以,正统的解决方法应该是使用委托。

        什么是委托?C#的委托类似于C++的函数指针,是一个可以批量执行函数的容器。我们知道这个问题是发生在跨线程访问中,那么我们如果让函数调用发生在创建这个控件的线程中,那么就不是跨线程访问,也就不存在这个问题了,因此我们使用this.invoke(delegrate)来使用委托。

        来解释这种解决方法,我们设想一个简单的情景:我们需要在一个文本框中实时的更新当前时间,我们希望同时还可以在窗体中进行别的操作。

        第一时间想到:

private void Form1_Load(object sender, EventArgs e)
{
    while(true) 
    { 
        string time = DateTime.Now.ToString();
        textBox1.Text = time;
        Thread.Sleep(1000);
    }
}

        这个显然是错误的,甚至连窗口都跳不出来,更别提还可以在窗口中执行其他操作。因为这个while(true)循环堵死了这个窗体的加载,根本加载不出来,会一直停留在加载这个环节。

        然后就应该想到使用新开线程来解决这个问题了:

private void Form1_Load(object sender, EventArgs e)
{
    // Control.CheckForIllegalCrossThreadCalls = false; 
    // 如果加上上面这行代码,就可以运行了,但是只是忽略了线程安全性
    Thread timeShower = new Thread(ShowTime);
    timeShower.IsBackground = true;
    timeShower.Start();

}
private void ShowTime()
{
    while(true)
    {
        string time = DateTime.Now.ToString();
        textBox1.Text = time;      
    }
}

        很可惜,线程的第一次使用报错了!跨线程访问了textbox控件,这不安全。

        接着,我们想使用委托的相关知识来解决这个问题:

private void Form1_Load(object sender, EventArgs e)
{
    // 这个线程传递的是我们使用委托的那个函数
    Thread timeShower = new Thread(Textbox1ShowTime);
    timeShower.IsBackground = true;
    timeShower.Start();

}
private delegate void TextBox1Delegrate(); // 声明了一个无参无返回的委托
private void Textbox1ShowTime()
{
    // 实例化委托,传递实际需要调用的ShowTime函数
    TextBox1Delegrate newDelegrate = new TextBox1Delegrate(ShowTime);
    while (true) 
    {
        this.Invoke(newDelegrate); // 这里的this代指Form1
        Thread.Sleep(1000);
    }
}
private void ShowTime()
{
    string time = DateTime.Now.ToString();
    textBox1.Text = time;
}

        好的,我们已经基本上完成了我们设想的情景。但是,我们能否一步到位,将while循环和Thread.Sleep(1000) 放到ShowTime函数中,然后直接使用委托启动ShowTime,就可以直接得到实时刷新的时间呢?

        如果你和我一样聪明,估计会这么想:

private void Form1_Load(object sender, EventArgs e)
{
    Thread timeShower = new Thread(Textbox1ShowTime);
    timeShower.IsBackground = true;
    timeShower.Start();

}
private delegate void TextBox1Delegrate();
private void Textbox1ShowTime()
{
    TextBox1Delegrate newDelegrate = new TextBox1Delegrate(ShowTime);
    this.Invoke(newDelegrate);
}
private void ShowTime()
{
    while (true)
    {
        string time = DateTime.Now.ToString();
        textBox1.Text = time;
        Thread.Sleep(1000);
    }
}

        好的,恭喜你,获得了一个无响应卡死的窗口!这是为什么呢?首先我们要知道this.Invoke(function)的原理是将这个function发送到创建this的这个线程,在这里也就是主线程中执行,因此来规避跨线程问题。但是这里,我们传了一个永真循环进去,其实和我们一开始想到的那个代码没有本质上的区别,能显现出窗口只是因为我们使用了委托(那还是进步了,恭喜我们)。

        那么应该怎么样才能实现一步到位的解决这个实时显示时间的问题呢?

        好,其实我也实现不了一步到位,如果有大神会的,一定要在评论区教教我QAQ

        但是可以实现一个两重外包,然后实现直接调用委托实时显示时间。究其根本,只要不把永真的while循环放到主线程中执行,就可以了,所以这两重外包就是把while单独分离了(其实上面那种实现方法也是同样的思路,分离了永真while这个讨厌的东西)

private void Form1_Load(object sender, EventArgs e)
{
    Thread timeShower = new Thread(Textbox1ShowTime);
    timeShower.IsBackground = true;
    timeShower.Start();

}
private delegate void TextBox1Delegrate();
private void Textbox1ShowTime()
{
    TextBox1Delegrate newDelegrate = new TextBox1Delegrate(ShowTime);
    newDelegrate.Invoke();
}
private void ShowTime()
{
    while (true)
    {
        TextBox1Delegrate newDelegrate = new TextBox1Delegrate(ShowTime1);
        this.Invoke(newDelegrate);
        Thread.Sleep(1000);
    }
}
private void ShowTime1()
{
    string time = DateTime.Now.ToString();
    textBox1.Text = time;
}

       同样的,顺着分离这个思路走下去其实还可以进一步精简我们的代码。

        这边要提到一个属性 textBox1.InvokeRequired。这个属性是bool类型,他表示获取一个值,该值指示调用方在对控件进行方法调用时是否因为调用的方法于不在创建控件的线程中,是否必须调用 Invoke 方法。

        对于有许多控件和线程需要进行操作的情况而言,这个属性可以非常方便的确定是否需要使用Invoke()方法进行委托的调用

       对于我们这个从子线程Textbox1ShowTime()进去的ShowTime而言,这个属性永远是真的;而对于我们使用this.Invoke(newDelegrate)使用的ShowTime而言,它相当于是在主线程中执行这个函数,从而实现了状态的分离。因此我们可以通过if(textBox1.InvokeRequired)实现将ShowTime1放到ShowTime中实现

private void Form1_Load(object sender, EventArgs e)
{
    Thread timeShower = new Thread(Textbox1ShowTime);
    timeShower.IsBackground = true;
    timeShower.Start();

}
private delegate void TextBox1Delegrate();
private void Textbox1ShowTime()
{
    TextBox1Delegrate newDelegrate = new TextBox1Delegrate(ShowTime);
    newDelegrate.Invoke();
}
private void ShowTime()
{
    if (textBox1.InvokeRequired) // 从new Thread(Textbox1ShowTime)调用的委托一定会进这里
    {
        while (true)
        {
            TextBox1Delegrate newDelegrate = new TextBox1Delegrate(ShowTime);
            this.Invoke(newDelegrate); // 这个相当于是在主线程调用委托
            Thread.Sleep(1000); // 注意这个sleep不能放到else中,不然会无限的休眠
        }
    }
    else // 从主线程调用的委托一定会进到这里
    {
        string time = DateTime.Now.ToString();
        textBox1.Text = time;
    }

}

        OK,针对这个跨线程访问的例子暂时我只能想这么多了,有错误的话大佬们不灵赐教啊QAQ