C#进程与线程

在操作系统中,每运行一个程序都会开启一个进程,一个进程由多个线程构成。

线程是程序执行流中最小的单元。

在应用程序中分为单线程程序和多线程程序。
• 单线程程序是指在一个进程空间中只有一个线程在执行;
• 多线程程序是指在一个进程空间中有多个线程在执行,并共享同一个进程的大小。

进程指在每个操作系统中自动启动的系统进程和一些自动启动的应用程序进程,在 Windows 操作系统中提供了任务管理器来查看当前启动的进程,并能关闭指定的进程。

文章目录

      • C# Process:进程类
        • Process 简介
        • 操作进程
      • C# Thread:线程及与线程有关的类
      • C# ThreadStart:创建线程
      • C# ParameterizedThreadStart:创建进程
      • C# Priority:多线程优先级设置
      • C# lock:给线程加锁,保证线程同步
      • C# Monitor:锁定资源
      • C# Mutex:(互斥锁)线程同步

C# Process:进程类

在 C# 语言中进程类是指 Process 类,该类所在的命名空间是 System.Diagnostics。

Process 简介

Process 类主要提供对本地和远程进程的访问,并提供对本地进程的启动、停止等操作。

Process 类的常用属性和方法如下表所示。

属性或方法 说明
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 组件关联

操作进程

在实际应用中经常会用到获取本地的进程、启动进程、关闭进程等操作,下面分别以实例形式介绍其具体的操作方法。

在获取当前操作系统中运行的进程时,如果要获取所有运行的进程的信息可以使用上表中的 GetProcesses() 方法,如果要获取指定名称的进程可以使用 GetProcessesByName(String) 方法。

【实例 1】创建 Windows 应用程序,在 RichTextBox 控件中显示所有当前系统中正在运行的进程。

//在“查看所有进程”按钮的单击事件中添加查看所有进程的代码,具体如下。
public partial class GetProcessesForm : Form
{
    public GetProcessesForm()
    {
        InitializeComponent();
    }
    //“查看所有进程”按钮的单击事件
    private void btnOk_Click(object sender, EventArgs e)
    {
        Process[] processes = Process.GetProcesses();
        foreach(Process p in processes)
        {
            richTextBox1.Text = richTextBox1.Text + p.ProcessName + "\r\n";
        }
    }
}

已经将系统中运行的进程名称显示在 RichTextBox 中,由于在当前系统中运行的进程较多,所以需要滑动 RichTextBox 控件中的滚动条来查看。

【实例 2】创建 Windows 应用程序,并在文本框中输入需要启动的进程名称,单击“启动进程”按钮启动该进程

public partial class ProcessForm : Form
{
    public ProcessForm()
    {
        InitializeComponent();
    }
    //“启动进程”按钮的单击事件
    private void button1_Click(object sender, EventArgs e)
    {
        //获取进程名称
        string ProcessName = textBox1.Text;
        //创建Process 类的对象
        Process p = new Process();
        //设置进程名称
        p.StartInfo.FileName = ProcessName;
        //启动进程
        p.Start();
    }
}

【实例 3】创建 Windows 应用程序,在 ListBox 控件中显示所有的进程名称,并右击选中的进程名称,通过弹出的右键菜单将其关闭。

public partial class ListBoxForm : Form
{
    public ListBoxForm()
    {
        InitializeComponent();
    }
    //窗体加载事件
    private void ListBoxForm_Load(object sender, EventArgs e)
    {
        //获取所有进程信息
        Process[] processes = Process.GetProcesses();
        foreach(Process p in processes)
        {
            //将进程添加到ListBox中
            lbProcess.Items.Add(p.ProcessName);
        }
    }
    //"停止进程"命令的单击事件
    private void 停止进程ToolStripMenuItem_Click(object sender, EventArgs e)
    {
        //获取进程名称
        string ProcessName = lbProcess.SelectedItem.ToString();
        //根据进程名称获取进程
        Process[] processes = Process.GetProcessesByName(ProcessName);
        //判断是否存在指定进程名称的进程
        if (processes.Length > 0)
        {
            try
            {
                foreach(Process p in processes)
                {
                    //判断进程是否处于运行状态
                    if (!p.HasExited)
                    {
                        //关闭进程
                        p.Kill();
                        MessageBox.Show(p.ProcessName + "已关闭!");
                        //获取所有进程信息
                        processes = Process.GetProcesses();
                        //清空ListBox中的项
                        lbProcess.Items.Clear();
                        foreach(Process p1 in processes)
                        {
                            //将进程添加到ListBox中
                            lbProcess.Items.Add(p1.ProcessName);
                        }
                    }
                }
            }
            catch
            {
                MessageBox.Show("该进程无法关闭!");
            }
        }
    }
}

C# Thread:线程及与线程有关的类

在 C# 语言中线程(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() 方法,终止线程

C# ThreadStart:创建线程

在 C# 语言中使用线程时首先需要创建线程,在使用 Thread 类的构造方法创建其实例时,需要用到 ThreadStart 委托或者 ParameterizedThreadStart 委托创建 Thread 类的实例。

ThreadStart 委托只能用于无返回值、无参数的方法,ParameterizedThreadStart 委托则可以用于带参数的方法。

本节主要介绍使用 ThreadStart 委托创建 Thread 类的实例,关于 ParameterizedThreadStart 委托我们将在下一节《C# ParameterizedThreadStart》中为大家讲解。

使用 ThreadStart 创建线程首先需要创建 ThreadStart 委托的实例,然后再创建 Thread 类的实例。

具体的代码如下。

ThreadStart ts = new ThreadStart( 方法名 );
Thread t = new Thread(ts);

【实例 1】使用 ThreadStart 委托创建线程,并定义一个方法输出 0〜10 中所有的偶数。

class Program
{
    static void Main(string[] args)
    {
        ThreadStart ts = new ThreadStart(PrintEven);
        Thread t = new Thread(ts);
        t.Start();
    }
    //定义打印0~10中的偶数的方法
    private static void PrintEven()
    {
        for(int i = 0; i <= 10; i=i+2)
        {
            Console.WriteLine(i);
        }
    }
}

//使用 Threadstart 委托为 PrintEven 方法创建了线程,通过线程的 Start 方法启动线程并调用了 PrintEven 方法。

在一个应用程序中能同时启动多个线程,下面通过实例演示启动多个线程的效果。

【实例 2】在上一实例的基础上添加一个打印 1〜10 中的奇数的方法,再分别使用两个 Thread 类的实例启动打印奇数和偶数的方法。

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()
    {
        for(int i = 0; i <= 10; i=i+2)
        {
            Console.WriteLine(i);
        }
    }
    //定义打印1~10 中的奇数的方法
    public static void PrintOdd()
    {
        for(int i = 1; i <= 10; i = i + 2)
        {
            Console.WriteLine(i);
        }
    }
}

//两个线程分别打印了 1〜10 中的奇数和 0〜10 中的偶数,但并不是按照线程的调用顺序先打印出所有的偶数再打印奇数。

 //需要注意的是,由于没有对线程的执行顺序和操作做控制,所以运行该程序每次打印的值的顺序是不一样的。

C# ParameterizedThreadStart:创建进程

在 C# 语言中使用 ParameterizedThreadStart 创建进程,首先需要创建 ParameterizedThreadStart 委托的实例,然后再创建 Thread 类的实例。

具体的代码如下。

ParameterizedThreadStart pts=new ParameterizedThreadStart( 方法名 );
Thread t=new Thread(pts);

【实例 1】创建一个方法输出0〜n的所有偶数,使用 ParameterizedThreadStart 委托调用该方法,并启动打印偶数的线程。

class Program
{
    static void Main(string[] args)
    {
        ParameterizedThreadStart pts = new ParameterizedThreadStart(PrintEven);
        Thread t = new Thread(pts);
        t.Start(10);
    }
    //打印0~n中的偶数
    private static void PrintEven(Object n)
    {
        for(int i=0;i<=(int)n; i = i + 2)
        {
            Console.WriteLine(i);
        }
    }
}

在使用 ParameterizedThreadStart 委托调用带参数的方法时,方法中的参数只能是 object 类型并且只能含有一个参数。

在启动线程时要在线程的 Start() 方法中为委托的方法传递参数。

如果需要通过 ParameterizedThreadStart 委托引用多个参数的方法,由于委托方法中的参数是 object 类型的,传递多个参数可以通过类的实例来实现。

下面通过实例来演示使用 ParameterizedThreadStart 委托引用多个参数的方法。

【实例 2】创建一个方法输出指定范围内数值的偶数,并创建线程调用该方法。

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;
    }
}

通过为 ParameterTest 类中的字段赋值,并将其通过线程的 Start 方法传递给委托引用的方法 PrintEven,即可实现在委托引用的方法中传递多个参数的操作。

C# Priority:多线程优先级设置

在《C# ThreadStart》一节中我们通过两个线程分别打印奇数和偶数,但是每次打印出来的结果是不同的。

如果需要控制输出值的顺序,可以通过对线程优先级的设置以及线程调度来实现。

在 C# 中线程的优先级使用线程的 Priority 属性设置即可,默认的优先级是 Normal。

在设置优先级后,优先级高的线程将优先执行。

优先级的值通过 ThreadPriority 枚举类型来设置,从低到高分别为Lowest、BelowNormal、Normal、AboveNormal、Highest。

【实例 1】通过设置线程的优先级来控制输出奇数和偶数的线程,为了看出设置线程优先级的效果将输出 1〜100 中的奇数和 0〜100 中的偶数。

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)。

【实例 2】使用暂停线程 (Sleep) 的方法让打印奇数和打印偶数的线程交替执行,即打印 0〜10 的数。

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);
        }
    }
}

通过 Sleep 方法能控制两个线程执行的先后顺序。

需要注意的是,两个线程虽然交替执行,但每次运行该程序的效果依然是不同的。

【实例 3】模拟发放 10 个红包,当剩余 5 个红包时线程终止。

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);
                Thread.Sleep(1000);
            }
        }
        static void Main(string[] args)
        {
            ThreadStart ts = new ThreadStart(GiveRedEnvelop);
            Thread t = new Thread(ts);
            t.Start();
        }
    }

目前,由于挂起线程 (Suspend) 和唤醒线程 (Resume) 的操作很容易造成线程的死锁状态,已经被弃用了,而是使用标识字段来设置线程挂起和唤醒的状态。

所谓线程死锁就是多个线程之间处于相互等待的状态。

线程分为前台线程和后台线程,前台线程不用等主程序结束,后台线程则需要应用程序运行结束后才能结束。

此外,在应用程序运行结束后,后台线程即使没有运行完也会结束,前台线程必须等待自身线程运行结束后才会结束。

使用 Thread 对象的 IsBackground 属性来判断线程是否为后台线程。

【实例 4】在上一实例的基础上判断发红包的线程是否为后台线程,如果不是后台线程,将其设置为后台线程。

//这里只在 Main() 方法中添加了对线程是否为后台线程的判断,
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("该线程是后台线程!");
    }
}

//直接输出“该线程不是后台线程!”,由于将该线程设置为后台线程,则不会输出红包发放的信息。

C# lock:给线程加锁,保证线程同步

虽然 Sleep 方法能控制线程的暂停时间,从而改变多个线程之间的先后顺序,但每次调用线程的结果是随机的。

线程同步的方法是将线程资源共享,允许控制每次执行一个线程,并交替执行每个线程。

在 C# 语言中实现线程同步可以使用 lock 关键字和 Monitor 类、Mutex 类来解决。

对于线程同步操作最简单的一种方式就是使用 lock 关键字,通过 lock 关键字能保证加锁的线程只有在执行完成后才能执行其他线程。虽然 Sleep 方法能控制线程的暂停时间,从而改变多个线程之间的先后顺序,但每次调用线程的结果是随机的。

lock 的语法形式如下。

lock(object)
 {
     //临界区代码
}

这里 lock 后面通常是一个 Object 类型的值,也可以使用 this 关键字来表示。

最好是在 lock 中使用私有的非静态或负变量或私有的静态成员变量,即使用 Private 或 Private static 修饰的成员。

private Object obj = new Object();
 lock (obj)
 {
     //临界区代码
}

【实例】创建控制台应用程序,使用 lock 关键字控制打印奇数和偶数的线程,要求先执行奇数线程,再执行偶数线程。

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();
    }
}

//当打印奇数的线程结束后才执行打印偶数的线程,并且每次打印的效果是一样的。

C# Monitor:锁定资源

在 C# 中 Monitor 类的命名空间是 System.Threading,它的用法要比《C# lock》一节中介绍的 lock 的用法复杂一些,但本质是一样的。

使用 Monitor 类锁定资源的代码如下。

Monitor.Enter(object);
 try
 {
     //临界区代码
}
finally
 {
     Monitor.Exit(object);
 }

在这里,object 值与 lock 中的 object 值是一样的。

简而言之,lock 的写法是 Monitor 类的一种简写。

【实例】将上一节《C# lock》实例中的 lock 关键字替换成 Monitor 类。

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() 方法可以给它传送一个超时值,决定等待获得对象锁的最长时间。

使用 TryEnter() 方法设置获得对象锁的时间的代码如下。

Monitor.TryEnter(object, 毫秒数 );

该方法能在指定的毫秒数内结束线程,这样能避免线程之间的死锁现象。

此外,还能使用 Monitor 类中的 Wait() 方法让线程等待一定的时间,使用 Pulse() 方法通知处于等待状态的线程。

C# Mutex:(互斥锁)线程同步

C# 中 Mutex 类也是用于线程同步操作的类,例如,当多个线程同时访问一个资源时保证一次只能有一个线程访问资源。

在 Mutex 类中,WaitOne() 方法用于等待资源被释放, ReleaseMutex() 方法用于释放资源。

WaitOne() 方法在等待 ReleaseMutex() 方法执行后才会结束。

【实例】使用线程互斥实现每个车位每次只能停一辆车的功能。

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");
    }
}

你可能感兴趣的:(C#)