进程、线程与网络协议(一)
C#网络应用高级编程学习
1.1 进程和线程
进程是对一段静态指令序列(程序)的动态执行过程,是系统进行资源分配和调度的一个基本单位。与进程相关的信息包括进程的用户标志、正在执行的已经编译好的程序、进程程序和数据在存储器中的位置等等。同一个进程又可以划分为若干个独立的执行流,我们称之为线程。线程是 CPU 调度和分配的基本单位。在 Windows 环境下,用户可以同时运行多个应用程序,每个执行的应用程序就是一个进程。例如一台电脑上同时打开两个 QQ 时,每个运行的QQ 就是一个进程;而用一个 QQ 和多个人聊天时,每个聊天窗口就是一个线程。进程和线程概念的提出,对提高软件的并行性有着重要的意义。并行性的主要特点就是并发处理。
在一个单处理器系统中,可以通过分时处理来获得并发,这种情况下,系统为每个线程分配一个 CPU 时间片,每个线程只有在分配的时间片内才拥有对 CPU 的控制权,其他时间都在等待。即同一时间只有一个线程在运行。由于系统为每个线程划分的时间片很小(20 毫秒左右),所以在用户看来,好像是多个线程在同时运行。
为什么要使用多线程呢?考虑这样一种情况:在 C/S 模式下,服务器需要不断监听来自各个客户端的请求,这时,如果采用单线程机制的话,服务器将无法处理其他事情,因为这个线程要不断的循环监听请求而无暇对其他请求做出响应。实际上,当要花费大量时间进行连续的操作时,或者等待网络或其他 I/O 设备响应时,都可以使用多线程技术。在 C#中,有两个专门用于处理进程和线程的类:Process 类和 Thread 类。
1.1.1Process 类
Process 类位于 System.Diagnostics 命名空间下,用于完成进程的相关处理任务。可以在本地计算机上启动和停止进程,也可以查询进程的相关信息。在自己的程序中运行其他的应用程序,实际上就是对进程进行管理。如果希望在自己的进程中启动和停止其他进程,首先要创建Process 类的实例,并设置对象的 StartInfo 属性,然后调用该对象的 Start 方法启动进程。
【例 1-1】启动、停止和观察进程
1 //展开工具箱中的【组件】选项卡,然后将Process 组件拖放到设计窗体上。 2 using System; 3 using System.Collections.Generic; 4 using System.ComponentModel; 5 using System.Data; 6 using System.Drawing; 7 using System.Linq; 8 using System.Text; 9 using System.Threading.Tasks; 10 using System.Windows.Forms; 11 using System.Diagnostics; 12 using System.Threading; 13 namespace NetProgramDemo 14 { 15 public partial class Form1 : Form 16 { 17 public Form1() 18 { 19 InitializeComponent(); 20 } 21 22 private void button1_Click(object sender, EventArgs e) 23 { 24 process1.StartInfo.FileName = "notepad.exe"; 25 //启动 Notepad.exe 进程. 26 process1.Start(); 27 } 28 29 private void button2_Click(object sender, EventArgs e) 30 { 31 //创建新的 Process 组件的数组,并将它们与指定的进程名称(Notepad)的所有进程资源相关联. 32 Process[] myprocesses; 33 myprocesses = Process.GetProcessesByName("Notepad"); 34 foreach (Process instance in myprocesses) 35 { 36 //设置终止当前线程前等待 1000 毫秒 37 instance.WaitForExit(1000); 38 instance.CloseMainWindow(); 39 } 40 } 41 42 private void button3_Click(object sender, EventArgs e) 43 { 44 listBox1.Items.Clear(); 45 //创建 Process 类型的数组,并将它们与系统内所有进程相关联 46 Process[] processes; 47 processes = Process.GetProcesses(); 48 foreach (Process p in processes) 49 { 50 //Idle 指显示 CPU 空闲率的进程名称 51 //由于访问 Idle 的 StartTime 会出现异常,所以将其排除在外 52 if (p.ProcessName != "Idle") 53 { 54 //将每个进程名和进程开始时间加入 listBox1 中 55 this.listBox1.Items.Add( 56 string.Format("{0,-30}{1:h:m:s}", p.ProcessName, p.StartTime)); 57 } 58 } 59 } 60 } 61 }
效果图:记得以管理员运行,否则会出现没有权限,拒绝访问类似提示
1.1.2Thread 类
在 System.Threading 命名空间下,包含了用于创建和控制线程的 Thread 类。对线程的常用操作有:启动线程、终止线程、合并线程和让线程休眠等。
1. 启动线程
在使用线程前,首先要创建一个线程。其一般形式为:
Thread t=new Thread(enterPoint);
其中 enterPoint 为线程的入口,即线程开始执行的方法。在托管代码中,通过委托处理线程执行的代码。例如:
Thread t=new Thread(new ThreadStart(methodName));
创建线程实例后,就可以调用 Start 方法启动线程了。
2. 终止线程
线程启动后,当不需要某个线程继续执行的时候,有两种终止线程的方法。一种是事先设置一个布尔变量,在其他线程中通过修改该变量的值作为传递给该线程是否需要终止的判断条件,而在该线程中循环判断该条件,以确定是否退出线程,这是结束线程的比较好的方法,实际编程中一般使用这种方法。
第二种方法是通过调用 Thread 类的 Abort 方法强行终止线程。例如:
t.Abort();
Abort 方法没有任何参数,线程一旦被终止,就无法再重新启动。由于 Abort 通过抛出异常强行终止结束线程,因此在实际编程中,应该尽量避免采用这种方法。调用 Abort 方法终止线程时,公共语言运行库(CLR)会引发 ThreadAbortException 异常,程序员可以在线程中捕获 ThreadAbortException 异常,然后在异常处理的 Catch 块或者 Finally块中作释放资源等代码处理工作;但是,线程中也可以不捕获 ThreadAbortException 异常,而由系统自动进行释放资源等处理工作。
注意,如果线程中捕获了 ThreadAbortException 异常,系统在 finally 子句的结尾处会再次引发 ThreadAbortException 异常,如果没有 finally 子句,则会在 Catch 子句的结尾处再次引发该异常。为了避免再次引发异常,可以在 finally 子句的结尾处或者 Catch 子句的结尾处调用System.Threading.Thread.ResetAbort 方法防止系统再次引发该异常。
使用 Abort 方法终止线程,调用 Abort 方法后,线程不一定会立即结束。这是因为系统在结束线程前要进行代码清理等工作,这种机制可以使线程的终止比较安全,但清理代码需要一定的时间,而我们并不知道这个工作将需要多长时间。因此,调用了线程的 Abort 方法后,如果系统自动清理代码的工作没有结束,可能会出现类似死机一样的假象。为了解决这个问题,可以在主线程中调用子线程对象的 Join 方法,并在 Join 方法中指定主线程等待子线程结束的等待时间。
3. 合并线程
Join 方法用于把两个并行执行的线程合并为一个单个的线程。如果一个线程 t1 在执行的过程中需要等待另一个线程 t2 结束后才能继续执行,可以在 t1 的程序模块中调用 t2 的 join()
方法。
例如:t2.Join();
这样 t1 在执行到 t2.Join()语句后就会处于阻塞状态,直到 t2 结束后才会继续执行。但是假如 t2 一直不结束,那么等待就没有意义了。为了解决这个问题,可以在调用 t2 的Join 方法的时候指定一个等待时间,这样 t1 这个线程就不会一直等待下去了。例如,如果希望将 t2 合并到 t1 后,t1 只等待 100 毫秒,然后不论 t2 是否结束,t1 都继续执行,就可以在 t1中加上语句:t2.Join(100);Join 方法通常和 Abort 一起使用。由于调用某个线程的 Abort 方法后,我们无法确定系统清理代码的工作什么时候才能结束,因此如果希望主线程调用了子线程的 Abort 方法后,主线程不必一直等待,可以调用子线程的 Join 方法将子线程连接到主线程中,并在连接方法中指定一个最大等待时间,这样就能使主线程继续执行了。
4. 让线程休眠
在多线程应用程序中,有时候并不希望某一个线程继续执行,而是希望该线程暂停一段时间,等待其他线程执行之后再继续执行。这时可以调用 Thread 类的 Sleep 方法,即让线程休眠。例如:
Thread.Sleep(1000);
这条语句的功能是让当前线程休眠 1000 毫秒。
注意,调用 Sleep 方法的是类本身,而不是类的实例。休眠的是该语句所在的线程,而不是其他线程。
5. 线程优先级
当线程之间争夺 CPU 时间片时,CPU 是按照线程的优先级进行服务的。在 C#应用程序中,可以对线程设定五个不同的优先级,由高到低分别是 Highest、AboveNormal、Normal、BelowNormal 和 Lowest。在创建线程时如果不指定其优先级,则系统默认为 Normal。假如想让一些重要的线程优先执行,可以使用下面的方法为其赋予较高的优先级:
Thread t=new Thread(new ThreadStart(enterpoint));
t.priority=ThreadPriority.AboveNormal;
通过设置线程的优先级可以改变线程的执行顺序,所设置的优先级仅仅适用于这些线程所属的进程。注意,当把某线程的优先级设置为 Highest 时,系统上正在运行的其他线程都会终止,所以使用这个优先级别时要特别小心。
6. 线程池
线程池是一种多线程处理形式,为了提高系统性能,在许多地方都要用到线程池技术。例如,在一个 C/S 模式的应用程序中的服务器端,如果每到一个请求就创建一个新线程,然后在新线程中为其请求服务的话,将不可避免的造成系统开销的增大。实际上,创建太多的线程可能会导致由于过度使用系统资源而耗尽内存。为了防止资源不足,服务器端应用程序应采取一定的办法来限制同一时刻处理的线程数目。线程池为线程生命周期的开销问题和资源不足问题提供了很好的解决方案。通过对多个任务重用线程,线程创建的开销被分摊到了多个任务上。其好处是,由于请求到达时线程已经存在,所以无意中也就消除了线程创建所带来的延迟。这样,就可以立即为新线程请求服务,使其应用程序响应更快。而且,通过适当地调整线程池中的线程数目,也就是当请求的数目超过了规定的最大数目时,就强制其他任何新到的请求一直等待,直到获得一个线程来处理为止,从而可以防止资源不足。线程池适用于需要多个线程而实际执行时间又不多的场合,比如有些常处于阻塞状态的线程。当一个应用程序服务器接受大量短小线程的请求时,使用线程池技术是非常合适的,它可以大大减少线程创建和销毁的次数,从而提高服务器的工作效率。但是如果线程要求运行的时间比较长的话,那么此时线程的运行时间比线程的创建时间要长得多,仅靠减少线程的创建时间对系统效率的提高就不是那么明显了,此时就不适合使用线程池技术,而需要借助其他的技术来提高服务器的服务效率。
7. 同步
同步是多线程中一个非常重要的概念。所谓同步,是指多个线程之间存在先后执行顺序的关联关系。如果一个线程必须在另一个线程完成某个工作后才能继续执行,则必须考虑如何让其保持同步,以确保在系统上同时运行多个线程而不会出现逻辑错误。当两个线程 t1 和 t2 有相同的优先级,并且同时在系统上运行时,如果先把时间片分给 t1使用,它在变量 variable1 中写入某个值,但如果在时间片用完时它仍没有完成写入,这时由于时间片已经分给 t2 使用,而 t2 又恰好要尝试读取该变量,它可能就会读出错误的值。这时,如果使用同步仅允许一个线程使用 variable1,在该线程完成对 variable1 的写入工作后再让 t2读取这个值,就可以避免出现此类错误。为了对线程中的同步对象进行操作,C#提供了 lock 语句锁定需要同步的对象。lock 关键字确保当一个线程位于代码的临界区时,另一个线程不进入临界区。如果其他线程试图进入锁定的代码,则它将一直等待(即被阻塞),直到该对象被释放。比如线程 t1 对 variable1 操作时,为了避免其他线程也对其进行操作,可以使用 lock 语句锁定 variable1,实现代码为:
lock(variable1)
{
variable1++;
}
注意,锁定的对象一定要声明为 private,不要锁定 public 类型的对象,否则将会使 lock语句无法控制,从而引发一系列问题。另外还要注意,由于锁定一个对象之后,其他任何线程都不能访问这个对象,需要使用该对象的线程就只能等待该对象被解除锁定后才能使用。因此如果在锁定和解锁期间处理的对象过多,就会降低应用程序的性能。还有,如果两个不同的线程同时锁定两个不同的变量,而每个线程又都希望在锁定期间访问对方锁定的变量,那么两个线程在得到对方变量的访问权之前都不会释放自己锁定的对象,从而产生死锁。在编写程序时,要注意避免这类操作引起的问题。
【例 1-2】线程的基本用法。
1 //手动添加timer 并添加事件 2 using System; 3 using System.Collections.Generic; 4 using System.ComponentModel; 5 using System.Data; 6 using System.Drawing; 7 using System.Linq; 8 using System.Text; 9 using System.Threading.Tasks; 10 using System.Windows.Forms; 11 using System.Threading; 12 namespace NetProgramDemo 13 { 14 public partial class Form2 : Form 15 { 16 StringBuilder sb = new StringBuilder(); 17 Thread thread1; 18 Thread thread2; 19 public Form2() 20 { 21 InitializeComponent(); 22 } 23 private void AppendString(string s) 24 { 25 lock (sb) 26 { 27 sb.Append(s); 28 } 29 } 30 public void Method1() 31 { 32 while (true) 33 { 34 Thread.Sleep(100); //线程休眠 100 毫秒 35 AppendString("a"); 36 } 37 } 38 public void Method2() 39 { 40 while (true) 41 { 42 Thread.Sleep(100); //线程休眠 100 毫秒 43 AppendString("b"); 44 } 45 } 46 private void button1_Click(object sender, EventArgs e) 47 { 48 sb.Remove(0, sb.Length); 49 timer1.Enabled = true; 50 thread1 = new Thread(new ThreadStart(Method1)); 51 thread2 = new Thread(new ThreadStart(Method2)); 52 thread1.Start(); 53 thread2.Start(); 54 } 55 56 private void button2_Click(object sender, EventArgs e) 57 { 58 thread1.Abort(); 59 thread1.Join(10); 60 thread2.Abort(); 61 thread2.Join(10); 62 } 63 private void timer1_Tick(object sender, EventArgs e) 64 { 65 if (thread1.IsAlive == true || thread2.IsAlive == true) 66 { 67 richTextBox1.Text = sb.ToString(); 68 } 69 else 70 { 71 timer1.Enabled = false; 72 } 73 } 74 } 75 }
效果图:
1.1.3 在一个线程中操作另一个线程的控件
默认情况下,C#不允许在一个线程中直接操作另一个线程中的控件,这是因为访 问Windows 窗体控件本质上不是线程安全的。如果有两个或多个线程操作某一控件的状态,则可能会迫使该控件进入一种不一致的状态。还可能出现其他与线程相关的 bug,以及不同线程争用控件引起的死锁问题。因此确保以线程安全方式访问控件非常重要。在调试器中运行应用程序时,如果创建某控件的线程之外的其他线程试图调用该控件,则调试器会引发一个 InvalidOperationException 异常,并提示消息:“从不是创建控件的线程访问它”。但是在 Windows 应用程序中,为了在窗体上显示线程中处理的信息,我们可能需要经常
在一个线程中引用另一个线程中的窗体控件。比较常用的办法之一是使用委托(delegate)来完成这个工作。
为了区别是否是创建控件的线程访问该控件对象,Windows 应用程序中的每一个控件对象都有一个 InvokeRequired 属性,用于检查是否需要通过调用 Invoke 方法完成其他线程对该控件的操作,如果该属性为 true,说明是其他线程操作该控件,这时可以创建一个委托实例,然后调用控件对象的 Invoke 方法,并传入需要的参数完成相应操作,否则可以直接对该控件对象进行操作,从而保证了安全代码下线程间的互操作。例如:
delegate void AppendStringDelegate(string str);
private void AppendString(string str)
{
if (richTextBox1.InvokeRequired)
{
AppendStringDelegate d = new AppendStringDelegate(AppendString);
richTextBox1.Invoke(d, "abc");
}
else
{
richTextBox1.Text += str;
}
}
这段代码中,首先判断是否需要通过委托调用对 richTextBox1 的操作,如果需要,则创建一个委托实例,并传入需要的参数完成 else 代码块的功能;否则直接执行 else 代码块中的内容。
实际上,由于我们在编写程序时就已经知道控件是在哪个线程中创建的,因此也可以在不是创建控件的线程中直接调用控件对象的 Invoke 方法完成对该线程中的控件的操作。注意,不论是否判断 InvokeRequired 属性,委托中参数的个数和类型必须与传递给委托的方法需要的参数个数和类型完全相同。
【例 1-3】一个线程操作另一个线程的控件的方法。
using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; using System.Threading; namespace NetProgramDemo { public partial class Form3 : Form { Thread thread1; Thread thread2; delegate void AppendStringDelegate(string str); AppendStringDelegate appendStringDelegate; public Form3() { InitializeComponent(); appendStringDelegate = new AppendStringDelegate(AppendString); } private void button1_Click(object sender, EventArgs e) { richTextBox1.Text = ""; thread1 = new Thread(new ThreadStart(Method1)); thread2 = new Thread(new ThreadStart(Method2)); thread1.Start(); thread2.Start(); } private void button2_Click(object sender, EventArgs e) { thread1.Abort(); thread1.Join(); thread2.Abort(); thread2.Join(); MessageBox.Show("线程 1、2 终止成功"); } private void AppendString(string str) { richTextBox1.Text += str; } private void Method1() { while (true) { Thread.Sleep(100); //线程 1 休眠 100 毫秒 richTextBox1.Invoke(appendStringDelegate, "a"); } } private void Method2() { while (true) { Thread.Sleep(100); //线程 2 休眠 100 毫秒 richTextBox1.Invoke(appendStringDelegate, "b"); } } } }