1.1 进程和线程的概念
进程是对一段静态指令序列(程序)的动态执行过程,是系统进行资源分配和调度的一个基本单位。与进程相关的信息包括进程的用户标志、正在执行的已经编译好的程序、进程程序和数据在存储器中的位置,等等。进程是程序文件的内存实例。Windows是一个多任务的系统,如果使用的是Windows2000及其以上版本,则可以通过任务管理器查看当前系统运行的程序和进程,如图1-1所示。当一个程序开始运行时,它就是一个进程,进程所指的是运行中的程序使用到的内存和系统资源。通俗点说,就是指运行中的程序,程序是“死”的,一旦运行进入内存,就“活”了,即程序的内存实例——进程。
图1-1 Windows系统的进程
同一进程又可以划分为若干个独立的执行流,我们称之为线程。线程是CPU调度和分配的基本单位。线程是程序中的一个执行流,每个线程都有自己的专有寄存器(栈指针、程序计数器等),但代码区是共享的,即不同的线程可以执行同样的函数。多线程是指程序中包含多个执行流,即在一个程序中可以同时运行多个不同的线程来执行不同的任务,也就是说允许单个程序创建多个并行执行的线程来完成各自的任务。在Windows环境下,用户可以同时运行多个应用程序,每个执行的应用程序就是一个进程。例如,浏览器就是一个很好的多线程的例子,在浏览器中用户可以在下载Java应用程序或图像的同时滚动页面,在访问新页面时,播放动画和声音,打印文件等。同时打开两个QQ时,每个运行的QQ就是一个进程;而用一个QQ和多个人聊天时,每个聊天窗口就是一个线程。
进程和线程概念的提出,对提高软件的并行性有着重要的意义。并行性的主要特点就是并发处理。当然,对一个单处理器的计算机系统来说,单个CPU在任何时刻只能执行一个线程。在一个单处理器系统中,操作系统可以通过分时处理来获得并发,在这种情况下,系统为每个线程分配一个CPU时间片,每个线程只有在分配的时间片内才拥有对CPU的控制权,其他时间都在等待。即同一时间只有一个线程在运行。由于系统为每个线程划分的时间片很小(20毫秒左右),所以在用户看来,好像是多个线程在同时运行。实际上,一个线程执行了一个时间单元后,另外一个线程接着执行下一个时间单元,如此反复,这就是并发。
在网络环境下为什么要使用多线程呢?考虑这样一种情况:在C/S模式下,服务器需要不断监听来自各个客户端的请求,这时,如果采用单线程机制的话,服务器将无法处理其他事情,因为这个线程要不断地循环监听请求而无暇对其他请求做出响应。实际上,当要花费大量时间进行连续的操作时,或者等待网络或其他I/O设备响应时,都可以使用多线程技术。
下面,我们引入两个例子,以便更直观地看到多线程编程技术的好处。
【例1.1】单线程例子,要求:单击【开始】,在左边的ListBok控件中添加10000个项,为Item n(n=0,1,2,…,99999),单击【查看】弹出对话框,显示ListBox里面的项的总数,如图1-2所示。
图1-2 单线程例子程序
例1.1的代码如下所示。
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; namespace ThreadEx101 { public partial class Form1 : Form { public Form1() { InitializeComponent(); } private void btnStart_Click(object sender, EventArgs e) { for (int index=0; index < 100000; index++) { this.lstTest.Items.Add(string.Format("Item {0}", index)); } } private void btnLook_Click(object sender, EventArgs e) { MessageBox.Show(string.Format("ListBox中一共有{0}项", this.lstTest.Items.Count)); } } }
运行上述程序,会发现两个问题:其一,由于添加的项比较多,要等待一段时间(具体时间由计算机的配置情况来确定),显示的项是添加完成后才全部显示出来的,而不是一个个显示出来的;其二,在ListBox项显示出来之前,单击【查看】会发现不起作用,因为在单线程中,添加项的线程还没有完成,独占了整个进程的资源,Item的个数一直在自增,是不确定的,“查看”这个线程任务只能等待“开始”线程任务结束后才能运行。
在下面的例1.2中,把例1.1的单线程程序改为多线程程序,解决了上述两个问题。
【例1.2】多线程例子,要求:单击【开始】,在左边的ListBok控件中添加10000个项,为Item n(n=0,1,2,…,99999),单击【查看】弹出对话框,显示ListBox里面的项的总数。
本例实现过程如下:添加1个新窗体Form2,在窗体Form2上添加和Form1同样的控件,如图1.3所示。
图1-3 多线程程序界面
Form2.cs的代码如下所示。
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 ThreadEx101 { public partial class Form2 : Form { public Form2() { InitializeComponent(); } private void button1_Click(object sender, EventArgs e) { ThreadStart threadStart=new ThreadStart(AddItem); Thread thread=new Thread(threadStart); //声明一个线程 thread.Start(); //启动线程 } private void AddItem() { for (int index=0; index < 100000; index++) { this.lstTest.Items.Add(string.Format("Item {0}", index)); } } private void button2_Click(object sender, EventArgs e) { MessageBox.Show(string.Format("ListBox中一共有{0}项", this.lstTest.Items.Count)); } } }
在主程序Program.cs中将启动窗体改为Form2,代码如下所示。
using System; using System.Collections.Generic; using System.Linq; using System.Windows.Forms; namespace ThreadEx101 { static class Program { //<summary> //应用程序的主入口点 //</summary> [STAThread] static void Main() { Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); Application.Run(new Form2()); //此处把原来的Form1改为Form2 } } }
注意此时运行时不要按【调试】下的【启动调试(S)】,而是按【开始执行(不调试)】来运行程序。
例1.2的运行结果和例1.1的运行结果不一样,Item n是动态一个个加上去的,而不是全部在内存中添加完成后显示出来。而且,随时可以单击【查看】,显示ListBox中的项数,运行结果如图1-4所示。
图1-4 多线程程序运行结果
例1.2的运行结果解决了例1.1出现的两个问题,原因就是例1.1是单线程运行的,而例1.2是多线程运行的,ListBox中显示的任务在另一个线程中运行,“查看”任务可以同时运行。