在Silverlight程序中使用Thread一个很容易被忽略的问题
有一个很常见的功能,我们需要在一个子窗口中定时调用服务,然后更新控件的内容,只要窗口开着就一直会调用服务。
那么现在就来完成这个功能,首先定义一个服务:
public class Service1 : IService1 { public string DoWork(string name) { File.AppendAllText(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "log.txt"), string.Format("时间:{0} 线程:{1}" + Environment.NewLine, DateTime.Now, name)); return "OK"; } }
这个服务在返回数据之前会写一个本地文本日志,写入时间和传入的参数。
然后添加一个子窗口,并在页面上放置一个按钮打开这个窗口:
ChildWindow1 c = new ChildWindow1();
c.Show();
子窗口的代码如下:
public partial class ChildWindow1 : ChildWindow { private Thread thread; private static int threadNum = 0; private ServiceReference1.Service1Client s = new ServiceReference1.Service1Client(); public ChildWindow1() { InitializeComponent(); s.DoWorkCompleted += new EventHandler<ServiceReference1.DoWorkCompletedEventArgs>(s_DoWorkCompleted); } private void s_DoWorkCompleted(object sender, ServiceReference1.DoWorkCompletedEventArgs e) { Dispatcher.BeginInvoke(() => Message.Items.Insert(0, string.Format("服务返回:{0} 控件:{1} 时间:{2} 线程:{3}", e.Result, Message.GetHashCode(), DateTime.Now, e.UserState.ToString()))); } private void ChildWindow_Loaded(object sender, RoutedEventArgs e) { Interlocked.Increment(ref threadNum); thread = new Thread(state => { var threadID = (int)state; while (true) { s.DoWorkAsync(threadID.ToString(), threadID); Thread.Sleep(1000); } }); thread.Start(threadNum); }
这个代码很简单:
1、我们有一个静态变量保存了总共的线程数
2、每次窗口打开+1
3、每次窗口打开的时候初始化一个线程,这个线程的作用就是不断调用服务,然后休眠1秒,调用的时候传入的参数就是当前线程编号(从1开始)
4、服务调用完成之后,我们会更新页面上的ListBox,添加一条数据,内容包括服务返回值、Message控件的标识、当前时间和后台线程的编号
好了,功能完成了,运行程序,我们的期望是在子窗口打开后:
1、子窗口的ListBox每一秒都会插入一行记录
2、服务端每一秒都会往文本文件写入一行记录
3、两者的线程编号保持一致,并且每次打开子窗口线程编号都会+1
运行程序,打开子窗口:
看看服务端的日志:
貌似完成了我们的需求,这个程序真的没问题吗?
第二次打开子窗口:
也没问题,但是服务端的日志就不对了(看到没问题不代表真的没问题啊):
这说明两个后台线程同时在运行,不能想当然认为子窗口关闭了,老的线程会结束(可能会觉得反正不是静态字段嘛,窗口关闭了等它回收Thread去)。
Silverlight的程序一般很少刷新页面,随着子窗口开关越来越多,线程也越来越多,并且以前的线程会一直进行服务调用。
随着子窗口开关无数次,后台有无数个线程在调用服务端,想想也是很恐怖的事情!
前端显示一点问题都没有(为什么ListBox却没有多显示?大家可以思考一下),潜在增加了服务端的压力,我们希望的是再子窗口关闭之后,后台线程可以结束。
很多人可能会这么写:
private void ChildWindow_Closed(object sender, EventArgs e) { if (thread != null) { try { thread.Abort(); } catch { } } }
子窗口关闭的时候结束线程嘛,反正它会抛ThreadAbortException,不用管了。
开关几次子窗口看看:
还是一样,难道线程没结束?
想当然了!其实何必强制结束线程呢?让线程自己完成就可以了:
这样就正常了。当然,也可以使用BackgroundWorker来实现这个功能:
using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Windows; using System.Windows.Controls; using System.Windows.Documents; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Animation; using System.Windows.Shapes; using System.Threading; using System.ComponentModel; namespace SilverlightApplication2 { public partial class ChildWindow1 : ChildWindow { private static int threadNum = 0; private ServiceReference1.Service1Client s = new ServiceReference1.Service1Client(); private BackgroundWorker bw = new BackgroundWorker(); public ChildWindow1() { InitializeComponent(); s.DoWorkCompleted += new EventHandler<ServiceReference1.DoWorkCompletedEventArgs>(s_DoWorkCompleted); bw.WorkerSupportsCancellation = true; bw.WorkerReportsProgress = true; bw.DoWork += new DoWorkEventHandler(bw_DoWork); bw.ProgressChanged += new ProgressChangedEventHandler(bw_ProgressChanged); } private void bw_ProgressChanged(object sender, ProgressChangedEventArgs e) { Message.Items.Insert(0, string.Format("服务器返回:{0} 控件:{1} 时间:{2} 线程:{3}", e.UserState, Message.GetHashCode(), DateTime.Now, e.UserState.ToString())); } private void s_DoWorkCompleted(object sender, ServiceReference1.DoWorkCompletedEventArgs e) { bw.ReportProgress(0, e.Result); } private void bw_DoWork(object sender, DoWorkEventArgs e) { while (!bw.CancellationPending) { var threadID = (int)e.Argument; s.DoWorkAsync(threadID.ToString(), threadID); Thread.Sleep(1000); } } private void ChildWindow_Loaded(object sender, RoutedEventArgs e) { Interlocked.Increment(ref threadNum); bw.RunWorkerAsync(threadNum); } private void ChildWindow_Closed(object sender, EventArgs e) { bw.CancelAsync(); } } }
当然,本文标题虽然说Silverlight,对于Winform也是一样的,只不过Silverlight的Thread.Abort()不奏效。总之两点,一,有的时候不能太想当然和凭经验,只有实际验证了才知道结果;二,写.NET程序也不能啥都不释放不去管,对于线程、网络链接等资源、大量内存分配还是需要多关注。