线程

线程是可以与其他线程并发运行并共享数据的任务。当程序启动时,它会为程序的入口点(通常是 Main 函数)创建一个线程。所以,你可以把“程序”看作是由线程组成的。.NET Framework 允许你在程序中使用线程来并行运行代码。这通常出于两个原因:

  • 如果执行图形用户界面的线程正在执行耗时的工作,程序可能会显得没有响应。通过使用线程,你可以创建一个新的线程来执行任务,并将其进度报告给 GUI 线程。
  • 在具有多个 CPU 或多核 CPU 的计算机上,线程可以最大化计算资源的使用,从而加速任务执行。

Thread 类

System.Threading.Thread 类提供了使用线程的基本功能。要创建一个线程,你只需创建一个 Thread 类的实例,并使用 ThreadStartParameterizedThreadStart 委托指向线程应该开始运行的代码。例如:

using System;
using System.Threading;

public static class Program
{
    private static void SecondThreadFunction()
    {
        while (true)
        {
            Console.WriteLine("Second thread says hello.");
            Thread.Sleep(1000); // 暂停当前线程的执行1秒(1000毫秒)
        }
    }
    
    public static void Main()
    {
        Thread newThread = new Thread(new ThreadStart(SecondThreadFunction));
        
        newThread.Start();
        
        while (true)
        {
            Console.WriteLine("First thread says hello.");
            Thread.Sleep(500); // 暂停当前线程的执行半秒(500毫秒)
        }
    }
}

你应该看到如下输出:

Second thread says hello.
First thread says hello.
First thread says hello.
Second thread says hello.
First thread says hello.
First thread says hello.
...

注意,在代码中需要使用 while 关键字,因为一旦函数返回,线程就会退出或终止。

ParameterizedThreadStart

void ParameterizedThreadStart(object obj) 委托允许你向新线程传递参数:

using System;
using System.Threading;

public static class Program
{
    private static void SecondThreadFunction(object param)
    {
        while (true)
        {
            Console.WriteLine("Second thread says " + param.ToString() + ".");
            Thread.Sleep(500); // 暂停当前线程的执行半秒(500毫秒)
        }
    }
    
    public static void Main()
    {
        Thread newThread = new Thread(new ParameterizedThreadStart(SecondThreadFunction));
        
        newThread.Start(1234); // 在此传递参数给新线程
        
        while (true)
        {
            Console.WriteLine("First thread says hello.");
            Thread.Sleep(1000); // 暂停当前线程的执行1秒(1000毫秒)
        }
    }
}

输出为:

First thread says hello.
Second thread says 1234.
Second thread says 1234.
First thread says hello.
...

共享数据

虽然我们可以使用 ParameterizedThreadStart 来向线程传递参数,但它并不类型安全,并且使用起来比较笨拙。我们可以利用匿名委托在多个线程之间共享数据:

using System;
using System.Threading;

public static class Program
{
    public static void Main()
    {
        int number = 1;
        Thread newThread = new Thread(new ThreadStart(delegate
        {
            while (true)
            {
                number++;
                Console.WriteLine("Second thread says " + number.ToString() + ".");
                Thread.Sleep(1000);
            }
        }));
        
        newThread.Start();
        
        while (true)
        {
            number++;
            Console.WriteLine("First thread says " + number.ToString() + ".");
            Thread.Sleep(1000);
        }
    }
}

注意,匿名委托的主体能够访问局部变量 number

异步委托

使用匿名委托可能会导致大量语法、作用域混乱以及缺乏封装性的问题。然而,通过使用 lambda 表达式,可以缓解一些这些问题。你可以使用异步委托来传递和返回数据,这样做是类型安全的。值得注意的是,当使用异步委托时,你实际上是在将新线程排入线程池。另外,使用异步委托要求你采用异步模型。

using System;

public static class Program
{
     delegate int del(int[] data);
     
     public static int SumOfNumbers(int[] data)
     {
          int sum = 0;
          foreach (int number in data) {
               sum += number;
          }

          return sum;
     }

     public static void Main()
     {
          int[] numbers = new int[] { 1, 2, 3, 4, 5 };
          del func = SumOfNumbers;
          IAsyncResult result = func.BeginInvoke(numbers, null, null);
          
          // 可以在此做其他事情,同时 numbers 正在被求和
          
          int sum = func.EndInvoke(result);
          sum = 15;
     }
}

同步

在共享数据的示例中,你可能已经注意到,通常(如果不是总是),你会看到如下输出:

First thread says 2.
Second thread says 3.
Second thread says 5.
First thread says 4.
Second thread says 7.
First thread says 7.

人们通常会期望数字按升序打印!这个问题的根本原因是这两段代码在同一时间运行。例如,它先打印了 3、5,然后是 4。我们来分析可能发生的情况:

  • 在 "First thread says 2" 打印后,第一线程将 number 增加,变为 3,并打印。
  • 第二线程然后将 number 增加,变为 4。
  • 就在第二线程有机会打印 number 之前,第一线程将 number 增加,变为 5,并打印。
  • 第二线程然后打印了在第一线程增加之前的 number,即 4。请注意,这可能是由于控制台输出缓冲的缘故。

这个问题的解决方案是同步两个线程,确保它们的代码不会像之前那样交叉执行。C# 通过 lock 关键字来支持这一点。我们可以将代码块放在这个关键字下:

using System;
using System.Threading;

public static class Program
{
    public static void Main()
    {
        int number = 1;
        object numberLock = new object();
        Thread newThread = new Thread(new ThreadStart(delegate
        {
            while (true)
            {
                lock (numberLock)
                {
                    number++;
                    Console.WriteLine("Second thread says " + number.ToString() + ".");
                }

                Thread.Sleep(1000);
            }
        }));
        
        newThread.Start();
        
        while (true)
        {
            lock (numberLock)
            {
                number++;
                Console.WriteLine("First thread says " + number.ToString() + ".");
            }

            Thread.Sleep(1000);
        }
    }
}

numberLock 变量是必需的,因为 lock 关键字仅作用于引用类型,而不适用于值类型。这一次,你将得到正确的输出:

First thread says 2.
Second thread says 3.
Second thread says 4.
First thread says 5.
Second thread says 6.
...

lock 关键字通过尝试获取对传递给它的对象(numberLock)的独占锁来工作。只有当代码块执行完毕(即 } 后),锁才会被释放。如果一个对象已经被锁定,当另一个线程尝试获取该对象的锁时,线程会被阻塞(暂停执行),直到锁被释放,并且线程能够成功获得锁。这样可以防止代码段交叉执行。

Thread.Join()

Thread 类的 Join 方法允许一个线程等待另一个线程,且可以选择指定超时时间:

using System;
using System.Threading;

public static class Program
{
    public static void Main()
    {
        Thread newThread = new Thread(new ThreadStart(delegate
        {
            Console.WriteLine("Second thread reporting.");
            Thread.Sleep(5000);
            Console.WriteLine("Second thread done sleeping.");
        }));

        newThread.Start();
        Console.WriteLine("Just started second thread.");
        newThread.Join(1000);
        Console.WriteLine("First thread waited for 1 second.");
        newThread.Join();
        Console.WriteLine("First thread finished waiting for second thread. Press any key.");
        Console.ReadKey();
    }
}

输出为:

Just started second thread.
Second thread reporting.
First thread waited for 1 second.
Second thread done sleeping.
First thread finished waiting for second thread. Press any key.
Last modified: Sunday, 12 January 2025, 12:39 AM