线程基础必知必会(一)

时间:2022-07-24
本文章向大家介绍线程基础必知必会(一),主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

从这篇文章开始,我将利用两篇文章讲解线程的基础知识,本篇文章涉及到了 创建线程线程等待线程暂停线程终止线程状态检测 相关的内容。这篇文章及其下一篇文章是这个专题的基础中的基础,因此我会用简单易懂的语言和示例代码来讲解,以求您在阅读完文章后能为后续线程其他只是打下坚实的基础。学习这张篇文章你需要具备 C# 语言基础和 Microsoft Visual Studio 2015 及以上任何版本。 所谓的线程,就是操作系统利用某种方式将计算单元分割成大量的虚拟进程,然后赋予这些虚拟进程一定的计算能力。这里需要注意,因为创建和使用多线程是一个消耗大量操作系统资源的过程,因此当只有一个单核处理器时多线程会导致操作系统忙于管理这些线程,进而无法运行程序甚至有时操作系统本身也会无法正常运行(即使操作系统访问处理器的优先级最高,也依然会出现这种问题)。因此目前主流的处理器都是多核心处理器,并且计算能力也是相当的高,但是我们不能因为硬件提高了而忽略软件的发展,目前主流的开发语言都支持多线程处理。废话不多说现在我们开始线程基础的第一篇。

一、创建线程

创建线程的方法很简单,我们只需要实例化 Thread 即可,在实例化的过程中我们将要在新线程中运行的方法传递给 Thread ,然后调用 start 方法运行新建的线程。下面我们先看一下创建线程的代码。

using System;
using System.Threading;

namespace CreateThread
{
    class Program
    {
        static void Main(string[] args)
        {
            Thread thread = new Thread(PrintNumber);
            thread.Start();
            PrintNumber();
            Console.Read();
        }
        static void PrintNumber()
        {
            for (int i = 0; i < 20; i++)
            {
                Console.WriteLine($"第 {i + 1} 个数字是 {i}");
            }
        }
    }
}

上述代码的第二行利用 using 引入了 Thread 所在的命名空间 System.Threading ,之后我们在 Program 类里创建了一个静态方法 PrintNumber ,在这个方法中我们编写了一个循环,通过循环在控制台打印出二十个数字。接着我们又在 Main 方法中实例化了 Thread ,并将前面我们定义的静态方法 PrintNumber 作为参数传递给了 Thread ,这里需要注意的是我们向 Thread 传递的是 ThreadStart 类型的参数,ThreadStart 是个委托表示应该在线程上执行的方法,此委托传递给 Thread 构造函数,直到调用Thread.Start方法,才调用此方法。最后我们调用 Thread 的 Start 方法来运行新创建的线程。代码的第十二行我们以普通方式调用了 PrintNumber 方法,这时为了做比较。这时我们运行代码,会看到两次调用 PrintNumber 方法的输出内容会随机交叉显示。

Tip:

  1. 当我们实例化 Thread 时,ThreadStart 或者 ParameterizedThreadStart 的实例委托会传给构造函数。我们只需指定在不同线程运行的方法名,C#编译器则会在后台创建这些对象。
  2. 线程位于进程中,一个进程包含至少一个线程,并且一个进程中始终有一个主线程在执行任务。

二、线程等待

当程序需要使用另一个线程的结果时我们就需要用到 Join 方法,Join 方法的作用是阻止调用线程的运行,让调用线程等待被调用线程(子线程)运行完成后在运行。简单说就是 Join 方法堵塞当前调用子线程成的方法,直到子线程执行完毕。我们可以利用这个方法实现在两个线程间同步执行步骤。下面我们通过代码来看一下 Join 方法的用法。

using System;
using System.Threading;

namespace ThreadWait
{
    class Program
    {
        static int count = 0;
        static void Main(string[] args)
        {
            Thread thread = new Thread(PrintNumber);
            thread.Start();
            thread.Join();
            PrintNumber();
            Console.WriteLine(count);
            Console.Read();
        }
        static void PrintNumber()
        {
            for (int i = 0; i < 20; i++)
            {
               count+=i;
            }
        }
    }
}

上述代码中的第十三行我们调用了 Join 方法来让主线程等待,因为我们需要用到 thread 线程的计算结果,当 thread 线程运行完毕后主线程将会继续运行,主线程会以普通方式调用 PrintNumber 方法,这时 PrintNumber 中的 count 值已经不是初始化的 0 了,而是通过 thread 线程执行后的结果 190 ,经过 PrintNumber 的再次计算,最终输出的结果是 380 。如果我们不使用 Join 方法的话输出结果就不得而知了。

三、线程暂停

比如当我们需要停止 Windows 服务或者 Kill 进程(不限于这两种情况)时,服务或进程中存在退出响应逻辑,这时我们不能马上就执行主程序后续的代码,需要等待服务或进程完全退出后方能执行后续代码。但是又因为被停止的服务或者被 Kill 的进程并不是当前程序的进程,因此我们无法通过 Join 方法来让主线程等待,这时我们就可以用到 Sleep 方法来让主线程停止一段时间后再运行后续代码(这种方法并不是最好的方法,这里我们只是用它来做个讲解)。 Sleep 方法有两个重载,一个是传入 int 类型的参数,参数的单位时毫秒,表示线程暂停时长。另一个重载是传入 TimeSpan 类型的参数,参数表示挂起线程的时间量。下面的代码就是模拟了 Kill 掉进城后暂停一定时长。

using System;
using System.Diagnostics;
using System.Threading;

namespace ThreadPause
{
    class Program
    {
        static void Main(string[] args)
        {
            Thread thread = new Thread(KillProcess);
            thread.Start();
            Console.ReadLine();
        }

        static void KillProcess()
        {
            Process process = Process.GetProcessesByName("notepad")[0];
            process.Kill();
            Thread.Sleep(20);
            if (Process.GetProcessesByName("notepad").Length == 0)
            {
                Console.WriteLine("已被Kill");
            }
        }
    }
}

上面只是模拟,在真是项目中回避这个要复杂。这里需要注意的是有时我们会在代码中看到这样的写法 Thread.Sleep(0),这种写法并不是暂停 0 毫秒的意思,其根本意思是当参数值为 0 ,则该线程会将其时间片的剩余部分让给任何已经准备好运行的、具有同等优先级的线程。如果没有其他已经准备好运行的、具有同等优先级的线程,则不会挂起当前线程的执行。

Tip:

  1. 线程处于休眠状态时,它会占用尽可能少的CPU时间。

四、线程终止

线程终止在实际开发中用的比较少,只有在极特殊的情况下使用到,根据我项目开发的经验来看,我还没有遇到过需要用到线程终止的情况,下面我们先来看一下代码。

using System;
using System.Threading;

namespace ThreadStop
{
    class Program
    {
        static void Main(string[] args)
        {
            Thread thread = new Thread(PrintNumber);
            thread.Start();
            Thread.Sleep(200);
            thread.Abort();
            Console.Read();
        }
        static void PrintNumber()
        {
            for (int i = 0; i < 1000; i++)
            {
                Console.WriteLine($"第 {i + 1} 个数字是 {i}");
            }
        }

    }
}

上述代码在线程开始运行 200 毫秒后调用 Abort 方法来终止线程继续执行,我们从下图中可以看到线程中的循环输出并没用完全执行完毕,因为线程被我们终止掉了。调用 Abort 方法相当于给线程注入了 ThreadAbortException 方法导致线程被终结。但是在这里需要提醒大家的是这么做对于程序来说是相当危险的,因为它引入了一个异常这个异常可以轻而易举的摧毁你的应用程序。并且需要被终止的线程可以通过处理这个一场并且调用 Thread.ResetAbort 方法来拒绝被终止。关于解决这个问题的方法我将在后面的文章中讲解。

五、线程状态检测

线程状态检测在很多时候都会用到,目前 C# 中线程的状态有十种,这十种状态见下表。常用的状态有 RunningUnstartedStoppedWaitSleepJoin

状态

说明

Running

线程已启动

StopRequested

正在请求线程停止

SuspendRequested

正在请求线程挂起

Background

线程正作为后台线程执行

Unstarted

线程未启动

Stopped

线程已停止

WaitSleepJoin

线程已被阻止

Suspended

线程已挂起

AbortRequested

线程正在停止

Aborted

线程已被终止,但状态还不是Stopped

线程的状态首先是 Unstarted 因为这个时候线程并没有启动,当线程启动时状态就变为了 Running ,当我们调用 Sleep 或者 Join 方法时线程状态就变成了 WaitSleepJoin 。终止线程后线程状态为 Aborted ,但是也有可能是 AbortRequested 。当线程执行完毕后状态将是 Stopped

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;

namespace ThreadState
{
    class Program
    {
        static void Main(string[] args)
        {
            Thread thread = new Thread(PrintNumber);
            Console.WriteLine("线程状态:" + thread.ThreadState);
            thread.Start();
            for (int i = 0; i < 20; i++)
            {
                Console.WriteLine("线程状态:" + thread.ThreadState);
            }
        }

        static void PrintNumber()
        {
            for (int i = 0; i < 1000; i++)
            {
                Thread.Sleep(2000);
            }
        }
    }
}

六、作业

  1. 新建多个线程,分别终止和暂停几个进程,然后查看他们的状态。

七、源码下载

本届源码下载地址:

  1. 地址一: https://github.com/Thomas-Zhu/Multithreading/tree/master/no1/NoOne
  2. 地址二:https://gitee.com/bugback_admin/Multithreading/tree/master/no1/NoOne