深入了解 C# 线程以及线程池基础

682 阅读4分钟
  • 持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第11天,点击查看活动详情

什么是进程(Process)?

是系统进行资源分配和调度的基本单位。在计算机任务管理器可以看到进程一栏存在许多的应用程序和一些exe后缀文件,一个程序或者资源代表着一个进程。

什么是线程(Thread)?

是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位即一个进程可以有多个线程。

前言

我们为什么要使用线程?

  • 你的应用程序某个按钮点击响应非常慢导致用户点击之后整个程序无法操作要等线程执行完毕响应才能操作,无疑用户体验是非常差的,如果我们开一个子线程去执行响应较慢的代码,不影响主线程阻塞,这样用户体验就得到了提升。
  • 使用多线程能优化程序性能

⚠️ 进程启动时,公共语言运行时会自动创建单个前台线程来执行应用程序代码。 除了此主前台线程,进程还可以创建一个或多个线程来执行与进程关联的部分程序代码。可以使用 ThreadPool类在由公共语言运行时管理的工作线程上执行代码

启动线程

实例化一个 Thread 类,并且将委托通过构造函数传递,然后调用 Start() 开始执行

// 定义的线程节点方法
public static void ThreadNodeOne()
{
    for (int i = 0; i < 10; i++)
    {
        Console.WriteLine("ThreadProc-1: {0}", i);
    }
}
// 程序入口主方法
public static void Main()
{
    // 实例化线程类,并且将定义的节点方法通过构造函数传递
    var threadOne = new Thread(ThreadNodeOne);
    // 如果方法没有参数,则向 ThreadStart 构造函数传递委托,这里写法等价于上面的写法
    var threadOne = new Thread(new ThreadStart(ThreadNodeOne));
    // 执行线程
    threadOne.Start();
}
  • 如果方法具有参数,编译器会自动识别往 ParameterizedThreadStart 构造函数传递委托,参数传递可以使用 Start(object obj) 重载传递

image.png

public static void Main()
{
    var threadOne = new Thread(ThreadNodeOne);
    threadOne.Start("这是传递的参数!");
}

前台线程与后台线程

类的实例 Thread 表示前台线程或后台线程,两者之间的区别在前台线程都终止的情况下,公共语言运行时将结束该进程,所有剩余的后台线程将停止,并且无法完成,而后台线程不会阻止进程终止。如果要将线程设置为后台线程,只需指定 IsBackground = true 即可。

// 线程执行方法
public static void ThreadLoop(object num)
{
    for (int i = 0; i < Convert.ToInt32(num); i++)
    {
        // Thread.CurrentThread.IsBackground 用于判断当前是前台还是后台线程
        Console.WriteLine(Thread.CurrentThread.IsBackground ? $"Background Thread {i}" : $"Foreground Thread {i}");
        Thread.Sleep(500);
    }
}

public static void Main()
{
    // 启动第一个线程,不指定IsBackground则默认为前台线程
    Thread thread = new Thread(ThreadLoop);
    
    // 启动第二个线程 指定为后台线程
    Thread thread1 = new Thread(ThreadLoop);
    thread1.IsBackground = true;

    thread.Start(5);// 第一个线程循环5次
    thread1.Start(7);// 第二个线程循环7次
}

image.png

线程池

线程的创建和销毁需要耗费一定的开销,过多的使用线程反而会造成内存资源的浪费,从而影响性能。
线程池线程是后台线程, 每个线程均使用默认的堆栈大小,以默认的优先级运行,并且位于多线程单元中。
一旦线程池中的线程完成任务,它将返回到等待线程队列中,这时开始即可重用它,通过这种重复使用,应用程序可以避免产生为每个任务创建新线程的开销。

C# 使用 ThreadPool 类: 提供一个线程池,该线程池可用于执行任务、发送工作项、处理异步 I/O、代表其他线程等待以及处理计时器。

  • 设置可以同时处于活动状态的线程池的请求数目
// 设置线程池中可以并发活动的请求数。
bool pool = ThreadPool.SetMaxThreads(7,7);
  • 将方法排入队列以便执行。 此方法在有线程池线程变得可用时执行
ThreadPool.QueueUserWorkItem(o => ThreadNodeOne("参数1"));
  • 线程池操作,检索线程池按需创建的线程的最小数量

处于活动状态的线程池的请求数目不能小于最小线程池大小

// Result : 8 - 8
ThreadPool.GetMinThreads(out int workerThreads, out int completionPortThreads);
  • 设置线程池请求数目为8,8
ThreadPool.SetMaxThreads(8,8);

获取线程相关信息

可以使用 Thread.CurrentThread 检索许多提供线程相关信息的属性值。 在某些情况下,还可以设置这些属性值来控制线程的操作。

public static void ThreadNodeOne()
{
    // 获取或设置线程的名称。
    string ThreadName = Thread.CurrentThread.Name;

    // 获取当前托管线程的唯一标识符。
    int ThreadId = Thread.CurrentThread.ManagedThreadId;

    // 返回当前线程的哈希代码
    Thread.CurrentThread.GetHashCode();

    // 获取当前线程的状态
    var ThreadState = Thread.CurrentThread.ThreadState;

    // 线程是否是线程池线程
    bool IsThreadPoolThread = Thread.CurrentThread.IsThreadPoolThread;

    // 线程是否是后台线程
    bool IsBackground = Thread.CurrentThread.IsBackground;

    // 指定 Thread 的调度优先级。 枚举值
    ThreadPriority.Lowest......
}

总结

线程在编程中是一个非常重要的概念,使用好线程也变得非常重要,如果使用不当可能会造成服务器崩溃,或者线程死锁,线程安全问题,大量的使用线程对服务器内存的要求当然也就高了,这都是大家在使用多线程需要去考虑的问题。