Skip to content

异步编程与多线程

线程基础

线程(Thread)是操作系统调度的基本单位,它代表程序中一个独立的执行路径。

一个进程可以包含多个线程,多个线程共享进程的资源,如内存、文件句柄等。

线程是实现并发编程的核心技术之一,尤其适合处理复杂和耗时的任务。

线程与进程的区别

  1. 进程:进程是操作系统分配资源的基本单位,每个进程有独立的内存空间和系统资源。进程之间的通信通常比线程之间复杂。
  2. 线程:线程是进程内的执行单元,多个线程共享同一进程的资源(如内存、文件等),因此线程间的通信比进程间通信更高效。

线程(Thread)

C# 提供 Thread 类,允许开发者创建和管理线程。线程是 CPU 执行任务的基本单位。

主线程

在C#中,每个应用程序从一个主线程开始,之后可以通过创建其他线程来分担工作。

C#
using System;
using System.Threading;

namespace MultithreadingApplication
{
    class MainThreadProgram
    {
        static void Main(string[] args)
        {
            Thread th = Thread.CurrentThread;
            th.Name = "MainThread";
            Console.WriteLine("This is {0}", th.Name);
            //This is MainThread
            Console.ReadKey();
        }
    }
}

创建线程并启动

创建一个线程实例,线程将执行指定的方法或委托。

通常使用 Thread 构造函数并传递一个委托(通常是 ThreadStart 或 ParameterizedThreadStart)。

示例:

Thread.Start() 是启动线程的关键方法,一旦调用 Start(),线程将处于 Running 状态,并执行传递给它的方法。

C#
Thread myThread = new Thread(new ThreadStart(MyMethod)); // 不带参数
myThread.Start();

void MyMethod()
{
    Console.WriteLine("线程正在执行 MyMethod");
}

Thread myThreadWithParam = new Thread(new ParameterizedThreadStart(MyMethodWithParam));// 传递参数
myThreadWithParam.Start(10); 

void MyMethodWithParam(object param)
{
    Console.WriteLine($"线程接收到参数:{param}"); // 线程接收到参数:10
}

生命周期

一个线程的生命周期包括以下几个状态(C#,Java中类似但不相同):

  1. Unstarted:线程已创建,但尚未启动。
  2. Running:线程正在运行。
  3. WaitSleepJoin:线程被阻塞或休眠。
  4. Stopped:线程已结束。 An imange

示例

C#
Thread thread = new Thread(SomeMethod);
Console.WriteLine(thread.ThreadState); // 输出 Unstarted
thread.Start();
Console.WriteLine(thread.ThreadState); // 输出 Running 或其他状态

线程的优先级

可以通过设置 Thread.Priority 属性来调整线程的优先级,默认情况下,线程的优先级是 Normal。 C# 支持以下优先级:

  1. ThreadPriority.Highest
  2. ThreadPriority.AboveNormal
  3. ThreadPriority.Normal
  4. ThreadPriority.BelowNormal
  5. ThreadPriority.Lowest

示例:

C#
Thread thread = new Thread(SomeMethod);
thread.Priority = ThreadPriority.Highest; // 设置最高优先级
thread.Start();

注意

高优先级线程有更高的机会被操作系统调度执行,但不意味着高优先级线程一定会先执行或持续执行。

即使线程优先级较低,操作系统依然会为其分配 CPU 时间,只是分配的频率可能较低。

现代操作系统使用抢占式多任务系统,线程调度取决于很多动态因素,而不仅仅是优先级。

如果你希望某个线程完成执行后才继续后续代码,可以使用 Thread.Join() 方法。

线程饥饿

如果高优先级线程持续占用大量 CPU 资源,低优先级线程可能会很少获得执行机会,这可能导致线程饥饿问题。

解决线程饥饿问题可以使用Thread.Yield()、Thread.Sleep(0)、Task.Delay(0) (异步方法)等来放弃当前时间片。

虽然这不会让某个特定线程立刻执行,但可以促使系统调度新线程。

示例:

C#
while (true)
{
    // 执行一些重要的工作
    
    // 让出时间片,允许其他线程执行
    Thread.Sleep(0);
}

public async Task DoSomethingAsync()
{
    // 模拟异步操作,不引入任何实际延迟
    await Task.Delay(0);
    Console.WriteLine("继续执行异步操作");
}

这么处理的优点:

  1. 平衡负载:如果某些线程的执行不那么紧急,通过在某些代码路径中加入 Thread.Sleep(0) 可以降低这些线程的执行频率,避免影响其他更重要的线程。
  2. 降低资源占用:在高负载环境下,线程主动让出时间片,可以减少资源占用并提高应用程序的整体性能。

前台线程与后台线程

前台线程:应用程序会等待所有前台线程执行完毕后才退出。

后台线程:应用程序会在主线程完成后直接退出,而不管后台线程是否完成。

可以通过 IsBackground 属性来将线程设置为前台或后台线程。

示例:

C#
Thread thread = new Thread(SomeMethod);
thread.IsBackground = true; // 设置为后台线程
thread.Start();

线程的中止(Abort)

在 C# 中,使用 Thread.Abort() 强行终止线程已经被废弃,因为它会引发不可预料的问题。

取而代之的是使用 CancellationToken 进行线程取消。

示例:

C#
Thread thread = new Thread(SomeMethod);
thread.Start();
thread.Abort(); // 强制中止线程(已被废弃)

注意

调用 Thread.Abort() 方法会导致目标线程抛出一个 ThreadAbortException 异常。

这个异常用于强制终止线程的执行,且是可捕获的,

但线程最终会终止,即使捕获了该异常,除非调用 Thread.ResetAbort() 方法。

使用thread.Abort()一定要在合适的位置调用Thread.ResetAbort()

线程的暂停与恢复

Thread.Suspend() 和 Thread.Resume() 方法也已经被废弃,不推荐使用。

如果需要让线程进入等待状态,通常使用 Monitor.Wait() 或者 ManualResetEvent 等同步机制。

线程的加入(Join)

Thread.Join() 方法可以让主线程等待某个线程完成后再继续执行。

如果不使用 Join(),主线程可能会在新线程完成之前结束。

示例:

C#
Thread thread = new Thread(SomeMethod);
thread.Start();
thread.Join(); // 等待该线程执行完毕
Console.WriteLine("线程已完成,主线程继续执行");

线程的休眠(Sleep)

Thread.Sleep() 方法可以让线程暂停执行指定时间。这对模拟长时间运行的操作或控制线程调度非常有用。

示例:

C#
Thread.Sleep(2000); // 线程休眠 2 秒
Console.WriteLine("线程休眠结束");

线程池(ThreadPool)

C# 中的线程池(Thread Pool)是一个高效的、多线程任务管理机制,

线程池管理一组预先创建或动态创建的线程,这些线程可以重复使用以执行多个任务。

线程池的主要作用是避免为每个任务单独创建和销毁线程所带来的高昂开销,提高多线程编程的效率和性能。

特点:

  1. 自动管理:线程池自动管理线程的创建、分配和销毁,开发者不需要手动创建或终止线程。 如果任务量很大,线程池会创建更多线程以满足需求。任务量减少时,线程池会回收一些不再需要的线程。
  2. 任务排队:如果线程池中的所有线程都在忙碌,新的任务会排队等待空闲线程执行。
  3. 不能控制线程的优先级:线程池中的线程优先级是由系统控制的,不能由用户设置。
  4. 线程重用:任务完成后,线程不会立即销毁,而是回到线程池中等待新的任务。这种机制减少了频繁创建和销毁线程的开销。
  5. 易于使用:无需手动管理线程的生命周期,只需提交任务给线程池。
  6. 线程数量优化:线程池会根据系统的负载自动调整线程数量,避免过多或过少的线程影响性能。

示例:

C#
using System;
using System.Threading;

class Program
{
    static void Main(string[] args)
    {
        // 设置线程池的最小和最大线程数(可选)
        ThreadPool.SetMinThreads(4, 4); // 设置最小工作线程数和异步I/O线程数
        ThreadPool.SetMaxThreads(16, 16); // 设置最大工作线程数和异步I/O线程数


        // 向线程池提交任务
        ThreadPool.QueueUserWorkItem(WorkItemMethod);

        Console.WriteLine("主线程继续执行...");
        Thread.Sleep(2000); // 确保后台线程有机会执行
    }

    static void WorkItemMethod(object state)
    {
        Console.WriteLine("工作项正在执行...");
        Thread.Sleep(1000); // 模拟一些耗时操作
        Console.WriteLine("工作项完成");
    }
}

避免阻塞线程池线程

由于线程池中的线程是共享的资源,如果某个任务长时间阻塞线程池中的线程,会导致其他任务无法及时得到执行。

因此,应该尽量避免在线程池任务中执行长时间阻塞的操作,如同步 I/O 操作。

任务并行库(TPL - Task Parallel Library)

任务并行库(Task Parallel Library, TPL)是 .NET Framework 中提供的一组用于并行编程的类库,旨在简化多线程编程。

TPL 是基于任务 (Task) 的,并且高度集成了线程池机制,用于高效执行并发和异步任务。

相比手动管理线程,TPL 提供了更高级的抽象和更好的开发体验。

优势

  1. 简化并发编程:TPL 提供了任务(Task)抽象,开发者无需手动管理线程。
  2. 与线程池集成:任务由线程池执行,系统自动调度。
  3. 丰富的功能:支持任务的取消、异常处理、父子关系等。
  4. 灵活的等待机制:可以等待一个任务、一组任务或任意任务完成。

注意

TPL 使用的是底层的 .NET 线程池,管理线程池需要通过ThreadPool。

如设置最大线程数: ThreadPool.SetMaxThreads(10, 10);

主要功能

任务创建与运行

  1. Task.Run():用于启动异步任务的最常见方法,背后使用线程池来执行任务。
C#
Task.Run(() => Console.WriteLine("任务并发执行"));
  1. Task.Factory.StartNew():提供更多的任务配置选项,但一般推荐使用 Task.Run() 作为更简便的方式。
C#
Task task = Task.Factory.StartNew(() => Console.WriteLine("通过工厂启动任务"));
task.Wait();  // 等待任务完成

任务的等待

  1. Task.Wait():阻塞当前线程,直到任务执行完成。
C#
Task task = Task.Run(() => Thread.Sleep(1000));
task.Wait();  // 等待任务完成
  1. Task.Result:对于返回结果的任务,使用 Result 属性来获取结果,Result 会在任务完成时返回结果,未完成前会阻塞调用线程。
C#
Task<int> task = Task.Run(() => 42);
int result = task.Result; // 阻塞等待任务完成并获取结果
  1. Task.WaitAll():等待多个任务完成。
C#
Task[] tasks = new Task[]
{
    Task.Run(() => Thread.Sleep(1000)),
    Task.Run(() => Thread.Sleep(500))
};
Task.WaitAll(tasks);  // 等待所有任务完成
  1. Task.WhenAll():用于异步等待一组任务全部完成。
C#
Task[] tasks = new Task[]
{
    Task.Run(() => Thread.Sleep(1000)),
    Task.Run(() => Thread.Sleep(2000))
};
await Task.WhenAll(tasks);  // 异步等待所有任务完成
  1. Task.WhenAny():用于异步等待一组任务中任意一个任务完成。
C#
Task[] tasks = new Task[]
{
    Task.Run(() => Thread.Sleep(1000)),
    Task.Run(() => Thread.Sleep(2000))
};
await Task.WhenAny(tasks);  // 异步等待任意一个任务完成

任务的取消

TPL 支持任务的取消,通过 CancellationToken 实现。开发者可以请求取消任务,并让任务响应取消请求。

C#
CancellationTokenSource cts = new CancellationTokenSource();
Task task = Task.Run(() =>
{
    while (!cts.Token.IsCancellationRequested)
    {
        Console.WriteLine("任务运行中...");
        Thread.Sleep(500);
    }
}, cts.Token);

// 在一秒后取消任务
Thread.Sleep(1000);
cts.Cancel();

任务的异常处理

如果任务抛出了异常,可以通过 Task.Wait() 或 Task.Result 捕获异常,Task 内部的异常会被包装在 AggregateException 中。

C#
Task task = Task.Run(() =>
{
    throw new InvalidOperationException("发生异常");
});

try
{
    task.Wait();  // 等待任务完成并捕获异常
}
catch (AggregateException ex)
{
    foreach (var innerException in ex.InnerExceptions)
    {
        Console.WriteLine(innerException.Message);  // 输出异常信息
    }
}

注意

当任务抛出异常且没有调用 Task.Wait() 或 Task.Result,异常会被封装在任务内部,并不会立即传播到主线程或调用它的线程。 (可以通过任务的 IsFaulted 属性来检测任务是否失败。)

这是因为 Task 中未处理的异常会在 Task 对象被垃圾回收时通过 TaskScheduler.UnobservedTaskException 事件触发。

C#
TaskScheduler.UnobservedTaskException += (sender, e) =>
{
    Console.WriteLine("捕获未观察到的任务异常:");
    foreach (var exception in e.Exception.InnerExceptions)
    {
        Console.WriteLine(exception.Message);
    }
    e.SetObserved(); // 标记异常为已观察,避免程序崩溃
};

并行编程

并行循环(Parallel.For和Parallel.ForEach)

TPL 提供了 Parallel.For 和 Parallel.ForEach 方法,用于并行执行循环迭代。

示例:

C#
Parallel.For(0, 10, i =>
{
    Console.WriteLine($"并行循环 {i}");
});

var items = new List<int> { 1, 2, 3, 4, 5 };
Parallel.ForEach(items, item =>
{
    Console.WriteLine($"并行处理项目 {item}");
});

并行LINQ(PLINQ)

PLINQ 是一个扩展 LINQ 的功能,可以并行化数据查询。使用 .AsParallel() 方法可以并行执行查询操作。

C#
var numbers = Enumerable.Range(1, 100).ToList();
var evenNumbers = numbers.AsParallel().Where(n => n % 2 == 0).ToList();

异步编程(async/await)

C# 中的异步编程模式是通过 async 和 await 关键字实现的,目的是简化异步操作的编写。

传统的异步编程通常需要回调或事件驱动,这使得代码变得复杂和难以维护。

通过 async 和 await,C# 提供了一种简洁的方式来编写异步代码,使得代码看起来像是同步的。

异步方法的执行特点:

  1. 当调用异步方法时,方法会立即返回一个 Task,表示异步操作的结果。调用方法的线程不会等待异步操作完成,而是继续执行接下来的代码。
  2. 通过 await 关键字,可以暂停当前方法的执行,等待异步操作完成,然后继续执行 await 之后的代码。
  3. 异步方法在等待操作完成时不会阻塞线程,调用线程可以继续执行其他任务。

示例:

C#
// 如果异步方法没有返回值,使用 Task 作为返回类型。
public async Task DoSomethingAsync()
{
    await Task.Delay(1000);
}

// 如果异步方法需要返回值,使用 Task<TResult> 作为返回类型。
public async Task<int> CalculateAsync()
{
    await Task.Delay(1000);
    return 42;
}

// async void 只在某些特定情况下使用,如事件处理器。一般避免使用 async void,因为无法对异常进行处理。
public async void Button_Click(object sender, EventArgs e)
{
    await DoSomethingAsync();
}

//执行
await DoSomethingAsync();
var result = await CalculateAsync();

// 异步流 (async 和 await foreach) 【> c#8】
public async IAsyncEnumerable<int> GetNumbersAsync()
{
    for (int i = 1; i <= 5; i++)
    {
        await Task.Delay(1000);  // 模拟异步操作
        yield return i;
    }
}

public async Task ProcessNumbersAsync()
{
    await foreach (var number in GetNumbersAsync())
    {
        Console.WriteLine(number);
    }
}

锁定与同步(Locks and Synchronization)

多线程编程中,锁定与同步(Locks and Synchronization)是确保多个线程正确地共享资源、避免竞态条件(Race Conditions)和数据不一致性的重要机制。

锁定和同步机制虽然可以保证线程安全,但也会带来性能开销,因为锁的获取和释放需要一定的时间,而且会导致线程等待。

所以,在使用同步机制时应尽量缩小锁的粒度,即只锁定确实需要保护的最小代码范围。

锁(lock)

lock 是 C# 中最简单也是最常用的同步机制。它用于确保在某个时刻只有一个线程可以进入某个临界区。

lock 本质上是 Monitor.Enter 和 Monitor.Exit 的语法糖。

示例:

C#
private readonly object _lockObject = new object();
private int _counter = 0;

public void Increment()
{
    lock (_lockObject)
    {
        _counter++;
    }
}

互斥锁(Mutex)

Mutex 是一个同步基元,允许多个线程之间同步对资源的访问。

与 lock 不同的是,Mutex 可以跨多个进程使用。

C#
Mutex mutex = new Mutex();

public void AccessResource()
{
    mutex.WaitOne();  // 请求互斥锁
    try
    {
        // 访问共享资源
    }
    finally
    {
        mutex.ReleaseMutex();  // 释放互斥锁
    }
}

Mutex可以实现分布式吗

Mutex(互斥体)在C#中主要用于进程间同步,它允许一个进程内的多个线程同步访问共享资源。

然而,对于分布式系统中的多个服务节点或进程,Mutex的功能受到限制,

因为它主要是设计用来在同一操作系统内多个进程间进行同步,而不是跨多个操作系统或分布式环境。

读写锁(ReaderWriterLockSlim)

ReaderWriterLockSlim 允许多线程环境下的读写操作更高效。它允许多个读者同时读取资源,但在写操作时,其他线程必须等待。

C#
ReaderWriterLockSlim rwLock = new ReaderWriterLockSlim();

public void ReadResource()
{
    rwLock.EnterReadLock();  // 进入读锁
    try
    {
        // 读取共享资源
    }
    finally
    {
        rwLock.ExitReadLock();  // 释放读锁
    }
}

public void WriteResource()
{
    rwLock.EnterWriteLock();  // 进入写锁
    try
    {
        // 写入共享资源
    }
    finally
    {
        rwLock.ExitWriteLock();  // 释放写锁
    }
}

死锁

死锁是指两个或多个线程相互等待对方持有的锁,导致这些线程永远无法继续执行的情况。

为了避免死锁:

  1. 避免嵌套锁定多个资源。
  2. 按照固定顺序获取多个锁。
  3. 使用超时锁定操作,如 Monitor.TryEnter() 或 Mutex.WaitOne(timeout)。

线程通信

在多线程编程中,线程之间需要进行通信来协调工作,确保数据的一致性和同步。

C# 提供了多种方式来实现线程间的通信,常见的方式有以下几种:

共享变量

这是最简单的线程通信方式,线程通过共享的变量进行通信。

然而,由于多个线程可能同时访问和修改这些共享变量,通常需要同步机制(如 lock、Monitor)来保护这些变量,防止竞态条件和数据不一致。

示例:

C#
private int sharedVariable = 0;
private readonly object _lockObject = new object();

public void Increment()
{
    lock (_lockObject)
    {
        sharedVariable++;
    }
}

public int GetSharedVariable()
{
    lock (_lockObject)
    {
        return sharedVariable;
    }
}

volatile关键字

volatile 关键字可以用于声明一个变量,使得该变量的值在多线程访问时,每次都会从内存中读取最新的值,确保通信时数据的一致性。

在单线程环境中,编译器和处理器可能会为了提高性能而进行一些优化。这些优化包括:

  1. 寄存器缓存:编译器可能会将某个变量的值缓存到寄存器中,避免频繁从内存中读取,以提高速度。
  2. 指令重排序:处理器可能会在不影响单线程执行结果的情况下重排序指令执行顺序,以提升性能。 然而,在多线程程序中,这些优化可能会导致错误的行为。

例如,线程可能从寄存器中读取到一个过时的值,而不是从内存中获取最新的值。

指令重排序可能会导致数据状态的不一致,从而引发竞态条件。

volatile 关键字通过以下方式防止编译器和 CPU 的优化,确保多线程之间共享的变量始终是最新值:

  1. 禁止寄存器缓存:当一个变量被声明为 volatile 时,每次读取该变量时,都会直接从主存(内存)中获取值,而不是从缓存或寄存器中获取。
  2. 防止指令重排序:volatile 保证对该变量的读写操作的顺序不被编译器或 CPU 重新排列。这意味着变量的读写操作会严格按照代码中定义的顺序执行。

示例:

C#
private bool _isRunning = true;

public void ThreadA()
{
    while (_isRunning)
    {
        // 执行操作
    }
}

public void ThreadB()
{
    _isRunning = false;  // 让线程 A 停止
}

在多线程环境中,线程 A 可能会持续从寄存器中读取 _isRunning 的旧值,导致它无法及时退出循环。

C#
private volatile bool _isRunning = true;

public void ThreadA()
{
    while (_isRunning)
    {
        // 执行操作
    }
}

public void ThreadB()
{
    _isRunning = false;  // 让线程 A 停止
}

volatile 的局限性

虽然 volatile 能防止编译器和 CPU 对单个变量的读写操作进行优化,但它不能解决以下问题:

  1. 复杂的同步问题:如果多个变量之间存在依赖关系,volatile 不能确保多个变量之间的操作是原子的,仍然可能需要使用锁(如 lock)来保护更复杂的同步操作。
  2. 复合操作的原子性:volatile 仅能保证单次读写操作是线程安全的,不能保证复合操作(如递增、递减等)是线程安全的。 因此,如果你需要在多线程中对一个变量进行复合操作,仍然需要使用其他同步机制(如锁或原子操作)。
  3. volatile 关键字可以用于引用类型,但它只能保证对引用本身的可见性,而不能保证引用对象的内部状态或成员变量的线程安全性。
  4. double 和 long无法标记为 volatile,因为对这些类型的字段的读取和写入不能保证是原子的。

因此,一般volatile只会修饰简单操作的sbyte、byte、short、ushort、int、uint、char、float 和 bool类型。

并发集合

参考集合

事件机制(ManualResetEvent/AutoResetEvent)

ManualResetEvent/AutoResetEvent都用于线程间的信号传递,让一个线程等待另一个线程发出信号。

ManualResetEvent:手动重置,多个线程可以在同一个信号上等待,直到手动重置信号。

AutoResetEvent:自动重置,每次信号触发时,只有一个线程能继续执行,信号会自动重置。

示例:

C#
ManualResetEvent manualEvent = new ManualResetEvent(false);

public void Thread1()
{
    // 等待信号
    manualEvent.WaitOne();
    Console.WriteLine("Thread1 被唤醒!");
}

public void Thread2()
{
    // 触发信号
    manualEvent.Set();
    Console.WriteLine("Thread2 触发了信号");
}
C#
AutoResetEvent autoEvent = new AutoResetEvent(false);

public void Thread1()
{
    autoEvent.WaitOne();  // 等待信号
    Console.WriteLine("Thread1 被唤醒!");
}

public void Thread2()
{
    autoEvent.Set();  // 触发信号,自动重置
    Console.WriteLine("Thread2 触发了信号");
}

new AutoResetEvent 传入的参数是干嘛用的呢? 在 AutoResetEvent 的构造函数中传入的参数是一个布尔值,用于指定初始的信号状态。这个参数被称为 initialState

  • 如果 initialStatetrue,则 AutoResetEvent 的初始状态为有信号状态。也就是说,线程在等待时不会被阻塞,而是可以直接执行。此时,调用 WaitOne 方法时,如果 AutoResetEvent 处于无信号状态,则线程将立即继续执行,不会等待信号。

  • 如果 initialStatefalse,则 AutoResetEvent 的初始状态为无信号状态。也就是说,线程在等待时会被阻塞,直到收到信号才能继续执行。此时,调用 WaitOne 方法时,如果 AutoResetEvent 处于无信号状态,则线程将被阻塞,直到调用 Set 方法将其置为有信号状态。

AutoResetEvent 和 ManualResetEvent 有啥区别:

  1. 触发方式:

    • AutoResetEvent:在调用 Set 方法后,只有一个等待线程会被唤醒并继续执行,然后 AutoResetEvent 会自动恢复为无信号状态。即每次调用 Set 方法只唤醒一个等待线程。
    • ManualResetEvent:在调用 Set 方法后,所有等待线程都会被唤醒并继续执行,直到显式调用 Reset 方法将 ManualResetEvent 设置回无信号状态为止。即每次调用 Set 方法会唤醒所有等待线程。
  2. 状态重置:

    • AutoResetEvent:自动重置为无信号状态。即当一个线程等待信号并被唤醒后,AutoResetEvent 会自动将自身重置为无信号状态,以便下一个等待的线程能够继续等待。
    • ManualResetEvent:需要显式调用 Reset 方法将其重置为无信号状态。即 ManualResetEvent 会保持有信号状态,直到调用 Reset 方法将其置回无信号状态。
  3. 等待方式:

    • AutoResetEvent:在调用 WaitOne 方法时,如果 AutoResetEvent 处于无信号状态,线程会被阻塞直到接收到信号。如果 AutoResetEvent 处于有信号状态,线程会消耗该信号并继续执行。
    • ManualResetEvent:在调用 WaitOne 方法时,如果 ManualResetEvent 处于无信号状态,线程会被阻塞直到接收到信号。即使 ManualResetEvent 处于有信号状态,线程也会消耗该信号并继续执行,而不会自动将其重置为无信号状态。

根据这些区别,可以选择使用 AutoResetEvent 或 ManualResetEvent 来适应不同的线程同步需求。

  • 如果需要每次只唤醒一个等待线程,并且希望线程在接收到信号后自动重置为无信号状态,可以使用 AutoResetEvent。这对于一些排队任务的场景很有用。
  • 如果需要唤醒所有等待线程,并且希望线程在接收到信号后保持有信号状态,直到显式调用 Reset 方法将其重置为无信号状态,可以使用 ManualResetEvent。这对于一些广播通知的场景很有用。

Monitor

Monitor 类可以用于线程间的通信,它可以手动控制线程的等待和唤醒,通过 Monitor.Wait() 和 Monitor.Pulse() 方法实现。

  1. Monitor.Wait():使线程等待,直到收到其他线程的通知。
  2. Monitor.Pulse():通知一个等待的线程继续执行。
  3. Monitor.PulseAll():通知所有等待的线程继续执行。

示例:

C#
private readonly object _lockObject = new object();
private bool isReady = false;

public void Thread1()
{
    lock (_lockObject)
    {
        while (!isReady)
        {
            Monitor.Wait(_lockObject);  // 等待通知
        }
        Console.WriteLine("Thread1 被唤醒!");
    }
}

public void Thread2()
{
    lock (_lockObject)
    {
        isReady = true;
        Monitor.Pulse(_lockObject);  // 通知 Thread1
        Console.WriteLine("Thread2 发送通知");
    }
}

Semaphore 和 SemaphoreSlim

Semaphore 是一种允许多个线程并发访问有限资源的同步机制,适用于需要控制并发访问数量的场景。

SemaphoreSlim 是 Semaphore 的轻量版本,适用于同一进程中的线程通信。

示例:

C#
Semaphore semaphore = new Semaphore(2, 2);  // 最多允许 2 个线程访问

public void AccessResource()
{
    semaphore.WaitOne();  // 请求信号量
    try
    {
        Console.WriteLine("资源访问中...");
        Thread.Sleep(1000);
    }
    finally
    {
        semaphore.Release();  // 释放信号量
    }
}

TaskCompletionSource 和 Task

Task 类和 TaskCompletionSource 提供了一种异步任务和线程通信的方式。

TaskCompletionSource 可以手动完成任务,适合异步方法之间的通信。

示例:

C#
TaskCompletionSource<bool> taskCompletionSource = new TaskCompletionSource<bool>();

public async Task WaitForSignalAsync()
{
    await taskCompletionSource.Task;
    Console.WriteLine("线程被唤醒");
}

public void Signal()
{
    taskCompletionSource.SetResult(true);  // 触发任务完成
    Console.WriteLine("信号已发送");
}

BlockingCollection

BlockingCollection 是一种线程安全的集合,允许线程安全地添加和移除元素,通常用于生产者-消费者模式。

线程可以阻塞等待集合中的数据,直到有可用的元素。

参考集合

计时器

System.Threading.Timer

System.Threading.Timer 是一种简单的定时器,可以执行周期性任务或延迟一定时间后执行任务。它不需要 UI 线程,因此适合后台任务。

示例:

C#
using System;
using System.Threading;

class Program
{
    static void Main(string[] args)
    {
        Timer timer = new Timer(TimerCallback, null, 0, 1000);  // 立即启动,间隔1秒

        Console.WriteLine("按任意键退出...");
        Console.ReadLine();  // 等待用户按键退出
    }

    private static void TimerCallback(Object o)
    {
        Console.WriteLine($"定时器触发时间:{DateTime.Now}");

        // GC.SuppressFinalize 使得对象不被终结器清理
        GC.SuppressFinalize(o);
    }
}

参数说明:

  • TimerCallback:回调方法,当定时器触发时调用。
  • dueTime:首次启动延迟时间(毫秒),0 表示立即执行。
  • period:之后的每次间隔时间(毫秒),1000 表示每隔 1 秒执行一次任务。

说明:

  • System.Threading.Timer 适合需要在后台定期执行任务的场景,执行的回调会在线程池中运行,因此不会阻塞主线程。
  • 可以使用 timer.Change(Timeout.Infinite, Timeout.Infinite) 来停止定时器。

System.Timers.Timer

System.Timers.Timer 类是专门用于定时任务的类,支持同步和异步回调。它可以用于触发一次或周期性任务,常用于服务或后台任务。

示例:使用 System.Timers.Timer

C#
using System;
using System.Timers;

class Program
{
    private static Timer _timer;

    static void Main(string[] args)
    {
        _timer = new Timer(1000);  // 每隔1秒触发
        _timer.Elapsed += OnTimedEvent;  // 绑定事件
        _timer.AutoReset = true;  // 自动重置
        _timer.Enabled = true;  // 启动定时器

        Console.WriteLine("按任意键退出...");
        Console.ReadLine();  // 等待用户按键退出
    }

    private static void OnTimedEvent(Object source, ElapsedEventArgs e)
    {
        Console.WriteLine($"定时器触发时间:{e.SignalTime}");
    }
}

说明:

  • Elapsed 事件会在每次触发定时器时调用。AutoReset 表示定时器是否应该自动重复,如果设置为 false,定时器只触发一次。
  • System.Timers.Timer 的回调方法在不同的线程上执行,适合处理后台工作任务。

System.Windows.Forms.Timer 和 DispatcherTimer

这两个定时器主要用于 GUI 应用程序中,特别是 Windows Forms 或 WPF 应用程序。它们都在 UI 线程上运行,适合需要更新 UI 或定期检查用户输入的场景。

  • System.Windows.Forms.Timer:主要用于 Windows Forms 应用程序。它在 UI 线程上运行,确保定时器触发时可以安全地更新 UI 元素。
  • DispatcherTimer:适用于 WPF 应用程序,同样在 UI 线程上运行。

示例:WPF 中的 DispatcherTimer:

C#
using System;
using System.Windows.Threading;

class Program
{
    static void Main(string[] args)
    {
        DispatcherTimer dispatcherTimer = new DispatcherTimer();
        dispatcherTimer.Tick += DispatcherTimer_Tick;
        dispatcherTimer.Interval = TimeSpan.FromSeconds(1);  // 设置间隔时间
        dispatcherTimer.Start();  // 启动定时器

        Console.WriteLine("按任意键退出...");
        Console.ReadLine();
    }

    private static void DispatcherTimer_Tick(object sender, EventArgs e)
    {
        Console.WriteLine($"WPF 定时器触发时间:{DateTime.Now}");
    }
}

Task.Run 和 CancellationToken 实现定时任务

可以使用 Task.Run 和 CancellationToken 来实现定时任务,并提供取消操作的支持。

示例:使用 Task.Run 实现可取消的定时任务:

C#
using System;
using System.Threading;
using System.Threading.Tasks;

class Program
{
    static async Task Main(string[] args)
    {
        CancellationTokenSource cts = new CancellationTokenSource();

        Task task = Task.Run(() => PeriodicTask(cts.Token), cts.Token);

        Console.WriteLine("按任意键停止任务...");
        Console.ReadKey();
        cts.Cancel();  // 请求取消

        await task;
        Console.WriteLine("任务已停止");
    }

    static async Task PeriodicTask(CancellationToken token)
    {
        while (!token.IsCancellationRequested)
        {
            Console.WriteLine($"执行任务:{DateTime.Now}");
            await Task.Delay(1000);  // 模拟周期任务
        }
    }
}

Document Base on VitePress.