额,我猜你现在可能会这么想“既然线程会对我的程序产生负面影响,那么我为什么要使用它呢?”。其实问题的关键不在于到底用不用线程,而在于何时何地使用线程。知道在什么情况下应该使用线程是好的设计决策的核心。使用线程有两个不同的优势。在这一部分,我们将讨论这两个优势是什么。
后台处理逻辑
第一个使用线程的优势是当你需要在后台运行一个很耗时的操作同时希望用户界面仍然可用时。我们都遇到过很多次由于后台在查询数据或者在处理大量信息而导致界面没有响应的情况。例如,专业图形处理软件以一个特定格式显示图片时。在一些产品的早期版本,让程序显示一个大的图片会导致程序没有响应直到图片显示出来。有时候为了完成某项工作,我们不得不让它晚上来处理图片,然后第二天早上来看我们期望的结果,这样做也是由于在电脑面前空坐一个小时忒不靠谱。这个问题在于提供一个后台线程来做一些CPU敏感工作同时让你的用户界面运行在主线程中。
我们来看一个需要生成新线程来处理的后台操作。这个例子主要是用来做文件搜索的。当搜索引擎找到一个匹配的文件后,它会向列表中添加一个新值。
下面的代码显示这个方法确实需要生成新线程:
/************************************* /* Copyright (c) 2012 Daniel Dong * * Author:oDaniel Dong * Blog:o www.cnblogs.com/danielWise * Email:o guofoo@163.com * */ using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Text; using System.Windows.Forms; using System.IO; using System.Threading; namespace SearchForm { public partial class Form1 : Form { string search; string[] args = new string[1]; delegate void UpdateData(string returnVal); UpdateData UIDel; public Form1() { InitializeComponent(); } private void cmdSingle_Click(object sender, EventArgs e) { Search(); } public void Search() { search = textBox1.Text; SearchDirectory(@"D:\Pictures"); } public void SearchDirectory(string path) { //Search the directory DirectoryInfo di = new DirectoryInfo(path); FileInfo[] fi = di.GetFiles(search); foreach (FileInfo myFile in fi) { args[0] = myFile.FullName; if (UIDel != null) { this.Invoke(UIDel, args); } else { UpdateUI(args[0]); } } //Search its sub directories. DirectoryInfo[] subDirs = di.GetDirectories(); foreach (DirectoryInfo myDir in subDirs) { SearchDirectory(myDir.FullName); } } private void cmdMultiple_Click(object sender, EventArgs e) { UIDel = new UpdateData(UpdateUI); Thread t = new Thread(new ThreadStart(Search)); t.Start(); } private void UpdateUI(string result) { lstPrime.Items.Add(result); } } }
编译上面的例子然后运行。在搜索框输入一个搜索条件,比如*.*, 单击单线程搜索按钮,然后观察结果。你会发现,我们在搜索文件同时试着在每次发现匹配的文件时向界面更新一次。然而,由于用户界面和搜索程序代码运行在同一个线程中,我们在搜索代码完成之前看不到任何更新。我们也无法在搜索过程中重新设定窗体尺寸大小。
这个很长的代码片段实际上是一个非常简单的场景。我们来看一下能否通过一个简单的的改动来解决这个问题。在多线程搜索按钮中添加下面代码并使用一个新线程来搜索:
private void cmdMultiple_Click(object sender, EventArgs e) { UIDel = new UpdateData(UpdateUI); Thread t = new Thread(new ThreadStart(Search)); t.Start(); }
我们再次编译运行上面的例子。输入同样的搜索条件然后单击多线程搜索按钮。你可以看到有一些不同的地方-这次我们的输出结果会立即显示出来。这是因为Windows现在会在后台搜索线程和前台显示线程之间进行切换。处理器现在给了界面显示程序独立执行时间。你也会发现在搜索过程中可以重新设定窗体大小。
也可能有其他导致我们的用户接口没有响应的后台程序。我们可能还有一些更消耗CPU 资源的处理过程,比如对数据库执行搜索,排序,格式化,截取以及过滤数据功能。这是使用多线程的另外一种情况。你也可能生成一个单独线程进行日志处理。在这些情况下用户接口不会卡死,但是如果不是在它自己线程(进行日志操作)里的话会显得稍微有点慢。
访问外部资源
第二种可能要用到线程的情况是当你需要访问一些非本地的资源时。可能是网络上的一个数据库文件或者是网络共享文件之类的。在这种情况下,网络性能会极大地影响你的程序的性能。
让我们来看一个例子。在这个例子中我们将连接一个数据库。假设网络性能很差导致我们的程序性能也很不好。同时也假定公司安全策略规定在远程数据库服务器上不可以安装任何应用程序。
/************************************* /* Copyright (c) 2012 Daniel Dong * * Author:Daniel Dong * Blog: www.cnblogs.com/danielWise * Email: guofoo@163.com * */ 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.Data.SqlClient; using System.Threading; namespace Chapter02 { public partial class Form1 : Form { public Form1() { InitializeComponent(); } private void cmdQuery_Click(object sender, EventArgs e) { QueryData(); } public void QueryData() { SqlDataReader objReader; SqlConnection objConn; SqlCommand objCommand; int intEmployeeID; string strFirstName; string strTitle; int intReportsTo; objConn = new SqlConnection("Data Source=.;Initial Catalog=Northwind;Integrated Security=True"); objCommand = new SqlCommand("SELECT EmployeeID, FirstName, Title, ReportsTo FROM Employees", objConn); objConn.Open(); objReader = objCommand.ExecuteReader(CommandBehavior.CloseConnection); while (objReader.Read()) { intEmployeeID = objReader.GetInt32(0); strFirstName = objReader.GetString(1); strTitle = objReader.GetString(2); if (objReader.IsDBNull(3)) { intReportsTo = 0; } else { intReportsTo = objReader.GetInt32(3); } listBox1.Items.Add(intEmployeeID.ToString() + " " + strFirstName + " " + strTitle + " " + intReportsTo.ToString()); } objReader.Close(); objConn.Close(); } } }
你可以从这个例子中发现我们所做的就是查询一个数据库(本例中为了方便使用本地数据库,实际应用时一般是远程数据库)。返回的数据不是很多,但是你会发现用户界面会在它花费时间获取数据并更新到列表中得这段时间内卡住。我们仍然通过另外生成一个线程并在这个新的线程中执行数据库查询来改进这个程序。现在再添加一个新的按钮并加入以下代码:
private void cmdMultiQuery_Click(object sender, EventArgs e) { Thread t = new Thread(new ThreadStart(QueryData)); t.Start(); }
现在我们运行这段代码,会得到与我们上个例子的结果类似的结果:
我们也可以在查询过程中重设窗口尺寸。用户界面在整个查询过程中都是相应的。当然,我想重申的是以上内容并不意味着你应该在每次访问数据库时都创建一个新线程。你应该先分析一下能否把数据库文件或者程序移动到同一台服务器上。同时确定这个组件不会被不同的客户端程序持续地调用。如果是这样的话那么就得为每次调用生成一个新线程且会比你期望的消耗更多资源。当每次调用我们的对象时不用都生成新线程,有些时候可以重用对象以及线程。这个问题将会在第三章和第五章介绍。
下一篇介绍线程陷阱以及循环中的线程...