简述
访问 Windows 窗体控件本质上不是线程安全的。如果有两个或多个线程操作某一控件的状态,则可能会迫使该控件进入一种不一致的状态。还可能出现其他与线程相关的 bug,包括争用情况和死锁。所以,确保以线程安全方式访问控件是非常重要的。
一、举个例子
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Threading;
namespace treadtest
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private Thread test_thread_run = null;
private static object dataLock = new object(); // 多线程访问链表使用的锁
private List<string> dataList = new List<string>();
bool is_test_thread_run;
private uint m_serialNumber = 0;
private void TestThreadFunc()
{
while (is_test_thread_run)
{
lock (dataLock)
{
if (0 == dataList.Count)
{
Thread.Sleep(20);
continue;
}
// 每当链表中有数据时就在listview中显示该条数据
int count = 0;
while (0 < (count = dataList.Count))
{
string param = dataList[0];
if (null != param)
{
ListViewItem item = new ListViewItem();
item.Text = (++m_serialNumber).ToString();
item.SubItems.Add(DateTime.Now.ToString());
item.SubItems.Add(param);
// 这里会报异常,因为在不是创建listview的线程中访问了它
listView.Items.Add(item);
}
dataList.Remove(param);
}
}
}
}
private void button_start_thread_Click(object sender, EventArgs e)
{
// 开启线程处理链表中的数据
is_test_thread_run = true;
test_thread_run = new Thread(new ThreadStart(TestThreadFunc));
test_thread_run.IsBackground = true;
test_thread_run.Start();
this.button_start_thread.Enabled = false;
}
private void button_pushback_data_Click(object sender, EventArgs e)
{
lock (dataLock)
{
// 链表中添加一条数据
this.dataList.Add(this.textBox_data.Text);
}
}
}
}
这里的这个程序想要开启一个线程,在这个线程里一直去读取一个链表中的数据,只要链表中有数据就把这个数据和当前的时间添加到上面的listview中。但是当访问listview控件显示数据的时候就会出现异常中断,提示你在不是创建控件listview的线程里访问了它。
二、解决办法
(一)、设置CheckForIllegalCrossThreadCalls为false
第一种方法是把CheckForIllegalCrossThreadCalls设置为false,禁止编译器对跨线程访问作检查。这样可以实现在另外的线程访问控件,但是这种方法并不安全,不能保证C#跨线程访问控件的运行时错误。
// 在初始化控件后就可以把这个属性设置为false,编译器就不会对跨线程访问做检查
public Form1()
{
InitializeComponent();
CheckForIllegalCrossThreadCalls = false;
}
(二)、使用MethodInvoker
MethodInvoker 表示一个委托,该委托可以执行托管代码中声明为void且不接受任何参数的任何方法。在对控件的 invoke 方法进行调用时或需要一个简单委托又不想自己定义时可以使用该委托。我们可以这样修改一下我们上面的代码:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Threading;
namespace treadtest
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private Thread test_thread_run = null;
private static object dataLock = new object(); // 多线程访问链表使用的锁
private List<string> dataList = new List<string>();
bool is_test_thread_run;
private static object displayDataObj = new object();
private uint m_serialNumber = 0;
private void TestThreadFunc()
{
while (is_test_thread_run)
{
lock (dataLock)
{
if (0 == dataList.Count)
{
Thread.Sleep(20);
continue;
}
// 每当链表中有数据时就在listview中显示该条数据
int count = 0;
while (0 < (count = dataList.Count))
{
string param = dataList[0];
if (null != param)
{
ShowTransComRecData(param);
}
dataList.Remove(param);
}
}
}
}
private void ShowTransComRecData(string str)
{
lock (displayDataObj)
{
// 这里使用MethodInvoker委托,在委托中访问listview控件显示数据
this.Invoke((MethodInvoker)delegate()
{
try
{
ListViewItem item = new ListViewItem();
item.Text = (++m_serialNumber).ToString();
item.SubItems.Add(DateTime.Now.ToString());
item.SubItems.Add(str);
listView.Items.Add(item);
}
catch
{ }
});
}
}
private void button_start_thread_Click(object sender, EventArgs e)
{
// 开启线程处理链表中的数据
is_test_thread_run = true;
test_thread_run = new Thread(new ThreadStart(TestThreadFunc));
test_thread_run.IsBackground = true;
test_thread_run.Start();
this.button_start_thread.Enabled = false;
}
private void button_pushback_data_Click(object sender, EventArgs e)
{
lock (dataLock)
{
// 链表中添加一条数据
this.dataList.Add(this.textBox_data.Text);
}
}
}
}
(三)、利用委托
每个控件都有一个InvokeRequired属性,当一个控件的InvokeRequired属性值为真时,说明有一个创建它以外的线程想访问它。此时它将会在内部调用new MethodInvoker(LoadGlobalImage)来完成下面的步骤,实质上和上面一种方法是一样的,这个做法保证了控件的安全。你可以这样理解,有人想找你借钱,他可以直接在你的钱包中拿,这样太不安全,因此必须让别人先要告诉你,你再从自己的钱包把钱拿出来借给别人,这样就安全了。可以这样修改最上面的代码:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Threading;
namespace treadtest
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private Thread test_thread_run = null;
private static object dataLock = new object(); // 多线程访问链表使用的锁
private List<string> dataList = new List<string>();
bool is_test_thread_run;
private static object displayDataObj = new object();
private uint m_serialNumber = 0;
private void TestThreadFunc()
{
while (is_test_thread_run)
{
lock (dataLock)
{
if (0 == dataList.Count)
{
Thread.Sleep(20);
continue;
}
// 每当链表中有数据时就在listview中显示该条数据
int count = 0;
while (0 < (count = dataList.Count))
{
string param = dataList[0];
if (null != param)
{
ShowTransComRecData(param);
}
dataList.Remove(param);
}
}
}
}
delegate void SetTextCallBack(string text);
private void ShowTransComRecData(string str)
{
// 判断是不是创建该控件以外的线程想访问它
if (this.listView.InvokeRequired)
{
SetTextCallBack stcb = new SetTextCallBack(ShowTransComRecData);
this.Invoke(stcb, new object[] { str });
}
else
{
ListViewItem item = new ListViewItem();
item.Text = (++m_serialNumber).ToString();
item.SubItems.Add(DateTime.Now.ToString());
item.SubItems.Add(str);
listView.Items.Add(item);
}
}
private void button_start_thread_Click(object sender, EventArgs e)
{
// 开启线程处理链表中的数据
is_test_thread_run = true;
test_thread_run = new Thread(new ThreadStart(TestThreadFunc));
test_thread_run.IsBackground = true;
test_thread_run.Start();
this.button_start_thread.Enabled = false;
}
private void button_pushback_data_Click(object sender, EventArgs e)
{
lock (dataLock)
{
// 链表中添加一条数据
this.dataList.Add(this.textBox_data.Text);
}
}
}
}
按照上面的三种方式修改后,可以看到程序能够按照我们的想的那样一直显示链表的数据了