且听风吟

疯狂的IT菜鸟巢

博客园 联系 订阅 管理
  13 Posts :: 0 Stories :: 69 Comments :: 0 Trackbacks

      我们在做winform应用的时候,大部分情况下都会碰到使用多线程控制界面上控件信息的问题。然而我们并不能用传统方法来做这个问题,下面我将详细的介绍。

      首先来看传统方法:

      public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }
        private void Form1_Load(object sender, EventArgs e)
        {
            Thread thread = new Thread(ThreadFuntion);
            thread.IsBackground = true;
            thread.Start();
        }
        private void ThreadFuntion()
        {
            while (true)
            {
                this.textBox1.Text = DateTime.Now.ToString();
                Thread.Sleep(1000);
            }
        }
    }

       运行这段代码,我们会看到系统抛出一个异常:Cross-thread operation not valid:Control 'textBox1' accessed from a thread other than the thread it was created on . 这是因为.net 2.0以后加强了安全机制,不允许在winform中直接跨线程访问控件的属性。那么怎么解决这个问题呢,下面提供几种方案。

      第一种方案,我们在Form1_Load()方法中加一句代码:

      private void Form1_Load(object sender, EventArgs e)
      {
            Control.CheckForIllegalCrossThreadCalls = false;
            Thread thread = new Thread(ThreadFuntion);
            thread.IsBackground = true;
            thread.Start();
        }
      加入这句代码以后发现程序可以正常运行了。这句代码就是说在这个类中我们不检查跨线程的调用是否合法(如果没有加这句话运行也没有异常,那么说明系统以及默认的采用了不检查的方式)。然而,这种方法不可取。我们查看CheckForIllegalCrossThreadCalls 这个属性的定义,就会发现它是一个static的,也就是说无论我们在项目的什么地方修改了这个值,他就会在全局起作用。而且像这种跨线程访问是否存在异常,我们通常都会去检查。如果项目中其他人修改了这个属性,那么我们的方案就失败了,我们要采取另外的方案。

      下面来看第二种方案,就是使用delegate和invoke来从其他线程中控制控件信息。网上有很多人写了这种控制方式,然而我看了很多这种帖子,表明上看来是没有什么问题的,但是实际上并没有解决这个问题,首先来看网络上的那种不完善的方式:

public partial class Form1 : Form
    {
        private delegate void FlushClient();//代理
        public Form1()
        {
            InitializeComponent();
        }
        private void Form1_Load(object sender, EventArgs e)
        {
            Thread thread = new Thread(CrossThreadFlush);

            thread.IsBackground=true;
            thread.Start();
        }

        private void CrossThreadFlush()
        {
            //将代理绑定到方法
            FlushClient fc = new FlushClient(ThreadFuntion);
            this.BeginInvoke(fc);//调用代理
        }
        private void ThreadFuntion()
        {
            while (true)
            {
                this.textBox1.Text = DateTime.Now.ToString();
                Thread.Sleep(1000);
            }
        }
    }

       使用这种方式我们可以看到跨线程访问的异常没有了。但是新问题出现了,界面没有响应了。为什么会出现这个问题,我们只是让新开的线程无限循环刷新,理论上应该不会对主线程产生影响的。其实不然,这种方式其实相当于把这个新开的线程“注入”到了主控制线程中,它取得了主线程的控制。只要这个线程不返回,那么主线程将永远都无法响应。就算新开的线程中不使用无限循环,使可以返回了。这种方式的使用多线程也失去了它本来的意义。

       现在来让我们看看推荐的解决方案:

public partial class Form1 : Form
    {
        private delegate void FlushClient();//代理
        public Form1()
        {
            InitializeComponent();
        }
        private void Form1_Load(object sender, EventArgs e)
        {
            Thread thread = new Thread(CrossThreadFlush);
            thread.IsBackground = true;
            thread.Start();
        }

        private void CrossThreadFlush()
        {
            while (true)
            {
                //将sleep和无限循环放在等待异步的外面
                Thread.Sleep(1000);
                ThreadFunction();
            }
        }
        private void ThreadFunction()
        {
            if (this.textBox1.InvokeRequired)//等待异步
            {
                FlushClient fc = new FlushClient(ThreadFunction);
                this.Invoke(fc);//通过代理调用刷新方法
            }
            else
            {
                this.textBox1.Text = DateTime.Now.ToString();
            }
        }
    }

       运行上述代码,我们可以看到问题已经被解决了,通过等待异步,我们就不会总是持有主线程的控制,这样就可以在不发生跨线程调用异常的情况下完成多线程对winform多线程控件的控制了。

 

       对于深山老林提出的问题,我最近找到了更优的解决方案,利用了delegate的异步调用,大家可以看看:

 

public partial class Form1 : Form
    {
        private delegate void FlushClient();//代理
        public Form1()
        {
            InitializeComponent();
        }
        private void Form1_Load(object sender, EventArgs e)
        {
            Thread thread = new Thread(CrossThreadFlush);
            thread.IsBackground = true;
            thread.Start();
        }

        private void CrossThreadFlush()
        {

             FlushClient fc=new FlushClient(ThreadFunction);

             fc.BeginInvoke(null,null);
        }
        private void ThreadFunction()
        {

              while (true)
            {
                this.textBox1.Text = DateTime.Now.ToString();
                Thread.Sleep(1000);
            }

        }
    }

     这种方法也可以直接简化为(因为delegate的异步就是开了一个异步线程):

 

public partial class Form1 : Form
    {
        private delegate void FlushClient();//代理
        public Form1()
        {
            InitializeComponent();
        }
        private void Form1_Load(object sender, EventArgs e)
        {
             FlushClient fc=new FlushClient(ThreadFunction);

             fc.BeginInvoke(null,null);
        }

         private void ThreadFunction()
        {

              while (true)
            {
                this.textBox1.Text = DateTime.Now.ToString();
                Thread.Sleep(1000);
            }

        }
    }

     

posted on 2009-03-17 12:00 微风吟 阅读(8441) 评论(34) 编辑 收藏

Feedback

#1楼 2009-03-17 12:10 dark_arthur      
直接用个timer可以吗?
 回复 引用 查看   

#2楼 2009-03-17 12:23 lisw      
控件的跨线程调用,与线程的实现方式无关,关键在于下面这句话
if (this.textBox1.InvokeRequired)//等待异步
{
FlushClient fc = new FlushClient(ThreadFunction);
this.Invoke(fc);//通过代理调用刷新方法
}


 回复 引用 查看   

#3楼 2009-03-17 12:24 iTech      
有个control也可以
 回复 引用 查看   

#4楼[楼主] 2009-03-17 12:32 微风吟      
@dark_arthur
用timer在这个例子里可以,但是如果客户端需要有一个线程等待服务器消息的话,用timer就不行了。

 回复 引用 查看   

#5楼 2009-03-17 12:41 Artech      
建议看看SynchronizationContext的介绍!
 回复 引用 查看   

#6楼 2009-03-17 12:47 shawnliu      
Control.Invoke就可以了
 回复 引用 查看   

#7楼 2009-03-17 14:34 沧海一声笑      
感觉代码逻辑怪怪的 呵呵

如果是想改变控件的值的话,可以在Form中开启工作线程,在另一个业务层中的类中执行方法,然后通过事件触发修改,

 回复 引用 查看   

#8楼 2009-03-17 14:44 泪狮      
delegate void delegateRefreshStatus(string txt);
private void RefreshStatus(string txt)
{
try
{
if (this.InvokeRequired)
{
this.BeginInvoke(new delegateRefreshStatus(RefreshStatus), txt);
}
else
{
this.lblStatus.Text = txt;
}
}
catch (Exception ex)
{
MsgBox.ShowInfo("更新界面状态时发生异常:" + ex.Message);
}
}

 回复 引用 查看   

#9楼 2009-03-17 19:59 葛云飞      
实现多线程控件,类的一种通用的方法(个人理解)
1.把你的类加一个Windows.Forms.Form的Parent或owner
2.对外声明你在线种中引发的事件。
3.当事件引发时,用Parent.Invoke...(同步)或Parent.BeginInvoke调用(异步)

 回复 引用 查看   

InvokeRequired,从名字上看也不是等待异步吧。。。只是判断当前线程是否是控件的创建线程相同的一个属性。。

【沧海一声笑】
使用事件的方式,被调用的handler仍然属于引发事件的线程。。跨线程设置属性的问题依然存在。。。

 回复 引用   

#11楼 2009-03-18 09:05 devil0153      
private ModifyControl(){
Action action = delegate(){
//Do Modify Control
};
if(this.InvokeRequired)
this.Invoke(action);
else
action();
}

 回复 引用 查看   

#12楼 2009-03-18 12:02 沧海一声笑      
@Sapphire
呵呵 那样的就在业务类中BeginInvoke开启线程,具体工作方法中触发事件,在form中的事件也是用InvokeRequired判断. 进度条的很多程序都这么弄的!

 回复 引用 查看   

#13楼 2009-03-19 09:18 深山老林      
文章写的非常的好,问题是解决了,可是新的问题来了,如果我的界面上文本框特别的多,难道都得编写如下代码来解决吗?
if (this.textBox1.InvokeRequired)//等待异步
{
FlushClient fc = new FlushClient(ThreadFunction);
this.Invoke(fc);//通过代理调用刷新方法
}

 回复 引用 查看   

#14楼[楼主] 2009-03-19 14:08 微风吟      
@深山老林
嗯 这个确实是个大问题 我再想想

 回复 引用 查看   

#15楼 2009-03-19 14:45 深山老林      
在实际的项目开发中,控件的数量可不像你的demo中的那么少,会非常的庞大,而且还会在不同流程中改变不同控件的一个或多个属性。如果用您的这种方法来解决问题,我想到时候代码会翻着倍的增加。其实,只要不涉及sleep()这个方法,就可以少些外围的那个函数了。
 回复 引用 查看   

delegate void delegateRefreshStatus(string txt);
private void RefreshStatus(string txt)
{
try
{
if (this.InvokeRequired)
{
this.BeginInvoke(new delegateRefreshStatus(RefreshStatus), txt);
}
else
{
this.lblStatus.Text = txt;
}
}
catch (Exception ex)
{
MsgBox.ShowInfo("更新界面状态时发生异常:" + ex.Message);
}
}
//把需要修改的的控件也作为参数传递过来就可以解决都很多控件的问题了
中RefreshStatus(string txt,TextBox TxtBox)
{
....
TxtBox.Text=txt;
....
}
有问题yaoohfox@163.com联系。有机会和博主交流交流。

 回复 引用   

#17楼 2009-03-20 16:30 深山老林      
@问问very goog
问题依然没有解决,你这个是批量修改控件的text属性,而我要修改个别控件的多个属性跟多个控件的属性,这样似乎是解决不了问题的。

 回复 引用 查看   

#18楼[楼主] 2009-03-20 16:55 微风吟      
@深山老林
这两天比较忙 没有时间来考虑
等我周末考虑考虑有什么好的解决方法

 回复 引用 查看   

#19楼[楼主] 2009-03-20 16:56 微风吟      
@问问very goog
不错 不错 我的msn zhaotiantang520@live.cn

 回复 引用 查看   

#20楼 2009-03-20 16:56 深山老林      
好的,谢谢了。
 回复 引用 查看   

#21楼[楼主] 2009-03-23 16:23 微风吟      
@深山老林
你有什么好的建议没有 发出来大家一起看看

 回复 引用 查看   

#22楼 2009-03-23 16:27 深山老林      
Control.CheckForIllegalCrossThreadCalls = false;
Thread thread = new Thread(ThreadFuntion);
thread.IsBackground = true;
我目前在项目中是通过这种方式来实现的,我目前还没有想出什么什么好的方法。感觉这种方案是可以解决问题的,而且暂时也没有看出来这种方法的缺点在什么地方。

 回复 引用 查看   

#23楼[楼主] 2009-03-24 10:18 微风吟      
@深山老林
这个就是我提的第一种解决方案,缺点这篇里面也说了,不太符合我们一般面向对象编程的规范.

 回复 引用 查看   

#24楼 2009-04-13 18:11 冰碟      
楼主
第二种方法是不是将代码
private void CrossThreadFlush()
{
//将代理绑定到方法
FlushClient fc = new FlushClient(ThreadFuntion);
this.BeginInvoke(fc);//调用代理
}

改成这样就可以了。
private void CrossThreadFlush()
{
//将代理绑定到方法
FlushClient fc = new FlushClient(ThreadFuntion);
fc.Invoke();//调用代理
}

 回复 引用 查看   

#25楼[楼主] 2009-05-15 11:23 微风吟      
@冰碟
是的,我也是最近才发现这种解决方法的

 回复 引用 查看   

#26楼 2009-07-22 15:05 Sid[未注册用户]
为什么最后一个简化的例子中没有 CrossThreadFlush 方法?
 回复 引用   

#27楼 2009-08-18 17:18 kkkk[未注册用户]
把最后的方法补全。我看昏了
 回复 引用   

#28楼 2009-12-26 23:51 冰封的心      
不知这种方法如何
ThreadPool.QueueUserWorkItem(new WaitCallback(delegate(object o)
{
while (true)
{
this.Invoke(new Action(delegate() { label1.Text = DateTime.Now.ToString(); }));
}
}));

 回复 引用 查看   

#29楼 2010-03-05 14:19 Mr Chai      
拿如果要更新多个控件有什么快捷的方式嘛? 比如说要更新十个 Label的Text 值?
 回复 引用 查看   

#30楼 2010-05-20 14:19 清明时节雨      
楼主最后改进的方法好像有问题。
private void CrossThreadFlush()
{
FlushClient=new FlushClient(ThreadFunction);
FlushClient.BeginInvoke(null,null);
}
语法就不对。
我改成
private void CrossThreadFlush()
{
FlushClient fc=new FlushClient(ThreadFunction);
fc.BeginInvoke(null,null);
}
又出现了不让跨线程修改的错误。

 回复 引用 查看   

#31楼[楼主] 2010-05-20 16:14 微风吟      
@清明时节雨
我又试了一下是可以的,你再调试下吧

 回复 引用 查看   

#32楼 2010-05-21 05:45 清明时节雨      
我又把现在的代码Copy下来试了一下。
运行后Text没有变化,我跟踪了一下,
运行到:this.textBox1.Text = DateTime.Now.ToString();
这一行就停了。

 回复 引用 查看   

#33楼 2010-05-22 20:19 清明时节雨      
我又反复尝试了以给出的最优解决方案。
this.textBox1.Text = DateTime.Now.ToString();
这一行总会中断它所处的线程。
如果写成this.textBox1.Text ="";就可以运行过去。
否则就停下了,this.textBox1.Text ="1";都不行。
你运行的时候不是这个样子吗?还是我哪个地方设置有问题?
或者环境问题?
我用的Vista系统,VS2005 SP1,全部升级到最新了。

 回复 引用 查看   

#34楼 2011-07-22 12:33 晓宁      
学习了,我先试一试去
 回复 引用 查看