一、为什么要使用多线程开发
1.提高性能
多线程允许程序同时执行多个任务,从而有效利用多核处理器,加快程序的执行速度。特别是在需要处理大量计算、I/O 操作或并行任务的应用中,多线程可以显著提高性能。
2.响应性
多线程使应用能够同时处理多个用户请求或事件,提高了应用的响应性。例如,多线程可以保持用户界面的响应,即使在执行长时间操作时也能让用户继续交互。
3.资源利用
多线程可以更有效地利用系统资源,如内存和网络连接。这对于高并发服务器、网络应用和数据处理任务特别有用。
4.任务分解
将复杂任务分解为多个小任务,每个任务在不同的线程中执行,可以简化问题并提高可维护性。
5.并行计算
多线程可以用于并行计算,例如在科学计算、数据分析和图像处理领域。这有助于加速大规模计算。
6.实时处理
在实时系统中,多线程可以保证任务在规定的时间内完成,从而满足对时间敏感性的需求。
二、多线程开发缺点
1.竞态条件
多线程可能会导致竞态条件,即多个线程竞争访问共享资源,可能导致数据不一致性和错误。
2.死锁和饥饿
不正确的线程同步可能导致死锁(多个线程无法继续执行)或饥饿(某些线程无法获取所需资源)问题。
线程1,2启动,分别占用锁lock1,lock2。之后线程1请求lock2,但是线程2已经占用lock2,线程1无法继续执行,进入等待。线程2请求lock1,但是线程1已经占用lock1,线程2无法继续执行,进入等待。这里陷入死锁,线程1,线程2,都在等待对方释放锁来给自己使用,程序一直无法运行,一直在等待中。
using System;
using System.Threading;
class DeadlockExample
{
static object lock1 = new object();
static object lock2 = new object();
static void Main()
{
Thread thread1 = new Thread(Method1);
Thread thread2 = new Thread(Method2);
thread1.Start();
thread2.Start();
thread1.Join();
thread2.Join();
Console.WriteLine("Main thread finished.");
}
static void Method1()
{
lock (lock1)
{
Console.WriteLine("Method1 acquired lock1.");
Thread.Sleep(1000);
Console.WriteLine("Method1 trying to acquire lock2.");
lock (lock2)
{
Console.WriteLine("Method1 acquired lock2.");
}
}
}
static void Method2()
{
lock (lock2)
{
Console.WriteLine("Method2 acquired lock2.");
Thread.Sleep(1000);
Console.WriteLine("Method2 trying to acquire lock1.");
lock (lock1)
{
Console.WriteLine("Method2 acquired lock1.");
}
}
}
}
以下方法不会造成死锁
因为Lock的锁级别是线程级的,这里test的递归中并未创建新的线程,所以一直是同一个线程在使用这个锁。
个人理解:来申请时lock只认当前使用的线程,可以多次申请,但是只能给一个线程使用。
在同一个线程中即使递归也只是对一个变量进行操作,因为有先后顺序,所以不会发生抢占,不用担心线程安全。
private object lockObject = new object();
public void test2(int i)
{
lock (lockObject)
{
if (i > 10)
{
i--;
test2(i);
}
}
}
3.调试复杂性
多线程程序的调试和错误跟踪可能会更加复杂,因为线程间的交互和排错可能变得更难。
4.上下文切换开销
上下文切换(Context Switching)是多线程环境中的一种操作,指的是在一个 CPU 核心上切换正在执行的线程,从当前线程的执行上下文(包括寄存器状态、程序计数器等)切换到另一个线程的执行上下文, 线程的切换需要额外的开销,因此在某些情况下,过多的线程可能会导致性能下降。
- 当一个线程的时间片(时间片轮转调度算法)用完,操作系统需要挂起该线程并切换到另一个线程。
- 当一个线程主动放弃 CPU,例如通过调用 Thread.Sleep()、Thread.Yield() 或等待某个事件时
3.当一个线程被高优先级的线程抢占
上下文切换的过程涉及以下步骤:
- 保存当前线程的上下文: 操作系统将当前线程的寄存器状态、程序计数器等信息保存到该线程的内存空间中,以便稍后能够恢复该线程的执行
2.恢复目标线程的上下文: 操作系统从目标线程的内存空间中恢复寄存器状态、程序计数器等信息,准备让目标线程继续执行。- 切换内核堆栈: 每个线程都有自己的内核堆栈,上下文切换时,操作系统会切换内核堆栈,以确保线程的隔离性。
上下文切换开销指的是从一个线程切换到另一个线程的过程中所涉及的时间和资源开销。这些开销主要包括以下几个方面:
- 寄存器保存和恢复: 当线程切换时,操作系统需要保存当前线程的寄存器状态,然后恢复目标线程的寄存器状态。这涉及到大量的数据拷贝和计算。
2.内存访问: 上下文切换过程中需要频繁访问内存,包括将寄存器状态和其他上下文信息写入内存,以及从内存中读取目标线程的上下文信息。
3.调度开销: 操作系统需要决定要切换到哪个线程,这涉及到调度算法的开销,包括选择合适的线程并进行必要的线程队列操作。
4.TLB(Translation Lookaside Buffer)失效: 当线程切换时,虚拟内存的映射可能会发生变化,导致 TLB 缓存失效,从而增加了内存访问的开销。
上下文切换开销会影响系统的整体性能,特别是在高并发、频繁切换的情况下。因此,在设计多线程应用程序时,需要考虑如何减少上下文切换的发生,以提高程序的执行效率。一些方法包括:
- 使用线程池:线程池可以减少线程的创建和销毁,从而减少上下文切换的频率。
- 合理设置线程数量:避免创建过多线程,以减少不必要的上下文切换。
3.使用异步编程模型:使用异步操作和任务可以减少线程的使用,从而减少上下文切换。
5.线程安全性
多线程编程需要谨慎处理线程安全性,以避免数据竞争和共享资源的冲突。
三、多线程开发涉及的相关概念
常用概念
(1)lock
在 C# 中,lock 关键字用于实现线程同步,以确保在多线程环境中对共享资源的访问是安全的。lock 关键字会创建一个互斥锁(也称为监视器锁),只有一个线程可以获得该锁,从而确保在同一时间只有一个线程能够执行被 lock 包围的代码块。
lock (lockObject)
{
// 在这里执行需要同步的代码
}
其中,lockObject 是一个用于同步的对象。它可以是任何引用类型的对象,但通常是一个专门用于同步的对象。多个线程可以共享同一个 lockObject,并且只有一个线程能够获得锁并执行被 lock 包围的代码块。
class Program
{
static readonly object lockObject = new object(); // 同步对象
static void Main(string[] args)
{
for (int i = 0; i < 5; i++)
{
ThreadStart start = () =>
{
lock (lockObject)
{
Console.WriteLine($"Thread {Thread.CurrentThread.ManagedThreadId} is in the critical section.");
Thread.Sleep(1000);
Console.WriteLine($"Thread {Thread.CurrentThread.ManagedThreadId} has exited the critical section.");
}
};
Thread thread = new Thread(start);
thread.Start();
}
Console.ReadKey();
}
}
(2)查看当前工作线程信息
可以使用 Thread.CurrentThread 属性来获取当前正在执行的线程的信息。这个属性返回一个表示当前线程的 Thread 对象,你可以使用它来查询线程的各种属性和状态。
Thread 类还提供了 Priority 属性,允许你设置线程的优先级。然而,操作系统不一定会完全遵循线程的优先级,这取决于操作系统的调度机制。
线程可以分为前台线程和后台线程。前台线程是主线程的一部分,如果所有前台线程都完成,程序将终止。后台线程是在后台运行的线程,如果所有前台线程都完成,程序会立即终止,不会等待后台线程完成。
using System;
using System.Threading;
class Program
{
static void Main()
{
Thread currentThread = Thread.CurrentThread;
Console.WriteLine($"Thread ID: {currentThread.ManagedThreadId}");
Console.WriteLine($"Thread Name: {currentThread.Name}");
Console.WriteLine($"Is Thread Background: {currentThread.IsBackground}");
Console.WriteLine($"Thread Priority: {currentThread.Priority}");
Console.WriteLine($"Thread State: {currentThread.ThreadState}");
}
}
(3)主线程、前台线程、后台线程
主线程(Main Thread),它是程序的入口点,并且在程序启动时自动创建。主线程负责启动其他线程,并且通常是其他线程的父线程,但并不是所有线程都是主线程的子线程。
线程之间没有严格的父子关系。主线程和其他线程之间通常是平等的,没有直接的父子关系。但是,你可以通过编程来模拟一种线程间的层次关系,使得某些线程在逻辑上看起来是其他线程的子线程。这通常涉及线程的创建、协调和通信
以下是一个示例,演示了如何通过逻辑上的组织来模拟一种主线程和子线程的关系:
using System;
using System.Threading;
class Program
{
static void Main()
{
Console.WriteLine("Main thread starts.");
Thread parentThread = new Thread(ParentThreadMethod);
parentThread.Start();
parentThread.Join();
Console.WriteLine("Main thread ends.");
}
static void ParentThreadMethod()
{
Console.WriteLine("Parent thread starts.");
Thread childThread = new Thread(ChildThreadMethod);
childThread.Start();
childThread.Join();
Console.WriteLine("Parent thread ends.");
}
static void ChildThreadMethod()
{
Console.WriteLine("Child thread starts.");
Thread.Sleep(2000);
Console.WriteLine("Child thread ends.");
}
}
前台线程(Foreground Threads):
这些线程是由主线程或其他前台线程创建的,它们的生命周期独立于主线程,但它们不是主线程的子线程。前台线程与主线程之间的关系是平级的。当所有前台线程都执行完毕时,程序才会退出,无论主线程是否结束。
- 生命周期:
前台线程的生命周期不受其他线程的影响。即使主线程退出,前台线程仍然可以继续执行,直到完成。- 程序退出:
如果程序中还有前台线程在运行,主程序将等待所有前台线程完成后才会退出。主线程也是前台线程,如果主线程退出,会等待其他前台线程完成后再退出。- 影响程序:
前台线程会阻塞程序的退出,直到所有前台线程完成。这可能会影响程序的退出速度。- 默认类型:
== 通过 new Thread(…) 创建的线程默认是前台线程。==
后台线程(Background Threads):
这些线程也是由主线程或其他前台线程创建的,它们同样是平级的,不是主线程的子线程。后台线程与主线程之间的关系也是平级的。当所有前台线程结束,程序会退出,同时会终止所有后台线程,不管后台线程是否执行完毕。
- 生命周期:
后台线程的生命周期受到主线程的影响。如果所有前台线程(包括主线程)都已经完成,程序会立即退出,同时终止后台线程,不管后台线程是否执行完毕。- 程序退出:
如果程序中只剩下后台线程在运行,即使主线程结束,程序也会立即退出,不会等待后台线程完成。- 影响程序:
后台线程不会阻塞程序的退出,它们对程序的退出速度没有影响。- 设置后台线程:
可以通过设置线程的 IsBackground 属性为 true 将线程设置为后台线程。通过 Thread 类创建的线程可以使用这个属性进行设置。
使用场景:
- 前台线程通常用于执行一些关键任务,确保这些任务的完成。例如,在主线程需要等待其他线程的结果时,可以使用前台线程。
- 后台线程通常用于执行一些非关键性的任务,如日志记录、监控等。它们不会阻止程序的退出,适用于在程序退出时不需要保证任务完全执行的情况。
错误使用后台线程,可能引起资源泄露或意外行为
- 资源泄露:
如果后台线程在程序退出时还在执行,可能会导致资源无法正确释放。例如,如果后台线程打开了文件、网络连接或其他资源,但程序退出时这些资源没有被正确关闭,就会发生资源泄露。- 不完整的操作:
如果后台线程执行一些需要完整执行的操作,例如数据的写入、状态的更新等,但程序退出时这些操作未完成,可能会导致数据不一致或损坏。- 异常处理:
后台线程的异常不会被捕获并传播到主线程,可能会导致未处理的异常,影响程序的稳定性。
4.线程同步:
在程序退出时,后台线程可能还在等待某些同步操作完成,但这些操作可能无法在后台线程终止之前完成,可能会导致死锁或其他线程同步问题。
(4)ContinueWith
ContinueWith 是 .NET 中用于创建任务延续的方法之一。延续任务是指在另一个任务完成后执行的任务,你可以对先前的任务的结果、状态和异常进行处理。ContinueWith 方法允许你创建一个在原始任务完成后执行的延续任务,并在延续任务中指定要执行的操作
public async Task CountinueWith()
{
//Task originalTask = Task.Run(() => { throw new NullReferenceException();});
Task originalTask = Task.Run(() => { Thread.Sleep(1000);});
Task continuationTask = originalTask.ContinueWith(prevTask =>
{
// 在原始任务完成后执行的逻辑
if (prevTask.Status == TaskStatus.RanToCompletion)
{
// 处理成功完成的任务结果
Console.WriteLine("Success");
}
else if (prevTask.IsFaulted)
{
// 处理原始任务引发的异常
foreach (var exception in prevTask.Exception.InnerExceptions)
{
Console.WriteLine($"Exception: {exception.Message}");
}
}
}, TaskContinuationOptions.ExecuteSynchronously);
}
1.Thread(线程)
表示一个执行线程,用于并行执行代码。可以使用 Thread 类来创建和管理线程。线程是执行程序的最小单位,多线程编程允许程序同时执行多个任务,从而提高性能和响应性。
Thread 类是 C# 中用于线程操作的基础类之一。然而,对于更高级的线程编程需求,你可能会使用 Task、ThreadPool、异步编程模型等更高级的机制,以便更好地管理和协调多线程操作。
Thead常用方法
- Start(): 启动线程,使其开始执行指定的方法。
- Join(): 阻塞当前线程,直到目标线程完成。
- Abort(): 强制终止线程的执行。不建议使用,因为可能导致资源泄漏或不稳定的状态。
- Sleep(int millisecondsTimeout): 使当前线程休眠指定的毫秒数。
- IsAlive(): 返回一个布尔值,指示线程是否处于活动状态。
- Interrupt(): 中断线程,引发一个 ThreadInterruptedException 异常。
- Suspend() 和 Resume(): 已过时,不推荐使用。用于暂停和恢复线程的执行。
- GetDomain() 和 GetDomainID(): 获取线程所属的应用程序域和域标识符。
- SetApartmentState(ApartmentState state): 设置线程的单元状态,用于控制线程的COM互操作行为。
- GetCurrentThreadId() 和 GetDomainID(): 获取当前线程的唯一标识符。
- Interrupt(): 中断线程的等待状态,引发 ThreadInterruptedException 异常。
- Yield(): 提示系统允许其他等待线程运行。
- Name 和 CurrentThread.Name: 获取或设置线程的名称。
- SetData 和 GetData: 在线程范围内设置和获取线程本地存储数据。
- Start(ParameterizedThreadStart) 和 Start(ParameterizedThreadStart, Object): 启动线程并传递参数给线程方法。
- TrySetApartmentState(ApartmentState): 尝试设置线程的单元状态,返回是否成功。
- StartNew(Action) 和 StartNew(Action, CancellationToken): 使用 Task 类来启动线程。
这些方法提供了各种线程管理和操作的能力。然而,需要注意,一些方法已经过时,不推荐使用,而且一些方法可能会涉及多线程编程的复杂性,需要谨慎使用。在编写多线程应用程序时,确保仔细阅读文档并根据需求选择适当的方法。
(1)创建线程
通常,你需要传递一个方法作为线程的入口点,然后调用 Start 方法来启动线程。
using System;
using System.Threading;
class Program
{
static void Main()
{
Thread thread = new Thread(WorkerMethod);
thread.Start(); // 启动线程
}
static void WorkerMethod()
{
Console.WriteLine("Thread is running.");
}
}
(2) 线程同步
在多线程环境中,线程同步是一种确保多个线程协调工作的机制。Thread 类提供了 Join 方法,允许一个线程等待另一个线程完成。这在需要等待某个线程的结果时特别有用。
using System;
using System.Threading;
class Program
{
static void Main()
{
Thread currentThread = Thread.CurrentThread;
Console.WriteLine($"Thread ID: {currentThread.ManagedThreadId}");
Thread thread = new Thread(WorkerMethod);
thread.Start();
// 主线程等待子线程完成
thread.Join();
Console.WriteLine("Thread has finished.");
}
static void WorkerMethod()
{
Thread currentThread = Thread.CurrentThread;
Console.WriteLine($"Thread ID: {currentThread.ManagedThreadId}");
Console.WriteLine("Thread is running.");
Thread.Sleep(2000); // 模拟耗时操作
}
}
(3)线程异步
using System;
using System.Threading;
class Program
{
static void Main()
{
Thread currentThread = Thread.CurrentThread;
Console.WriteLine($"Thread ID: {currentThread.ManagedThreadId}");
Thread thread = new Thread(WorkerMethod);
thread.Start();
// 主线程等待子线程完成
//thread.Join();
Console.WriteLine("Thread has finished.");
//这里子线程虽然还没有处理完,但是直接返回了,没有继续等待子线程,但是子线程还在继续处理工作,没有出现阻塞现象
return "ok";
}
static void WorkerMethod()
{
Thread currentThread = Thread.CurrentThread;
Console.WriteLine($"Thread ID: {currentThread.ManagedThreadId}");
Console.WriteLine("Thread is running.");
Thread.Sleep(10000); // 模拟耗时操作
//这里在主线程结束后,继续在处理10s后打印Thread is WordEnd;
Console.WriteLine("Thread is WordEnd.");
}
}
可以思考下,主线程返回成功了,但是子线程执行失败了,这可怎么办?
(4)Thead的并发与并行
1)并发
在并发操作中,多个任务在不同的时间段内交替执行,从宏观上看,它们似乎是在同时运行。
using System;
using System.Threading;
class Program
{
static void Main()
{
Thread thread1 = new Thread(DoWork);
Thread thread2 = new Thread(DoWork);
thread1.Start();
thread2.Start();
thread1.Join();
thread2.Join();
Console.WriteLine("Both threads completed.");
}
static void DoWork()
{
for (int i = 0; i < 5; i++)
{
Console.WriteLine($"Thread {Thread.CurrentThread.ManagedThreadId}: Iteration {i}");
Thread.Sleep(100);
}
}
}
2)并行
在并行操作中,多个任务在多个处理器核心上同时执行,从而实现真正的并行计算。
Parallel 类会自动将任务分区并分配到多个线程上,以实现负载均衡和并行执行。它会根据系统资源和硬件情况智能地管理线程池中的线程。
using System;
using System.Threading.Tasks;
class Program
{
static void Main()
{
Parallel.For(0, 5, i =>
{
Console.WriteLine($"Thread {Thread.CurrentThread.ManagedThreadId}: Iteration {i}");
});
Console.WriteLine("Parallel loop completed.");
}
}
Parallel 类是 .NET Framework(包括 .NET Core 和 .NET 5+)中提供的一个用于并行编程的工具,它旨在简化多线程和并行操作的编写,提高代码的性能和可读性。Parallel 类提供了一些方法,可以帮助开发者更轻松地实现并行计算。以下是 Parallel 类的一些主要方法和特性的详细解释:
//1. Parallel.For
Parallel.For(0, 10, i =>
{
// 并行执行的操作
});
//Parallel.ForEach 方法类似于 Parallel.For,但用于并行迭代集合元素。它接受一个集合和一个 Lambda 表达式,用于指定集合中每个元素的操作。
var numbers = Enumerable.Range(0, 10);
Parallel.ForEach(numbers, num =>
{
// 并行执行的操作
});
//Parallel.Invoke 方法用于同时执行多个操作。它接受多个 Lambda 表达式作为参数,这些表达式指定了要并行执行的操作。
Parallel.Invoke(
() => { /* 操作1 */ },
() => { /* 操作2 */ },
() => { /* 操作3 */ }
);
//Parallel 类还提供了一些扩展方法,如 AsParallel,用于使 LINQ 查询并行化,以便在多个线程上并行执行查询操作。
var result = source.AsParallel().Where(item => item.Condition).Select(item => item.Value);
//Parallel 类提供了一些静态属性,如 MaxDegreeOfParallelism,用于配置并行操作的最大并发度。
Parallel.MaxDegreeOfParallelism = Environment.ProcessorCount; // 设置最大并发度为处理器核心数
2.ThreadPool(线程池)
线程池(ThreadPool)是一种用于管理和复用线程的技术,旨在提高多线程编程的性能和效率。线程池允许在应用程序中维护一组预先创建的线程,这些线程可以在需要时被重复使用来执行任务,而不必频繁地创建和销毁线程。
线程池的主要优势包括:
- 减少线程创建和销毁的开销: 创建和销毁线程是昂贵的操作,会消耗大量的系统资源。线程池可以避免这种开销,通过重用现有线程来执行多个任务。
- 提高系统性能:
线程池可以有效地管理线程的数量,防止过多的线程竞争系统资源,从而提高系统的性能和响应速度。- 控制并发度:线程池允许您控制同时执行的线程数量,以防止系统资源被耗尽。这有助于避免过度并发和线程之间的竞争。
- 简化编程: 使用线程池可以简化多线程编程,您只需将任务提交到线程池,而不必手动管理线程的生命周期。
.NET Core API 中默认情况下会使用一个称为线程池(ThreadPool)的机制来管理和调度线程。线程池是一种用于管理多个工作线程的技术,它可以帮助您更有效地管理系统资源,避免频繁地创建和销毁线程。在 .NET Core API 中,默认情况下,您可以使用线程池来执行异步操作,如通过 Task.Run、async 和 await 等方式。线程池会根据系统的资源状况自动管理线程的数量,并尝试最优地分配线程以提高性能。
需要注意的是,使用 async 和 await 并不代表一定会使用线程池中的线程。某些异步操作(例如 I/O 操作)可能会利用其他机制,如异步 I/O,而不一定会占用线程池中的线程。但总体而言,async 和 await 是一种有效地利用线程池资源来处理异步任务的方式。
using System;
using System.Threading.Tasks;
class Program
{
static async Task Main(string[] args)
{
Console.WriteLine("Start of Main");
// 使用线程池执行异步操作
await Task.Run(() =>
{
Console.WriteLine("Async operation in thread pool");
});
Console.WriteLine("End of Main");
}
}
(1)查看默认线程池的信息
- 最小线程数(MinThreads):
最小线程数是线程池中允许的最少活动线程数。当线程池中的线程数低于最小线程数时,线程池会自动创建新的线程,以满足活动任务的需求。最小线程数的设置可以确保在线程池中始终有一定数量的线程可用,从而减少任务启动和销毁线程的开销。- 最大线程数(MaxThreads)
最大线程数是线程池中允许的最大活动线程数。当线程池中的线程数达到最大线程数时,线程池不会再创建新的线程。超过最大线程数的任务会排队等待,直到有线程可用为止。设置适当的最大线程数可以防止线程池无限制地创建过多的线程,从而导致资源耗尽或性能下降。
public void TheadPoolInfo()
{
//获取当前线程池的线程数
int workerThreads, ioThreads;
ThreadPool.GetAvailableThreads(out workerThreads, out ioThreads);
Console.WriteLine($"Available worker threads: {workerThreads}");
Console.WriteLine($"Available I/O threads: {ioThreads}");
//获取当前线程池的最大线程数:
int maxWorkerThreads, maxIoThreads;
ThreadPool.GetMaxThreads(out maxWorkerThreads, out maxIoThreads);
Console.WriteLine($"Max worker threads: {maxWorkerThreads}");
Console.WriteLine($"Max I/O threads: {maxIoThreads}");
//获取当前线程池的最小线程数
int minWorkerThreads, minIoThreads;
ThreadPool.GetMinThreads(out minWorkerThreads, out minIoThreads);
Console.WriteLine($"Min worker threads: {minWorkerThreads}");
Console.WriteLine($"Min I/O threads: {minIoThreads}");
}
(2)工作线程与I/O线程
工作线程(Worker Threads)和 I/O 线程(I/O Threads)是线程池中的两种不同类型的线程,用于执行不同类型的任务。线程池通过合理分配这些线程,以提高多线程编程的性能和效率。
工作线程(Worker Threads):
- 工作线程主要用于执行计算密集型的任务,即需要进行大量计算和处理的操作。这些线程执行 CPU 密集型的工作,如执行复杂的算法、数据处理、数值计算等。
- 工作线程执行的任务可能会占用较长的 CPU 时间,因此线程池会根据需要创建和回收工作线程,以便高效地利用系统资源。
- 线程池维护工作线程的数量,确保系统不会过度并发,避免消耗过多的 CPU 资源。
I/O 线程(I/O Threads):
- I/O 线程主要用于执行 I/O 操作,如文件读写、网络通信、数据库访问等。这些操作通常涉及等待外部资源的响应,因此适合异步执行,以充分利用系统的并发性能。
- I/O 线程执行的任务通常不会占用大量的 CPU 时间,大部分时间都是在等待 I/O 完成。因此,线程池可以创建更多的 I/O 线程,以处理多个 I/O 操作。
- 通过异步执行 I/O 操作,可以避免阻塞主线程,提高应用程序的响应性。
3.Task(任务)
Task 是 .NET Framework 和 .NET Core(包括 .NET 5+)中用于表示异步操作的类型。它是异步编程的核心概念之一,用于管理和操作异步操作的状态、结果和执行。
(1)Task与Thead的关系
Task 和 Thread 在并发和异步编程中都有各自的作用和优势。在现代的 C# 编程中,通常优先考虑使用 Task 进行异步操作,Task是Thead的更高的抽象和更简洁的代码。同时,了解 Thread 也是重要的,因为在某些情况下可能需要直接控制线程的创建和管理。
(2)Task的相关用法
1) Task.Start()&&Task.Run()&&Task.Factory.StartNew
- Task.Factory.StartNew 是一个较早版本的任务启动方式,它提供了更多的选项来配置任务的行为,例如任务的调度选项、任务类型等。这个方法在创建和启动任务时提供了更多的灵活性,但由于提供了很多选项,使用起来可能会更复杂。
Task task = Task.Factory.StartNew(() => Console.WriteLine("Task is running."));
- Task.Start() 是用于手动启动一个已创建但尚未启动的 Task。在使用这个方法之前,你需要先通过 new Task(…) 来创建一个任务,然后可以调用 Start() 方法来开始任务的执行。这个方法允许你更细粒度地控制任务的启动时间,你可以在适当的时候手动启动任务。
Task task = new Task(() => Console.WriteLine("Task is running."));
// 手动启动任务
task.Start();
- Task.Run() 是一个便捷的方法,用于启动一个新的 Task 并立即运行指定的操作。它封装了任务的创建和启动过程,使代码更简洁。Task.Run() 方法将操作排入线程池,这意味着它会在后台线程上执行。
Task task = Task.Run(() => Console.WriteLine("Task is running."));
2) Task.WhenAll()&&Task.WaitAll
- Task.WhenAll:
Task.WhenAll 是异步方法,返回一个代表所有传入任务的新任务(Task)。这个新任务在所有传入的任务都完成时变为完成状态。
它适用于异步编程场景,不会阻塞主线程,能够更好地利用并行执行的优势。
如果传入的任何一个任务引发异常,这些异常会被封装在 AggregateException 中,并在等待的代码上下文中传播。
public async Task WhenAll()
{
Task task1 = Task.Run(() => {
for (int i = 0; i < 5; i++)
{
Console.WriteLine("Task 1 - iteration {0}", i);
Thread.Sleep(1000);
}
Console.WriteLine("Task 1 complete");
});
Task task2 = Task.Run(() => {
Console.WriteLine("Task 2 complete");
});
// 等待所有任务完成
try
{
Console.WriteLine("等待任务完成");
Task.WhenAll(task1, task2);
Console.WriteLine("任务完成");
}
catch (AggregateException ae)
{
foreach (var innerException in ae.InnerExceptions)
{
Console.WriteLine($"Inner Exception: {innerException.GetType()}: {innerException.Message}");
}
}
}
- Task.WaitAll:
Task.WaitAll 是同步方法,会阻塞当前线程,直到所有传入的任务都完成。
它适用于同步编程场景,如果在主线程中使用,会导致主线程阻塞,影响应用程序的响应性。
如果传入的任何一个任务引发异常,异常将会立即抛出,不会等待所有任务完成后再抛出。
public async Task WaitAll()
{
Task task1 = Task.Run(() => {
for (int i = 0; i < 5; i++)
{
Console.WriteLine("Task 1 - iteration {0}", i);
Thread.Sleep(1000);
}
Console.WriteLine("Task 1 complete");
});
Task task2 = Task.Run(() => {
Console.WriteLine("Task 2 complete");
});
// 等待所有任务完成
try
{
Console.WriteLine("等待任务完成");
Task.WaitAll(task1, task2);
Console.WriteLine("任务完成");
}
catch (AggregateException ae)
{
foreach (var innerException in ae.InnerExceptions)
{
Console.WriteLine($"Inner Exception: {innerException.GetType()}: {innerException.Message}");
}
}
}
3)WhenAny&&WaitAny
- Task.WhenAny 是异步方法,返回一个新的 Task 对象,该对象会在传入的任意一个任务完成时变为完成状态。
- 它适用于异步编程场景,不会阻塞当前线程,能够更好地利用异步操作的特性。
- 返回的 Task 对象会包含第一个完成的任务,你可以通过检查返回的任务的状态和结果来确定哪个任务完成了。
public async Task WhenAny()
{
Task task1 = Task.Run(() =>
{
Console.WriteLine("skdjfl");
return Task.Delay(5000);
});
Task task2 = Task.Run(() => Task.Delay(10000));
Console.WriteLine("Any开始");
Task firstCompletedTask = Task.WhenAny(task1, task2);
Console.WriteLine("Any完成");
}
- Task.WaitAny 是同步方法,会阻塞当前线程,直到传入的任意一个任务完成。
- 它适用于同步编程场景,如果在主线程中使用,会导致主线程阻塞,影响应用程序的响应性。
- 返回的是第一个完成的任务的索引,你可以通过索引获取完成的任务。
public async Task WaitAny()
{
Task task1 = Task.Run(() =>
{
Console.WriteLine("skdjfl");
return Task.Delay(5000);
});
Task task2 = Task.Run(() => Task.Delay(10000));
Console.WriteLine("Any开始");
int firstCompletedTask = Task.WaitAny(task1, task2);
Console.WriteLine("Any完成");
}
(2)任务状态
Task 有不同的状态,包括等待状态、运行状态、完成状态等。你可以查询任务的状态以确定它是否已经完成或正在执行。
public async Task SpinLock()
{
Task<int> task = PerformAsyncOperation();
Console.WriteLine($"Task status before await: {task.Status}");
int result = await task;
Console.WriteLine($"Task status after await: {task.Status}");
Console.WriteLine($"Async operation completed with result: {result}");
}
static async Task<int> PerformAsyncOperation()
{
await Task.Delay(1000); // 模拟异步操作
return 42;
}
(3)任务结果
- 任务结果类型: 任务可以返回不同类型的结果,这取决于异步操作的返回值类型。常见的任务结果类型包括:
Task:没有返回值的任务。
Task T:返回一个泛型类型的结果,例如 Task 表示异步操作返回一个整数结果。
static async Task<int> PerformAsyncOperation()
{
await Task.Delay(1000); // 模拟异步操作
return 42;
}
4.Task Parallel Library (TPL)(任务并行库)
是 C# 中用于并行编程的高级库,用于处理异步和并行操作,包括数据并行和任务并行。
(1)Parallel.For
Parallel.For 方法用于并行执行一个数字范围内的循环迭代。它接受起始值、终止值和一个委托,用于定义迭代任务的操作。每个迭代都可以并行地执行在不同的处理器核心上。
public async Task ParallelFor()
{
int startIndex = 1;
int endIndex = 10;
Parallel.For(startIndex, endIndex + 1, index =>
{
int square = index * index;
Console.WriteLine($"Square of {index} is {square}");
});
}
(2)Parallel.ForEach
Parallel.ForEach 方法用于并行执行一个集合的循环迭代。它接受一个集合和一个委托,用于定义迭代任务的操作。每个集合元素都可以并行地在不同的处理器核心上执行。
public async Task ParallelForEach()
{
List<string> words = new List<string> { "apple", "banana", "cherry", "date" };
Parallel.ForEach(words, word =>
{
string upperCaseWord = word.ToUpper();
Console.WriteLine($"Uppercase version of {word} is {upperCaseWord}");
});
}
ParallelOptions 对象,设置并行操作的选项,如最大并行度、取消标记等。
ParallelOptions options = new ParallelOptions
{
MaxDegreeOfParallelism = 4, // 设置最大并行度
CancellationToken = cancellationToken // 设置取消标记
};
Parallel.ForEach(collection, options, item =>
{
// 并行任务的操作
});
Parallel.For(startIndex, endIndex, options, index =>
{
// 并行任务的操作
});
Parallel.Break 和 ParallelLoopState.Break: 在并行循环中,可以使用 Parallel.Break 和 ParallelLoopState.Break 来中断循环的执行。
Parallel.For(0, 100, (i, state) =>
{
if (i == 50)
{
state.Break(); // 中断循环的执行
}
// 循环任务的操作
});
(3)并行 LINQ(PLINQ)
TPL 还引入了并行 LINQ(PLINQ),允许你在集合上执行并行查询。PLINQ 可以自动将 LINQ 查询中的操作并行化,从而提高查询的性能。
int[] numbers = Enumerable.Range(1, 1000000).ToArray();
var parallelQuery = numbers.AsParallel()
.Where(x => x % 2 == 0)
.Select(x => Math.Pow(x, 2));
foreach (var result in parallelQuery)
{
Console.WriteLine(result);
}
(4)Parallel.Invoke
Parallel.Invoke 是 Task Parallel Library(TPL)中用于并行调用多个方法的方法。它允许你在不同的处理器核心上同时执行多个方法,从而实现方法级别的并行操作。这在某些情况下可以提高应用程序的性能,特别是当有多个独立的任务需要同时执行时。
- 需要注意的是,Parallel.Invoke 方法会等待所有并行调用的方法都完成后才会继续执行后续的代码。在本示例中,主线程会等待所有方法完成后输出 “All methods completed.”。
- Parallel.Invoke 适用于那些需要同时执行的独立方法,但不适合在方法之间共享数据或进行复杂的同步操作。如果方法之间需要进行数据交互或同步操作,可能需要考虑其他并发控制机制。
using System;
using System.Threading.Tasks;
class Program
{
static void Method1()
{
Console.WriteLine("Method1 started");
// 方法1的逻辑
Console.WriteLine("Method1 completed");
}
static void Method2()
{
Console.WriteLine("Method2 started");
// 方法2的逻辑
Console.WriteLine("Method2 completed");
}
static void Method3()
{
Console.WriteLine("Method3 started");
// 方法3的逻辑
Console.WriteLine("Method3 completed");
}
static void Main()
{
Parallel.Invoke(
() => Method1(),
() => Method2(),
() => Method3()
);
Console.WriteLine("All methods completed.");
}
}
5.Async/Await(异步/等待)
是 C# 5.0 引入的异步编程模型,用于创建和管理异步方法和操作。
async 和 await 是 C# 中用于处理异步编程的关键字,它们在 Task Parallel Library(TPL)中引入,用于简化异步操作的编写和管理。通过使用 async 和 await,你可以以一种更清晰、更易于理解的方式编写异步代码,而无需显式地使用回调函数或处理线程。
- async 关键字:
async 关键字用于修饰方法,表明该方法是一个异步方法。异步方法在执行时不会阻塞调用线程,而是可以在异步任务执行的同时执行其他代码。异步方法返回一个 Task、Task 或 ValueTask 对象,表示异步操作的结果。
- await 关键字:
await 关键字用于在异步方法内等待一个异步操作完成。当遇到 await 关键字时,方法会暂时挂起,允许调用线程在等待异步操作完成的同时执行其他操作。一般情况下,await 后面会跟着一个返回 Task 或 Task 的方法调用。
using System;
using System.Net.Http;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
string url = "https://www.example.com";
string content = await DownloadContentAsync(url);
Console.WriteLine(content);
}
static async Task<string> DownloadContentAsync(string url)
{
using (HttpClient client = new HttpClient())
{
string content = await client.GetStringAsync(url);
return content;
}
}
}
(1).Await与Thead.Start()区别
Await执行效果,按照代码片的顺序执行。
public async Task TheadPoolInfo1()
{
Console.WriteLine("Before first await: " + DateTime.Now);
await DelayAsync();
Console.WriteLine("After first await: " + DateTime.Now);
await Task.Delay(1000); // This is an async method too
Console.WriteLine("After second await: " + DateTime.Now);
}
static async Task DelayAsync()
{
await Task.Delay(2000); // Simulate async operation
Console.WriteLine("Inside DelayAsync: " + DateTime.Now);
}
thead.Start()执行效果,详见目录 三、1.(3)
- Thead.Start 不会阻塞线程,也不会释放当前线程,当前线程与开启的新线程都在运行,所以当前代码片继续执行,直到结束,新线程也在同时进行。
- await 不会阻塞线程,但是会释放掉当前的线程,允许其他操作在等待期间继续执行。同时开启一个子线程,子线程执行完了,会重新执行剩下的代码片。
- 共同点:都开启了新的线程 ,不同点:await释放当前线程,子线程完成后,再继续执行剩下代码。thead.start()不会释放线程,不会等待子线程完成,会直接执行剩下的代码。
(2)Await怎么做到释放线程后,继续执行剩余代码
await 关键字通过将异步操作分解成状态机并利用编译器生成的异步状态机模式,使得在等待异步操作完成期间能够释放线程,并在异步操作完成后继续执行剩下的代码。这种机制允许异步方法在等待耗时操作时不会阻塞主线程或当前线程,从而保持程序的并发性和响应性。
- 当执行到 await 关键字时,异步方法会生成一个状态机,并将后续的代码作为回调保存起来。
- await 关键字会让调用线程(通常是主线程或当前线程)暂时退出该方法,然后返回到调用方,不会阻塞线程。
- 在等待的异步操作完成后,异步方法会从状态机中恢复执行,并将剩余的代码(回调)添加到线程池的任务队列中。
- 线程池中的线程会继续执行剩余的代码,以完成异步操作后的后续工作。
6.Monitor(监视器)
是用于实现线程同步的一种机制,用于保护共享资源,避免竞态条件。可以使用 Monitor 类或 lock 关键字来实现。
首先lock和Minitor有什么区别呢?
其实lock在IL代码中会被翻译成Monitor。也就是Monitor.Enter(obj)和Monitor.Exit(obj).
lock(obj)
{}
//等价为:
try
{
Monitor.Enter(obj)
}
catch()
{}
finally
{
Monitor.Exit(obj)
}
- Monitor 是基于内核的同步机制,可能会带来一些性能开销,特别是在频繁的线程同步操作中。
- 在使用 Monitor 时,要确保使用相同的锁对象来进行进入和退出操作,否则会导致死锁或无法正确同步。
- Monitor 需要谨慎使用,避免出现死锁、饥饿和竞争条件等问题。
7.Semaphore(信号量)
用于控制并发访问资源的数量,可以使用 Semaphore 类来创建和管理信号量。
Semaphore(信号量)是 C# 中用于线程同步的一种同步原语,用于管理资源的并发访问。它提供了一种限制同时访问共享资源的方法,可以用于控制同时执行的线程数量。Semaphore 可以用于解决一些并发编程中的问题,如资源池管理、限制访问频率等。
获取资源(WaitOne):
使用 WaitOne 方法可以尝试获取一个信号量资源。如果有可用资源,线程将继续执行。如果没有可用资源,线程将阻塞等待,直到有资源可用或超时。
semaphore.WaitOne(); // 获取一个信号量资源
释放资源(Release)
使用 Release 方法可以释放一个信号量资源,使其他等待的线程可以继续执行。
semaphore.Release(); // 释放一个信号量资源
using System;
using System.Threading;
class Program
{
static Semaphore semaphore = new Semaphore(2, 2); // 允许同时有两个线程访问资源
static void Main()
{
for (int i = 0; i < 5; i++)
{
Thread thread = new Thread(Worker);
thread.Start(i);
}
}
static void Worker(object id)
{
Console.WriteLine($"Thread {id} is waiting to enter.");
semaphore.WaitOne(); // 获取一个信号量资源
Console.WriteLine($"Thread {id} entered.");
Thread.Sleep(1000); // 模拟工作
Console.WriteLine($"Thread {id} is leaving.");
semaphore.Release(); // 释放一个信号量资源
}
}
// See https://aka.ms/new-console-template for more information
using System.Runtime.CompilerServices;
const int NumThreads = 10; // 并发线程数
const int BufferSize = 5; // 缓存池大小
const int NumIterations = 20; // 每个线程迭代次数
Semaphore semaphore = new Semaphore(BufferSize, BufferSize); // 创建信号量
int[] buffer = new int[BufferSize]; // 缓存数组
for (int i = 0; i < NumThreads; i++)
{
int threadId = i;
new Thread(() => {
for (int j = 0; j < NumIterations; j++)
{
Console.WriteLine($"Thread {threadId} tries to acquire a slot in the buffer.");
semaphore.WaitOne(); // 等待信号量,限制并发数
// 访问共享资源
int index = -1;
lock (buffer)
{
for (int k = 0; k < BufferSize; k++)
{
if (buffer[k] == 0)
{
index = k;
buffer[k] = threadId + 1;
break;
}
}
for (int n = 0; n < BufferSize; n++)
{
Console.WriteLine(buffer[n]);
}
}
// 模拟一定的处理时间
Thread.Sleep(10000);
// 释放信号量
lock (buffer)
{
buffer[index] = 0;
}
semaphore.Release(); // 释放信号量
Console.WriteLine($"Thread {threadId} releases a slot in the buffer.");
}
}).Start();
}
Console.ReadLine();
8.SemaphoreSlim
是 Semaphore 的改进版本,提供更好的性能和可伸缩性。
SemaphoreSlim 是 .NET 中用于线程同步的轻量级同步原语,它是对传统的 Semaphore 的一种改进和优化。与 Semaphore 相比,SemaphoreSlim 在某些情况下可以提供更好的性能和资源利用,尤其是在高并发情况下。它适用于需要限制并发访问数量的场景,类似于 Semaphore,但是更灵活和轻量级。
获取资源(WaitAsync):
使用 WaitAsync 方法可以异步地尝试获取一个信号量资源。如果有可用资源,方法将立即返回。如果没有可用资源,方法将返回一个代表等待的 Task 对象。
await semaphore.WaitAsync(); // 异步获取一个信号量资源
释放资源(Release):
使用 Release 方法可以释放一个信号量资源,使其他等待的线程可以继续执行。
semaphore.Release(); // 释放一个信号量资源
using System;
using System.Threading;
using System.Threading.Tasks;
class Program
{
static SemaphoreSlim semaphore = new SemaphoreSlim(2, 2); // 允许同时有两个线程访问资源
static async Task Main()
{
for (int i = 0; i < 5; i++)
{
_ = Worker(i);
}
await Task.Delay(5000); // 等待所有任务完成
}
static async Task Worker(int id)
{
Console.WriteLine($"Thread {id} is waiting to enter.");
await semaphore.WaitAsync(); // 异步获取一个信号量资源
Console.WriteLine($"Thread {id} entered.");
await Task.Delay(1000); // 模拟工作
Console.WriteLine($"Thread {id} is leaving.");
semaphore.Release(); // 释放一个信号量资源
}
}
- SemaphoreSlim 是一个轻量级的同步原语,相比传统的 Semaphore,它在某些情况下可能提供更好的性能。
- SemaphoreSlim 支持异步等待资源的获取,适用于异步编程场景。
- 在使用 SemaphoreSlim 时,要确保在适当的时候释放资源,以避免出现死锁或资源泄漏等问题。
9.AutoResetEvent(自动复位事件)
AutoResetEvent 是 .NET 中用于线程同步的同步原语之一,它用于实现线程之间的协作和同步。具体来说,AutoResetEvent 提供了一种等待和通知机制,允许一个线程等待某个事件的发生,而另一个线程在事件发生时通知等待的线程继续执行。
创建 AutoResetEvent:
使用 AutoResetEvent 的构造函数可以创建一个自动复位的事件对象。初始状态可以指定为有信号(true)或无信号(false)。
//这个就是复位信号,当Thread1在WaitOne的时候,这里为true则继续执行下一段代码
AutoResetEvent autoResetEvent = new AutoResetEvent(initialState);
等待事件(WaitOne):
使用 WaitOne 方法可以使线程等待事件的发生。如果事件没有发生,线程将阻塞等待。一旦事件发生,WaitOne 方法将返回,线程继续执行。
autoResetEvent.WaitOne(); // 等待事件发生
通知事件(Set):
使用 Set 方法可以通知等待的线程事件已经发生,从而使其中一个线程继续执行。
//设置复位信号为true,
autoResetEvent.Set(); // 通知事件发生
AutoResetEvent 适用于一对一的线程通信和协作,一旦一个线程等待的事件发生,该事件会自动复位为无信号状态,需要再次通知才能继续等待。
AutoResetEvent 的主要用途是在不同线程之间同步操作,通常用于线程等待某个条件满足,然后继续执行。
初始化状态为true时,线程1直接执行剩余代码,WaitOne及Set方法调用后,自动复位都会变成无信号状态。
static AutoResetEvent autoResetEvent = new AutoResetEvent(false);
public void AutoResetEvent()
{
Thread thread1 = new Thread(Worker1);
Thread thread2 = new Thread(Worker2);
thread1.Start();
thread2.Start();
thread1.Join();
thread2.Join();
}
static void Worker1()
{
Console.WriteLine("Worker 1 is waiting for event.");
autoResetEvent.WaitOne(); // 等待事件发生
Console.WriteLine("Worker 1 received event.");
}
static void Worker2()
{
Console.WriteLine("Worker 2 is sleeping.");
Thread.Sleep(2000); // 模拟工作
Console.WriteLine("Worker 2 is signaling event.");
autoResetEvent.Set(); // 通知事件发生
}
10.ManualResetEvent(手动复位事件)
用于线程同步,允许一个线程等待多个线程发出信号。
static ManualResetEvent manualResetEvent = new ManualResetEvent(false);
public void ManualResetEvent()
{
Thread thread1 = new Thread(Worker3);
Thread thread2 = new Thread(Worker4);
thread1.Start();
thread2.Start();
thread1.Join();
thread2.Join();
}
static void Worker3()
{
Console.WriteLine("Worker 1 is waiting for event.");
manualResetEvent.WaitOne(); // 等待事件发生
Console.WriteLine("Worker 1 received event.");
}
static void Worker4()
{
Console.WriteLine("Worker 2 is sleeping.");
Thread.Sleep(2000); // 模拟工作
Console.WriteLine("Worker 2 is signaling event.");
manualResetEvent.Set(); // 通知事件发生
manualResetEvent.Reset(); // 手动复位事件为无信号状态
}
AutoResetEvent:在调用 Set() 方法后,会自动复位为无信号状态,等待下一个等待的线程。只有一个等待线程可以通过。
ManualResetEvent:在调用 Set() 方法后,不会自动复位,需要手动调用 Reset() 方法来复位。多个等待线程可以同时通过。
11.CancellationToken(取消标记)
CancellationToken 是 .NET 中用于取消操作的一种机制,它允许你在多线程或异步操作中有效地通知任务停止,并进行资源清理。CancellationToken 通常与异步方法一起使用,以提供一种可靠的取消机制,以避免不必要的计算或资源浪费。
创建 CancellationToken:
使用 CancellationTokenSource 可以创建一个 CancellationToken 实例,用于控制取消操作。CancellationTokenSource 同时提供了一个 Token 属性,通过该属性可以获得 CancellationToken。
CancellationTokenSource cts = new CancellationTokenSource();
CancellationToken cancellationToken = cts.Token;
传播取消信号:
当你想要取消某个操作时,可以调用 CancellationTokenSource 的 Cancel() 方法来传播取消信号。所有使用该 CancellationToken 的任务将会收到取消请求。
cts.Cancel(); // 发出取消信号
检查取消请求:
在异步方法中,你可以通过检查 CancellationToken 的 IsCancellationRequested 属性来判断是否已经发出了取消请求。如果已取消,你可以根据情况执行清理操作并提前结束任务。
if (cancellationToken.IsCancellationRequested)
{
// 执行清理操作
cancellationToken.ThrowIfCancellationRequested(); // 抛出异常以退出任务
}
using System;
using System.Threading;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
CancellationTokenSource cts = new CancellationTokenSource();
CancellationToken cancellationToken = cts.Token;
Task longRunningTask = LongRunningAsync(cancellationToken);
// 模拟等待一段时间后取消操作
await Task.Delay(1000);
cts.Cancel(); // 发出取消信号
try
{
await longRunningTask;
}
catch (TaskCanceledException)
{
Console.WriteLine("Operation was canceled.");
}
}
static async Task LongRunningAsync(CancellationToken cancellationToken)
{
while (!cancellationToken.IsCancellationRequested)
{
Console.WriteLine("Working...");
await Task.Delay(200);
}
}
}
12.volatile(易失性修饰符)
volatile 是一种关键字(修饰符),用于在多线程环境中标识一个字段,以指示编译器和运行时系统该字段可能被多个线程同时访问和修改。在某些情况下,使用 volatile 可以确保字段的可见性和一致性,以防止由于编译器优化或 CPU 缓存等原因引发的不正确的结果。
- 可见性:
volatile 保证当一个线程修改了标记为 volatile 的字段的值时,其他线程可以立即看到最新的值。这是通过防止编译器将字段的读取或写入优化掉实现的。- 禁止优化:
volatile 可以阻止编译器对标记字段的读取和写入进行某些优化,以确保每次访问都会从内存中获取最新的值,而不是从缓存中读取。- 顺序性:
在某些情况下,volatile 也可以用来保证操作的顺序性,即确保标记字段的读写操作按照预期的顺序进行,不会被重排序。- 不适用情况:
尽管 volatile 可以解决某些多线程问题,但它并不是解决所有问题的万能解决方案。它不能替代锁定(lock)等更强大的同步机制,特别是在需要复合操作时。
class Program
{
private static volatile bool flag = false;
static void Main()
{
Thread thread1 = new Thread(Worker);
Thread thread2 = new Thread(Worker);
thread1.Start();
thread2.Start();
Thread.Sleep(1000);
flag = true; // 修改标志为 true,通知线程退出
thread1.Join();
thread2.Join();
}
static void Worker()
{
while (!flag)
{
Console.WriteLine("Working...");
}
}
}
volatile 为什么不能替代lock
volatile 和 lock 都是用于处理多线程并发问题的机制,但它们的作用和适用场景有很大的区别,因此不能简单地用 volatile 替代 lock,主要有以下几个原因:
- 可见性 vs. 原子性:
volatile:主要用于保证字段的可见性,即一个线程修改了标记为 volatile 的字段后,其他线程可以立即看到最新的值。但它不能保证复合操作的原子性,例如自增或自减等操作。
lock:用于提供临界区的互斥性,以确保某一时刻只有一个线程可以访问被锁定的代码块,从而保证了复合操作的原子性。- 复合操作:
多线程场景中,一些操作可能由多个步骤组成,例如读取一个值并修改它。volatile 无法保证整个操作是原子的,而 lock 可以确保在同一时间只有一个线程执行临界区的代码,从而确保复合操作的原子性。- 性能:
volatile:由于它只处理字段的可见性,所以在某些情况下会比较轻量级,但它无法处理复杂的同步需求。
lock:提供了更强大的同步机制,但由于涉及到线程的上下文切换和资源竞争,可能会引入一些性能开销。- 等待机制:
volatile:没有提供等待机制,当一个线程在一个标记为 volatile 的字段上等待时,它只能不断地进行检查。这会导致一些 CPU 资源的浪费。
lock:通过进入临界区并在临界区内等待来解决并发问题,这可以减少资源浪费。
13.Mutex(互斥锁)
Mutex 是 .NET 中用于多线程同步的同步原语之一,用于实现多个线程之间的互斥访问共享资源。Mutex 提供了一种互斥锁定机制,以确保在任何时刻只有一个线程可以进入临界区,从而避免并发问题。以下是关于 Mutex 的详细解释和用法:
创建 Mutex:
使用 Mutex 的构造函数可以创建一个互斥体(Mutex)实例,互斥体的名称可以是全局唯一的。
Mutex mutex = new Mutex(); // 创建一个互斥体
等待互斥锁(WaitOne):
使用 WaitOne 方法可以使线程等待互斥锁,如果互斥锁当前被其他线程占用,那么当前线程将被阻塞,直到互斥锁被释放。
mutex.WaitOne(); // 等待互斥锁
释放互斥锁(ReleaseMutex):
使用 ReleaseMutex 方法可以释放互斥锁,允许其他线程进入临界区。
mutex.ReleaseMutex(); // 释放互斥锁
using System;
using System.Threading;
class Program
{
static Mutex mutex = new Mutex();
static void Main()
{
Thread thread1 = new Thread(Worker);
Thread thread2 = new Thread(Worker);
thread1.Start();
thread2.Start();
thread1.Join();
thread2.Join();
}
static void Worker()
{
mutex.WaitOne(); // 等待互斥锁
Console.WriteLine("Working...");
Thread.Sleep(1000); // 模拟工作
Console.WriteLine("Done.");
mutex.ReleaseMutex(); // 释放互斥锁
}
}
Mutex与lock区别
Mutex 和 lock 都是用于实现多线程同步的机制,但它们在实现方式、适用场景和特点方面有一些区别。以下是 Mutex 和 lock 的主要区别:
- 实现方式:
Mutex:是一个操作系统级别的同步原语,可以用于不同进程之间的同步,适用于跨进程的互斥访问。
lock:是 .NET 中的一个关键字,用于在同一个进程中的多个线程之间实现同步,适用于同一进程内的多线程同步。
适用场景:- Mutex:适用于多个进程之间需要互斥访问共享资源的情况,如多个应用程序之间的同步。
lock:适用于同一进程内的多线程同步,通常用于避免多个线程同时访问临界区。- 范围:
Mutex:可以在多个进程中使用,因此范围更广泛。
lock:只适用于同一进程内的多线程同步。- 性能:
Mutex:由于是操作系统级别的同步原语,使用上可能会比较重量级,涉及到用户态和内核态的切换,性能开销相对较高。
lock:通常比较轻量级,性能相对较好,适用于同一进程内的线程同步。- 异常处理:
Mutex:需要显式地释放,通常使用 WaitOne 和 ReleaseMutex 方法。需要注意适当的异常处理,以确保资源得到释放。
lock:使用 lock 关键字时,会自动处理资源的释放,不需要显式调用方法,且在异常抛出时会自动释放资源。
14.ReaderWriterLock(读写锁)
ReaderWriterLock 是 .NET 中用于多线程同步的一种机制,它提供了对共享资源的读取和写入操作的控制,以实现高效的读取操作并保证写入操作的独占性。ReaderWriterLock 可以有效地提高读取操作的并发性能,同时保证写入操作的一致性
- 读取锁和写入锁:
ReaderWriterLock 支持两种锁定模式:读取锁和写入锁。多个线程可以同时获取读取锁,以实现并发的读取操作。但只有一个线程可以获取写入锁,以确保独占性的写入操作。
创建 ReaderWriterLock:
使用 ReaderWriterLock 的构造函数可以创建一个 ReaderWriterLock 实例。
ReaderWriterLock rwLock = new ReaderWriterLock();
获取读取锁(AcquireReaderLock):
使用 AcquireReaderLock 方法可以获取读取锁,允许多个线程同时读取共享资源。
rwLock.AcquireReaderLock(Timeout.Infinite); // 获取读取锁
获取写入锁(AcquireWriterLock):
使用 AcquireWriterLock 方法可以获取写入锁,只允许一个线程进行写入操作。
rwLock.AcquireWriterLock(Timeout.Infinite); // 获取写入锁
释放锁(ReleaseReaderLock 和 ReleaseWriterLock):
使用 ReleaseReaderLock 方法释放读取锁,使用 ReleaseWriterLock 方法释放写入锁。
rwLock.ReleaseReaderLock(); // 释放读取锁
rwLock.ReleaseWriterLock(); // 释放写入锁
using System;
using System.Threading;
class Program
{
static ReaderWriterLock rwLock = new ReaderWriterLock();
static int sharedResource = 0;
static void Main()
{
Thread writerThread = new Thread(Write);
Thread readerThread1 = new Thread(Read);
Thread readerThread2 = new Thread(Read);
writerThread.Start();
readerThread1.Start();
readerThread2.Start();
writerThread.Join();
readerThread1.Join();
readerThread2.Join();
}
static void Write()
{
rwLock.AcquireWriterLock(Timeout.Infinite);
sharedResource = 42; // 写入操作
Console.WriteLine("Writer wrote: " + sharedResource);
rwLock.ReleaseWriterLock();
}
static void Read()
{
rwLock.AcquireReaderLock(Timeout.Infinite);
Console.WriteLine("Reader read: " + sharedResource); // 读取操作
rwLock.ReleaseReaderLock();
}
}
15.ReaderWriterLockSlim(轻量级读写锁)
ReaderWriterLockSlim 是 .NET Framework 中用于多线程同步的一种机制,它是对传统的 ReaderWriterLock 的改进版本,提供了更好的性能和可伸缩性。ReaderWriterLockSlim 适用于需要同时支持读取和写入操作的场景,可以提高并发性能并减少写入线程的饥饿问题
升级和降级锁:
ReaderWriterLockSlim 支持升级和降级锁,可以在读取锁的基础上获取写入锁,或者在写入锁的基础上获取读取锁。
rwLock.EnterUpgradeableReadLock(); // 获取升级锁
rwLock.ExitUpgradeableReadLock(); // 释放升级锁
using System;
using System.Threading;
class Program
{
static ReaderWriterLockSlim rwLock = new ReaderWriterLockSlim();
static int sharedResource = 0;
static void Main()
{
Thread writerThread = new Thread(Write);
Thread readerThread1 = new Thread(Read);
Thread readerThread2 = new Thread(Read);
writerThread.Start();
readerThread1.Start();
readerThread2.Start();
writerThread.Join();
readerThread1.Join();
readerThread2.Join();
}
static void Write()
{
rwLock.EnterWriteLock();
sharedResource = 42; // 写入操作
Console.WriteLine("Writer wrote: " + sharedResource);
rwLock.ExitWriteLock();
}
static void Read()
{
rwLock.EnterReadLock();
Console.WriteLine("Reader read: " + sharedResource); // 读取操作
rwLock.ExitReadLock();
}
}
16.SpinLock
SpinLock 是 .NET Framework 中用于多线程同步的一种轻量级机制,它适用于需要短时间内获取锁的场景。与传统的锁机制不同,SpinLock 不会引起线程的阻塞,而是采用自旋的方式在循环中尝试获取锁
创建 SpinLock:
使用 SpinLock 的构造函数可以创建一个 SpinLock 实例。
SpinLock spinLock = new SpinLock(); // 创建一个 SpinLock
获取锁(Enter 和 TryEnter):
使用 Enter 方法可以获取锁,如果锁已经被其他线程占用,当前线程会一直自旋等待。使用 TryEnter 方法可以尝试获取锁,如果锁未被占用,返回 true,否则返回 false。
bool lockTaken = false;
spinLock.Enter(ref lockTaken); // 获取锁,lockTaken 表示是否获取到锁
if (lockTaken)
{
// 执行临界区代码
spinLock.Exit(); // 释放锁
}
释放锁(Exit):
使用 Exit 方法释放锁,将锁标记为可用状态,以便其他线程可以获取锁。
spinLock.Exit(); // 释放锁
using System;
using System.Threading;
class Program
{
static SpinLock spinLock = new SpinLock();
static int sharedResource = 0;
static void Main()
{
Thread thread1 = new Thread(Increment);
Thread thread2 = new Thread(Increment);
thread1.Start();
thread2.Start();
thread1.Join();
thread2.Join();
Console.WriteLine("Final shared resource value: " + sharedResource);
}
static void Increment()
{
bool lockTaken = false;
spinLock.Enter(ref lockTaken);
for (int i = 0; i < 100000; i++)
{
//这里进行简单的计算,耗时非常短,如果用lock进行阻塞的话,开销会大于进行自旋的开销
sharedResource++;
}
spinLock.Exit();
}
}
不加锁结果
加锁结果
在上面的示例中,两个线程并行地增加了共享资源的值。SpinLock 的自旋机制确保了线程在短时间内等待并尝试获取锁,从而减少了线程阻塞的开销。需要注意的是,SpinLock 在自旋等待期间会消耗 CPU 资源,因此适用于短时间内能够获取锁的场景。在某些情况下,如果等待时间较长,可以考虑使用其他锁机制,如 Monitor、Mutex 或 ReaderWriterLockSlim。
SpinLock为什么减少线程阻塞开销
- 避免上下文切换: 在传统的锁机制中,如果一个线程尝试获取锁但锁已被占用,它会被阻塞挂起,从用户态切换到内核态,这会引发上下文切换的开销。上下文切换需要保存当前线程的上下文,加载另一个线程的上下文,这在多线程高并发的情况下可能会耗费较多的资源。而 SpinLock 避免了线程的阻塞,减少了上下文切换的发生,从而降低了开销。
- 短时间内获取锁: SpinLock 适用于需要短时间内获取锁的情况。它通过自旋等待在一个循环中尝试获取锁,如果锁被其他线程持有,自旋的循环会一直运行,直到锁变为可用。自旋的时间通常是短暂的,这意味着线程不会花费太多时间在自旋上,因此不会对系统性能造成较大的影响。
- 减少线程阻塞延迟: 在一些情况下,线程被阻塞挂起后,当锁变为可用时,需要等待操作系统将线程重新调度到运行态,这会引入一定的延迟。SpinLock 的自旋等待能够立即响应锁的可用性,从而避免了阻塞延迟。
17.SpinWait
SpinWait 是 .NET Framework 中的一个结构体,用于实现自旋等待的机制,它可以在自旋等待期间对线程的执行进行优化,避免过度消耗 CPU 资源。SpinWait 提供了一种在自旋等待时逐渐增加自旋次数和增加线程休眠时间的方式,以平衡自旋等待的性能和资源消耗。
SpinWait 的特点:
SpinWait 支持在等待期间的自旋次数逐渐增加,从而减少短期内高强度的 CPU 使用。
如果自旋次数达到一定阈值仍未获取到锁,SpinWait 会根据当前系统的情况决定是否将线程挂起,避免持续高强度的自旋。
使用 SpinWait:
SpinWait 提供了两个主要方法来进行自旋等待:
SpinOnce():执行一次自旋,适合在循环中使用,每次自旋后会逐渐增加自旋次数。
SpinUntil(Func condition):在指定的条件为真之前一直进行自旋,适用于等待某个条件满足的情况。
using System;
using System.Threading;
class Program
{
static void Main()
{
SpinWaitExample();
}
static void SpinWaitExample()
{
SpinWait spinWait = new SpinWait();
for (int i = 0; i < 5; i++)
{
Console.WriteLine($"Spin count: {spinWait.Count}");
spinWait.SpinOnce();
}
}
}
SpinUntil(Func condition) 方法:
SpinUntil(condition) 方法在指定的条件为真之前一直进行自旋等待。它可以用于等待某个条件满足的情况。
SpinWait spinWait = new SpinWait();
bool conditionMet = false;
ThreadPool.QueueUserWorkItem(state =>
{
Thread.Sleep(2000);
conditionMet = true;
});
spinWait.SpinUntil(() => conditionMet);
Console.WriteLine("Condition met!");
18.Barrier(屏障)
Barrier 是 .NET Framework 中用于多线程同步的一种机制,它允许多个线程在达到某个同步点前等待,并在所有参与线程都到达同步点后继续执行。Barrier 通常用于并行计算中,用于将多个并行任务分阶段执行,确保所有任务完成当前阶段后再进入下一个阶段。
创建 Barrier:
使用 Barrier 的构造函数可以创建一个 Barrier 实例,需要指定参与同步的线程数量。
Barrier barrier = new Barrier(participantCount);
参与同步的线程等待:
参与同步的线程可以调用 barrier.SignalAndWait() 方法来等待其他线程到达同步点。当所有参与线程都调用了 SignalAndWait 后,它们会在同步点处等待,直到所有线程都到达。
barrier.SignalAndWait(); // 等待其他线程到达同步点
阶段计数(PhaseCount):
Barrier 具有一个阶段计数,表示当前已经完成了多少个同步阶段。在所有线程到达同步点后,阶段计数会自动增加,然后线程会继续执行下一个阶段。文章来源:https://www.toymoban.com/news/detail-647048.html
int phaseCount = barrier.CurrentPhaseNumber;
完成阶段的回调(PostPhaseAction):
可以通过构造函数指定一个委托,当所有线程完成同步点后,会自动调用这个委托。这样可以在阶段之间执行一些特定的操作。文章来源地址https://www.toymoban.com/news/detail-647048.html
Barrier barrier = new Barrier(participantCount, action => Console.WriteLine("Phase completed."));
using System;
using System.Threading;
using System.Threading.Tasks;
class Program
{
static Barrier barrier = new Barrier(3, barrier => Console.WriteLine("Phase completed."));
static void Main()
{
for (int i = 0; i < 3; i++)
{
Task.Factory.StartNew(PerformTask, i);
}
Console.ReadLine();
}
static void PerformTask(object id)
{
int taskId = (int)id;
for (int phase = 0; phase < 3; phase++)
{
Console.WriteLine($"Task {taskId} - Phase {phase}");
Thread.Sleep(1000); // 模拟任务执行
barrier.SignalAndWait();
}
}
}
到了这里,关于C#多线程开发详解的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!