文章目录
14.进程与线程
在操作系统中,每运行一个程序都会开启一个进程,一个进程由多个线程构成。线程是程序执行流中最小的单元。
在应用程序中分为单线程程序和多线程程序。
- 单线程程序是指在一个进程空间中只有一个线程在执行;
- 多线程程序是指在一个进程空间中有多个线程在执行,并共享同一个进程的大小。
进程指在每个操作系统中自动启动的系统进程和一些自动启动的应用程序进程,在Windows操作系统中提供了任务管理器来查看当前启动的进程,并能关闭指定的进程。
14.1 Process:进程类
Process类主要提供对本地和远程进程的访问,并提供对本地进程的启动、停止等操作,该类所在的命名空间是System.Diagnostics
。
属性或方法 | 说明 |
---|---|
MachineName | 属性,获取关联进程正在其上运行的计算机的名称 |
Id | 属性,获取关联进程的唯一标识符 |
ExitTime | 属性,获取关联进程退出的时间 |
ProcessName | 属性,获取该进程的名称 |
StartTime | 属性,获取关联进程启动的时间 |
Threads | 属性,获取在关联进程中运行的一组线程 |
TotalProcessorTime | 属性,获取此进程的总的处理器时间 |
UserProcessorTime | 属性,获取此进程的用户处理器时间 |
Close() | 方法,释放与此组件关联的所有资源 |
CloseMainWindow() | 方法,通过向进程的主窗口发送关闭消息来关闭拥有用户界面的进程 |
Dispose() | 方法,释放由 Component 使用的所有资源 |
GetCurrentProcess() | 方法,获取新的 Process 组件,并将其与当前活动的进程关联 |
GetProcesses() | 方法,为本地计算机上的每个进程资源创建一个新的 Process 组件 |
GetProcesses(String) | 方法,为指定计算机上的每个进程资源创建一个新的 Process 组件 |
GetProcessesByName(String) | 方法,创建新的 Process 组件的数组,并将它们与本地计算机上共享指定的进程名称的所有进程资源关联 |
Kill() | 方法,立即停止关联的进程 |
Start() | 方法,启动(或重用)此 Process 组件的 Startinfo 属性指定的进程资源, 并将其与该组件关联 |
Start(String) | 方法,通过指定文档或应用程序文件的名称来启动进程资源,并将资源与新的 Process 组件关联 |
下面通过例子来显示所有当前系统中正在运行的进程。
using System;
using System.Diagnostics;
using System.Windows.Forms;
namespace WinFormsApp1
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private void button1_Click(object sender, EventArgs e)
{
Process[] processes = Process.GetProcesses();
foreach (Process p in processes)
{
richTextBox1.Text = richTextBox1.Text + p.ProcessName + "\r\n";
}
}
}
}
如果要启动进程,主要代码如下。
//获取进程名称
string ProcessName = {ProcessName};
//创建Process 类的对象
Process p = new Process();
//设置进程名称
p.StartInfo.FileName = ProcessName;
//启动进程
p.Start();
需要注意的是,一些进程由于权限不够是无法关闭的,因此在关闭进程的代码中要做异常处理。
14.2 Thread:线程及与线程有关的类
线程(Thread)是包含在进程中的,它位于System.Threading
命名空间中,与线程有关的类同样也都在System.Threading
命名空间中。
类名 | 说明 |
---|---|
Thread | 在初始的应用程序中创建其他的线程 |
ThreadState | 指定 Thread 的执行状态,包括开始、运行、挂起等 |
ThreadPriority | 线程在调度时的优先级枚举值,包括 Highest、AboveNormal、Normal、BelowNormal、Lowest |
ThreadPool | 提供一个线程池,用于执行任务、发送工作项、处理异步I/O等操作 |
Monitor | 提供同步访问对象的机制 |
Mutex | 用于线程间同步的操作 |
ThreadAbortException | 调用Thread类中的Abort方法时出现的异常 |
ThreadStateException | Thead处于对方法调用无效的ThreadState时出现的异常 |
Thread类主要用于实现线程的创建以及执行,其常用的属性和方法如下表所示。
属性或方法 | 说明 |
---|---|
Name | 属性,获取或设置线程的名称 |
Priority | 属性,获取或设置线程的优先级 |
ThreadState | 属性,获取线程当前的状态 |
IsAlive | 属性,获取当前线程是否处于启动状态 |
IsBackground | 属性,获取或设置值,表示该线程是否为后台线程 |
CurrentThread | 属性,获取当前正在运行的线程 |
Start() | 方法,启动线程 |
Sleep(int millisecondsTimout) | 方法,将当前线程暂停指定的毫秒数 |
Suspend() | 方法,挂起当前线程(已经被弃用) |
Join() | 方法,阻塞调用线程,直到某个线程终止为止 |
Interrupt() | 方法,中断当前线程 |
Resume() | 方法,继续已经挂起的线程(已经被弃用) |
Abort() | 方法,终止线程 |
14.3 ThreadStart:创建线程
使用线程时首先需要创建线程,在使用Thread类的构造方法创建其实例时,需要用到ThreadStart委托或者ParameterizedThreadStart委托创建 Thread 类的实例。ThreadStart委托只能用于无返回值、无参数的方法,ParameterizedThreadStart委托则可以用于带参数的方法。
使用ThreadStart创建线程首先需要创建ThreadStart委托的实例,然后再创建Thread类的实例。
ThreadStart ts = new ThreadStart(方法名);
Thread t = new Thread(ts);
下面的例子使用ThreadStart委托创建线程,并输出0-10中所有的偶数。
using System;
using System.Threading;
class Program
{
static void Main(string[] args)
{
ThreadStart ts1 = new ThreadStart(PrintEven);
Thread t1 = new Thread(ts1);
ThreadStart ts2 = new ThreadStart(PrintOdd);
Thread t2 = new Thread(ts2);
t1.Start();
t2.Start();
}
//打印0-10中的偶数
private static void PrintEven()
{
Console.WriteLine("打印0-10之间的偶数:");
for (int i = 0; i <= 10; i = i + 2)
{
Console.WriteLine(i);
}
}
//打印1-10 中的奇数
public static void PrintOdd()
{
Console.WriteLine("打印0-10之间的奇数:");
for (int i = 1; i <= 10; i = i + 2)
{
Console.WriteLine(i);
}
}
}
// 打印0-10之间的偶数:
// 0
// 2
// 4
// 6
// 8
// 10
// 打印0-10之间的奇数:
// 1
// 3
// 5
// 7
// 9
使用Threadstart委托为PrintEven方法创建了线程,通过线程的Start方法启动线程并调用了PrintEven方法。
14.4 ParameterizedThreadStart:创建进程
创建ParameterizedThreadStart委托的实例,然后再创建 Thread 类的实例。
ParameterizedThreadStart pts=new ParameterizedThreadStart( 方法名 );
Thread t=new Thread(pts);
下面的例子使用ParameterizedThreadStart委托打印0-n中的所有偶数。
using System;
using System.Threading;
class Program
{
static void Main(string[] args)
{
ParameterizedThreadStart pts = new ParameterizedThreadStart(PrintEven);
Thread t = new Thread(pts);
t.Start(9);
}
//打印0~n中的偶数
private static void PrintEven(Object n)
{
for (int i = 0; i <= (int)n; i = i + 2)
{
Console.WriteLine(i);
}
}
}
// 0
// 2
// 4
// 6
// 8
在使用ParameterizedThreadStart委托调用带参数的方法时,方法中的参数只能是object类型并且只能含有一个参数,在启动线程时要在线程的Start()方法中为委托的方法传递参数。
传递多个参数可以通过类的实例来实现。
using System;
using System.Threading;
class Program
{
static void Main(string[] args)
{
ParameterizedThreadStart pts = new ParameterizedThreadStart(PrintEven);
ParameterTest pt = new ParameterTest(1, 10);
Thread t = new Thread(pts);
t.Start(pt);
}
private static void PrintEven(Object n)
{
//判断n是否为ParameterTest 类的对象
if (n is ParameterTest)
{
int beginNum = ((ParameterTest)n).beginNum;
int endNum = ((ParameterTest)n).endNum;
for (int i = beginNum; i <= endNum; i++)
{
if (i % 2 == 0)
{
Console.WriteLine(i);
}
}
}
}
}
public class ParameterTest
{
public int beginNum;
public int endNum;
public ParameterTest(int a, int b)
{
this.beginNum = a;
this.endNum = b;
}
}
// 2
// 4
// 6
// 8
// 10
通过给ParameterTest类中的字段赋值,并将其通过线程的Start方法传递给委托引用的方法PrintEven,即可实现在委托引用的方法中传递多个参数的操作。
14.5 Priority:多线程优先级设置
如果需要控制输出值的顺序,可以通过对线程优先级的设置以及线程调度来实现。线程的优先级使用线程的Priority属性设置即可,默认的优先级是Normal。在设置优先级后,优先级高的线程将优先执行。
优先级的值通过ThreadPriority枚举类型来设置,从低到高分别为Lowest、BelowNormal、Normal、AboveNormal、Highest。
14.5.1 实例1:设置线程优先级
using System;
using System.Threading;
class Program
{
static void Main(string[] args)
{
ThreadStart ts1 = new ThreadStart(PrintEven);
Thread t1 = new Thread(ts1);
//设置打印偶数线程的优先级
t1.Priority = ThreadPriority.Lowest;
ThreadStart ts2 = new ThreadStart(PrintOdd);
Thread t2 = new Thread(ts2);
//设置打印奇数线程的优先级
t2.Priority = ThreadPriority.Highest;
t1.Start();
t2.Start();
}
//打印1~100中的奇数
public static void PrintOdd()
{
for (int i = 1; i <= 100; i = i + 2)
{
Console.WriteLine(i);
}
}
//打印0~100中的偶数
public static void PrintEven()
{
for (int i = 0; i <= 100; i = i + 2)
{
Console.WriteLine(i);
}
}
}
由于输岀奇数的线程的优先级高于输出偶数的线程,所以在输出结果中优先输出奇数。此外,每次输出的结果也不是固定的。通过优先级是不能控制线程中的先后执行顺序的,只能是优先级高的线程优先执行的次数多而已。
线程状态控制的方法包括暂停线程 (Sleep)、中断线程 (Interrupt)、挂起线程 (Suspend)、唤醒线程 (Resume)、终止线程 (Abort)。
14.5.2 实例2:线程交替
下面的例子使用暂停线程 (Sleep) 的方法让打印奇数和打印偶数的线程交替执行。
using System;
using System.Threading;
class Program
{
static void Main(string[] args)
{
ThreadStart ts1 = new ThreadStart(PrintOdd);
Thread t1 = new Thread(ts1);
ThreadStart ts2 = new ThreadStart(PrintEven);
Thread t2 = new Thread(ts2);
t1.Start();
t2.Start();
}
//打印1~100中的奇数
public static void PrintOdd()
{
for (int i = 1; i <= 10; i = i + 2)
{
//让线程休眠 1 秒
Thread.Sleep(1000);
Console.WriteLine(i);
}
}
//打印0~100中的偶数
public static void PrintEven()
{
for (int i = 0; i <= 10; i = i + 2)
{
Console.WriteLine(i);
//让线程休眠 1 秒
Thread.Sleep(1000);
}
}
}
// 0
// 1
// 2
// 3
// 4
// 5
// 6
// 7
// 8
// 9
// 10
通过Sleep方法能控制两个线程执行的先后顺序。两个线程虽然交替执行,但每次运行该程序的效果依然是不同的。
14.5.3 实例3:终止线程
using System;
using System.Threading;
class Program
{
private static int count = 10;
private static void GiveRedEnvelop()
{
while (count > 0)
{
count--;
if (count == 4)
{
//终止当前线程
Console.WriteLine("红包暂停发放!");
Thread.CurrentThread.Abort();
}
Console.WriteLine("剩余 {0} 个红包", count);
}
}
static void Main(string[] args)
{
ThreadStart ts = new ThreadStart(GiveRedEnvelop);
Thread t = new Thread(ts);
t.Start();
}
}
Unhandled exception. System.PlatformNotSupportedException: Thread abort is not supported on this platform.
报错原因:Thread.Abort
API已过时。 如果调用这些方法,则面向.NET 5或更高版本的项目将遇到编译时警告SYSLIB0006。
目前,由于挂起线程 (Suspend) 和唤醒线程 (Resume) 的操作很容易造成线程的死锁状态,已经被弃用了,而是使用标识字段来设置线程挂起和唤醒的状态。所谓线程死锁就是多个线程之间处于相互等待的状态。
线程分为前台线程和后台线程,前台线程不用等主程序结束,后台线程则需要应用程序运行结束后才能结束。此外,在应用程序运行结束后,后台线程即使没有运行完也会结束,前台线程必须等待自身线程运行结束后才会结束。
14.5.4 实例4:判断后台进程
在上一实例的基础上判断发红包的线程是否为后台线程,如果不是后台线程,将其设置为后台线程。使用Thread对象的IsBackground属性来判断线程是否为后台线程。
using System;
using System.Threading;
class Program
{
private static int count = 10;
private static void GiveRedEnvelop()
{
while (count > 0)
{
count--;
if (count == 4)
{
//终止当前线程
Console.WriteLine("红包暂停发放!");
Thread.CurrentThread.Abort();
}
Console.WriteLine("剩余 {0} 个红包", count);
}
}
static void Main(string[] args)
{
ThreadStart ts = new ThreadStart(GiveRedEnvelop);
Thread t = new Thread(ts);
t.Start();
if (t.IsBackground == false)
{
Console.WriteLine("该线程不是后台线程!");
t.IsBackground = true;
}
else
{
Console.WriteLine("该线程是后台线程!");
}
}
}
// 该线程不是后台线程!
// 剩余 9 个红包
运行该程序,直接输出“该线程不是后台线程!”,由于将该线程设置为后台线程,则不会输出红包发放的信息。
14.6 lock:线程加锁
虽然Sleep方法能控制线程的暂停时间,从而改变多个线程之间的先后顺序,但每次调用线程的结果是随机的。线程同步的方法是将线程资源共享,允许控制每次执行一个线程,并交替执行每个线程。实现线程同步可以使用lock关键字和Monitor类、Mutex类来解决。
对于线程同步操作最简单的一种方式就是使用lock关键字,通过lock关键字能保证加锁的线程只有在执行完成后才能执行其他线程。
// 参数通常是一个Object类型的值,也可以使用this关键字来表示。
lock(object)
{
//临界区代码
}
最好是在lock中使用私有的非静态或负变量或私有的静态成员变量,即使用Private
或Private static
修饰的成员。
private Object obj = new Object();
lock (obj)
{
//临界区代码
}
下面的例子使用lock
关键字控制打印奇数和偶数的线程,要求先执行奇数线程,再执行偶数线程。
using System;
using System.Threading;
class Program
{
public void PrintEven()
{
lock (this)
{
for (int i = 0; i <= 10; i = i + 2)
{
Console.WriteLine(Thread.CurrentThread.Name + "--" + i);
}
}
}
public void PrintOdd()
{
lock (this)
{
for (int i = 1; i <= 10; i = i + 2)
{
Console.WriteLine(Thread.CurrentThread.Name + "--" + i);
}
}
}
static void Main(string[] args)
{
Program program = new Program();
ThreadStart ts1 = new ThreadStart(program.PrintOdd);
Thread t1 = new Thread(ts1);
t1.Name = "打印奇数的线程";
t1.Start();
ThreadStart ts2 = new ThreadStart(program.PrintEven);
Thread t2 = new Thread(ts2);
t2.Name = "打印偶数的线程";
t2.Start();
}
}
当打印奇数的线程结束后才执行打印偶数的线程,并且每次打印的效果是一样的。
14.7 Monitor:锁定资源
Monitor类的命名空间是System.Threading。
Monitor.Enter(object);
try
{
//临界区代码
}
finally
{
Monitor.Exit(object);
}
将14.6节例子中的lock关键字替换成Monitor类。
using System;
using System.Threading;
class Program
{
public void PrintEven()
{
Monitor.Enter(this);
try
{
for (int i = 0; i <= 10; i = i + 2)
{
Console.WriteLine(Thread.CurrentThread.Name + "--" + i);
}
}
finally
{
Monitor.Exit(this);
}
}
public void PrintOdd()
{
Monitor.Enter(this);
try
{
for (int i = 1; i <= 10; i = i + 2)
{
Console.WriteLine(Thread.CurrentThread.Name + "--" + i);
}
}
finally
{
Monitor.Exit(this);
}
}
static void Main(string[] args)
{
Program program = new Program();
ThreadStart ts1 = new ThreadStart(program.PrintOdd);
Thread t1 = new Thread(ts1);
t1.Name = "打印奇数的线程";
t1.Start();
ThreadStart ts2 = new ThreadStart(program.PrintEven);
Thread t2 = new Thread(ts2);
t2.Name = "打印偶数的线程";
t2.Start();
}
}
Monitor类的用法虽然比lock关键字复杂,但其能添加等待获得锁定的超时值,这样就不会无限期等待获得对象锁。使用TryEnter()
方法可以给它传送一个超时值,决定等待获得对象锁的最长时间。该方法能在指定的毫秒数内结束线程,这样能避免线程之间的死锁现象。
Monitor.TryEnter(object, 毫秒数 );
此外,还能使用Monitor类中的Wait()
方法让线程等待一定的时间,使用Pulse()
方法通知处于等待状态的线程。
14.8 Mutex:(互斥锁)线程同步
Mutex类也是用于线程同步操作的类,例如,当多个线程同时访问一个资源时保证一次只能有一个线程访问资源。
在Mutex类中,WaitOne()方法用于等待资源被释放, ReleaseMutex()方法用于释放资源。WaitOne()方法在等待ReleaseMutex()方法执行后才会结束。
下面的例子使用线程互斥实现每个车位每次只能停一辆车的功能。
using System;
using System.Threading;
class Program
{
private static Mutex mutex = new Mutex();
public static void PakingSpace(object num)
{
if (mutex.WaitOne())
{
try
{
Console.WriteLine("车牌号{0}的车驶入!", num);
Thread.Sleep(1000);
}
finally
{
Console.WriteLine("车牌号{0}的车离开!", num);
mutex.ReleaseMutex();
}
}
}
static void Main(string[] args)
{
ParameterizedThreadStart ts = new ParameterizedThreadStart(PakingSpace);
Thread t1 = new Thread(ts);
t1.Start("冀A12345");
Thread t2 = new Thread(ts);
t2.Start("京A00000");
}
}
从上面的运行效果可以看出,每辆车驶入并离开后其他车才能占用停车位,即当一个线程占用资源时,其他线程是不使用该资源的。