.NET多线程
.NET多线程,从使用到原理
作为一名.NET开发,官方文档庞大且繁杂,博客文章少且老,每次看见隔壁JAVA的文档详细又齐全,眼泪只能从嘴角流下。多线程的文档更是少的可怜,所以这是对自己知识的一次梳理,同时为.NET生态贡献自己的一份绵薄之力。(文中涉及操作系统、.NET 5的相关CLR,如部分名词解释不到位,请自行百度)。
一、进程和线程的概念
1.1 进程
进程的引入:在传统操作系统中,为了提高资源利用率和系统吞吐量,通常采用多道批处理,将多个程序同时装入内存中,使之并发处理。为了能够对并发执行的程序加以控制和描述,引入了“进程”的概念。
进程的定义:
关于对进程定义,从不同角度有不同的定义,比较常见的有:
-
进程是程序的一次执行的实例
-
进程是一个程序及其数据在处理机上顺序执行时所发生的活动
-
进程是具有独立功能的程序在一个数据集合上运行的过程,是系统进行资源分配和调度的一个独立单位。
个人理解:程序在服务器上运行时,占用的计算机资源合集,就是进程。
1.2 线程
线程的引入:由于进程是资源的拥有者,而在进程的创建、切换中会付出较大的时空开销,限制了并发。所以将进程的资源分配和调度两个属性分开,引入“线程”作为调度的基本单位。
线程的定义:是程序能够独立运行的最小单位。线程具有进程所具有的特征,所以线程又叫轻型进程。
1.3 线程的生命周期
二、原生线程与托管线程
2.1 原生线程
定义:由操作系统负责创建、运行、切换、终止的线程就是原生线程。
线程切换:由于单个逻辑逻辑核心同一个一刻只能运行一个线程(并行),所以并发处理时需要线程流转也就是线程切换。
扩展寄存器:
线程调度:
分时调度:所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。
抢占式调度:优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性)。
没找到相关资源,理论上应该和进程调度差不多,进程调度有好几种方法:轮转调度算法、优先级算法、多级反馈等等。
2.2 托管线程
定义:基于原生线程,由.NET管理的线程。
托管线程对象:我们.NET为了更方便的管理和使用原生线程,抽象出来的一套线程模型,Thread
托管线程与原生线程的关系:
托管线程的创建:展示由托管代码创建:
跟踪方法,发现.NET Core会调用外部程序(c++),创建原生线程,并将托管线程和原生线程关联
线程本地存储:实现线程范围内的局部变量,即ThreadLocal在一个线程中是共享的,在不同线程之间是隔离的
实现方法
[ThreadStatic] 特性
ThreadLocal()
示例
/// <summary>
/// 线程的本地存储
/// </summary>
public static class Sample01
{
[ThreadStatic]
private static int a;
[ThreadStatic]
private static int b;
//private readonly ThreadLocal<int> a = new ThreadLocal<int>();
//private readonly ThreadLocal<int> b = new ThreadLocal<int>();
private static void Thread1()
{
a = 1;
b = 2;
Console.WriteLine($"a={a} -- 【From Thread1】");
Console.WriteLine($"b={b} -- 【From Thread1】");
}
private static void Thread2()
{
a = 10;
b = 20;
Console.WriteLine($"a={a} -- 【From Thread2】");
Console.WriteLine($"b={b} -- 【From Thread2】");
}
public static void ThreadLocalDemo()
{
var thread1 = new Thread(Thread1);
var thread2 = new Thread(Thread2);
thread1.Start();
thread2.Start();
}
}
三、.NET的多线程
3.1 Thread(Framework1.X)
Thread的创建和启动
//就是一个无参委托
public delegate void ThreadStart();
//带了一个object参数,只能使用object类型当参数
public delegate void ParameterizedThreadStart(object? obj);
// int maxStackSize 设置线程最大栈空间
使用Lambda表达式
Thread thread = new Thread(() =>
{
Test("", "");
});
thread.Start(); //其实用的也是ThreadStart
前台线程与后台线程
后台线程:
所谓后台(daemon)线程,是指在程序运行的时候在后台提供一种通用服务的线程,并且这种线程并不属于程序中不可或缺的部分。因此,当所有非后台线程结束时,程序也就终止了,同时会杀死所有的后台线程。反过来说,只要有任何非后台线程还在运行,程序就不会终止。比如,执行main()的就是一个非后台线程。 --java编程思想
tips:
1.通过Thread类构造的线程默认情况下都是前台线程,可以通过 IsBackground 属性来更改
2.通过线程池、Task构造的都是后台线程
常用属性
CurrentThread | 获取当前正在运行的线程。 |
---|---|
IsAlive | 获取指示当前线程的执行状态的值。 |
IsBackground | 获取或设置一个值,该值指示某个线程是否为后台线程。 |
IsThreadPoolThread | 获取指示线程是否属于托管线程池的值。 |
ManagedThreadId | 获取当前托管线程的唯一标识符。 |
Name | 获取或设置线程的名称。 |
Priority | 获取或设置指示线程的调度优先级的值。 |
ThreadState | 获取一个值,该值包含当前线程的状态。 |
常用方法
Start() | 导致操作系统将当前实例的状态更改为 Running。 |
---|---|
Sleep(TimeSpan) | 将当前线程挂起指定的时间。 |
Join() | 在继续执行标准的 COM 和 SendMessage 消息泵处理期间,阻止调用线程,直到由该实例表示的线程终止。 |
GetDomainID() | 返回唯一的应用程序域标识符。 |
Suspend() | 已过时。挂起线程,或者如果线程已挂起,则不起作用。 |
Resume() | 已过时。继续已挂起的线程。 |
Finalize() | 确保垃圾回收器回收 Thread 对象时释放资源并执行其他清理操作。 |
Abort() | 在调用此方法的线程上引发 ThreadAbortException,以开始终止此线程的过程。 调用此方法通常会终止线程。 |
ResetAbort() | 取消当前线程所请求的 Abort(Object)。 |
官方文档:https://docs.microsoft.com/zh-cn/dotnet/api/system.threading.thread?view=netcore-3.1
Thread的缺陷:
- 官方提供了丰富的API,Thread操纵的是托管线程,然后对CPU发出指令,操控原生线程。这就导致响应不灵敏,无法很好的控制线程。
- Thread对启动线程数量不设控制,如果使用不当,会造成死机。
3.2 ThreadPool(Framework2.X)
ThreadPool引入:
在Thread中对线程的管理须要咱们本身去从操做,在不断的开启线程和销毁中,存在很大的开销,为了让线程能够反复的使用,出现了池化思想!
ThreadPool创建和使用
ThreadPool属性
CompletedWorkItemCount | 获取迄今为止已处理的工作项数。 |
---|---|
PendingWorkItemCount | 获取当前已加入处理队列的工作项数。 |
ThreadCount | 获取当前存在的线程池线程数。 |
ThreadPool方法:
GetAvailableThreads(Int32, Int32) | 检索由 GetMaxThreads(Int32, Int32) 方法返回的最大线程池线程数和当前活动线程数之间的差值。 |
---|---|
GetMaxThreads(Int32, Int32) | 检索可以同时处于活动状态的线程池请求的数目。 所有大于此数目的请求将保持排队状态,直到线程池线程变为可用。 |
GetMinThreads(Int32, Int32) | 发出新的请求时,在切换到管理线程创建和销毁的算法之前检索线程池按需创建的线程的最小数量。 |
QueueUserWorkItem(WaitCallback) | 将方法排入队列以便执行。 此方法在有线程池线程变得可用时执行。 |
QueueUserWorkItem(WaitCallback, Object) | 将方法排入队列以便执行,并指定包含该方法所用数据的对象。 此方法在有线程池线程变得可用时执行。 |
官方文档:https://docs.microsoft.com/zh-cn/dotnet/api/system.threading.threadpool?view=netcore-3.1
ThreadPool缺陷
1.提供的API太少了,无法操控线程。虽然可以使用ManualResetEvent来进行阻塞和恢复线程,但是操作还是不方便。
3.3 Task(Framework4.X)
Task
1.类 Task 表示一个不返回值且通常异步执行的单个操作
2.派生类 Task
表示返回值且通常异步执行的单个操作 3.Task的工作通常以异步方式在线程池线程上执行
Task的构建
-
实例化对象
Task task = new Task(() => { Console.WriteLine(""); }); task.Start();//开启了一个新的线程
-
使用Run方法
Task.Run(() => { Console.WriteLine(""); });
-
使用Factory工厂
TaskFactory taskFactory = Task.Factory; taskFactory.StartNew(() => { Console.WriteLine(""); });
-
使用Delay创建在指定的毫秒数后完成的任务
Task task = Task.Delay(2000).ContinueWith(t => //任务在2000ms 之后执行 { Console.WriteLine(""); });
线程异常
使用AggregateException类型异常
try {
Task.WaitAll(tasks);
}
catch (AggregateException ae) {
Console.WriteLine("One or more exceptions occurred:");
foreach (var ex in ae.InnerExceptions)
Console.WriteLine("{0}: {1}", ex.GetType().Name, ex.Message);
}
线程取消
通常情况下,多线程中一个线程出现异常,其它的线程任务就不用进行了,我们如何取消线程
代码演示
/// <summary>
/// 取消线程
/// </summary>
public static void CancelThreadDemo()
{
CancellationTokenSource cts = new CancellationTokenSource();// 通知式的
try
{
List<Task> taskList = new List<Task>();
for (int i = 0; i < 100; i++)
{
string name = $"btnThreadCore_Click_{i}";
int k = i;
taskList.Add(Task.Run(() =>
{
if (k == 5)
{
throw new Exception($"{name} 异常了");
}
if (!cts.IsCancellationRequested)//是否取消
{
Console.WriteLine($"this is {name} Ok!");
}
else
{
Console.WriteLine($"this is {name} Stop!");
}
}));
};
Task.WaitAll(taskList.ToArray());
}
catch (AggregateException aex) //能够有多个Catch 在匹配异常类型的时候,先具体,而后在寻找父类
{
cts.Cancel(); //执行该方法之后,IsCancellationRequested会被指定为false
foreach (var exception in aex.InnerExceptions)
{
Console.WriteLine(exception.Message);
}
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
throw;
}
}
Task的属性:
CompletedTask | 获取一个已成功完成的任务。 |
---|---|
CurrentId | 返回当前正在执行 Task 的 ID。 |
Exception | 获取导致 AggregateException 提前结束的 Task。 如果 Task 成功完成或尚未引发任何异常,这将返回 null 。 |
Factory | 提供对用于创建和配置 Task 和 Task 实例的工厂方法的访问。 |
IsCompleted | 获取一个值,它表示是否已完成任务。 |
Task的方法:
Start() | 启动 Task,并将它安排到当前的 TaskScheduler 中执行。 |
---|---|
Run(Action) | 将在线程池上运行的指定工作排队,并返回代表该工作的 Task 对象。 |
Delay(Int32) | 创建一个在指定的毫秒数后完成的任务。 |
ContinueWith(Action) | 创建一个在目标 Task 完成时异步执行的延续任务。 |
Dispose() | 释放 Task 类的当前实例所使用的所有资源。 |
Wait() | 等待 Task 完成执行过程。 |
WhenAny(Task[]) | 非阻塞,任何提供的任务已完成时,创建将完成的任务。 |
WhenAll(Task[]) | 非阻塞,创建一个任务,该任务将在数组中的所有 Task 对象都已完成时完成。 |
WaitAny(Task[]) | 阻塞线程,等待提供的任一 Task 对象完成执行过程。 |
WaitAll(Task[]) | 阻塞线程,等待提供的所有 Task 对象完成执行过程。 |
官方文档:https://docs.microsoft.com/zh-cn/dotnet/api/system.threading.tasks.task?view=netcore-3.1
3.4 parallel(TPL)(Framework4.5)
数据并行(任务并行库)
数据并行指的是对源集合或数组的元素同时(即,并行)执行相同操作的场景。 在数据并行操作中,对源集合进行分区,以便多个线程能够同时在不同的网段上操作。
简单理解就是 Task WaitAll + 主线程(参与计算)
常用方法
Paraller.For()、Parallel.ForEach()、Parallel.Invoke()
修改parallel线程数量
ParallelOptions options = new ParallelOptions();
options.MaxDegreeOfParallelism = 3;
简单使用parallel
/// <summary>
/// Paraller
/// </summary>
public static void ParallerDemo()
{
// 求零到一百万的和
int[] nums = Enumerable.Range(0, 1000000).ToArray();
long total = 0;
#region 使用Parallel.For
Parallel.For<long>(0, nums.Length, () => 0, (j, loop, subtotal) =>
{
subtotal += nums[j];
return subtotal;
},
(x) => Interlocked.Add(ref total, x)//每个分区结束后执行
);
#endregion
#region 使用Parallel.ForEach
Parallel.ForEach<int, long>(nums,() => 0,(j, loop, subtotal) =>
{
subtotal += j;
return subtotal;
},
(finalResult) => Interlocked.Add(ref total, finalResult));
#endregion
//Invoke
//Parallel.Invoke(() =>
//{
// Thread.Sleep(100);
// Console.WriteLine("method1");
//}, () =>
//{
// Thread.Sleep(10);
// Console.WriteLine("method2");
//});
Console.WriteLine("The total is {0:N0}", total);
Console.ReadKey();
}
官方文档:https://docs.microsoft.com/zh-cn/dotnet/standard/parallel-programming/task-parallel-library-tpl
四、线程同步
线程同步
使并发执行的多个线程能够按照一定的规则(时序)共享系统资源。即当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作, 其他线程才能对该内存地址进行操作
4.1 Interlocked
.NET 为原子操作提供的一个类
方法
Decrement(Int32) | 以原子操作的形式递减指定变量的值并存储结果。 |
---|---|
Increment(Int32) | 以原子操作的形式递增指定变量的值并存储结果。 |
Add(Int32, Int32) | 对两个 32 位整数进行求和并用和替换第一个整数,上述操作作为一个原子操作完成。 |
CompareExchange(Int32, Int32, Int32) | 比较两个 32 位有符号整数是否相等,如果相等,则替换第一个值。 |
Exchange(Int32, Int32) | 以原子操作的形式,将 32 位有符号整数设置为指定的值并返回原始值。 |
示例
/// <summary>
/// 线程锁
/// </summary>
public class Sample05
{
//0 未使用, 1 正在使用.
private static int usingResource = 0;
private const int numThreads = 10;
private const int numThreadIterations = 5;
public static void InterLockedDemo()
{
//var result1 = Interlocked.Exchange(ref usingResource, 5);
//var result2 = Interlocked.CompareExchange(ref usingResource, 10, 5);
Thread myThread;
Random rnd = new Random();
//开启了10个线程
for (int i = 0; i < numThreads; i++)
{
myThread = new Thread(new ThreadStart(MyThreadProc));
myThread.Name = String.Format("Thread{0}", i + 1);
Thread.Sleep(rnd.Next(0, 1000));
myThread.Start();
}
}
private static void MyThreadProc()
{
for (int i = 0; i < numThreadIterations; i++)
{
UseResource();
Thread.Sleep(1000);
}
}
static bool UseResource()
{
//判断资源是否被占用
if (0 == Interlocked.Exchange(ref usingResource, 1))
{
Console.WriteLine("{0} 获取锁", Thread.CurrentThread.Name);
Thread.Sleep(500);
Console.WriteLine("{0} 释放锁", Thread.CurrentThread.Name);
//释放锁
Interlocked.Exchange(ref usingResource, 0);
return true;
}
else
{
Console.WriteLine(" {0} 获取锁失败", Thread.CurrentThread.Name);
return false;
}
}
}
4.2 线程安全容器
ConcurrentBag | 表示对象的线程安全的无序集合。 |
---|---|
ConcurrentDictionary | 表示可由多个线程同时访问的键/值对的线程安全集合。 |
ConcurrentQueue | 表示线程安全的先进先出 (FIFO) 集合。 |
ConcurrentStack | 表示线程安全的后进先出 (LIFO) 集合。 |
官方文档:https://docs.microsoft.com/zh-cn/dotnet/api/system.collections.concurrent?view=netcore-3.1
4.2 自旋锁
自旋锁:自旋锁(Spinlock)是最简单的线程锁,基于原子操作实现
.NET提供对象
SpinWait
SpinLock
代码展示
/// <summary>
/// 自旋锁
/// </summary>
internal class Sample06
{
//0 未使用, 1 正在使用.
private static int _lock = 0;
public static void SpinWaitDemo()
{
//var spinWait = new SpinWait();
while (Interlocked.Exchange(ref _lock, 1) != 0)
{
Thread.SpinWait(1);
//spinWait.SpinOnce();//(推荐)
// 一定次数以内,核心大于1,Thread.SpinWait
// 超过一定次数,核心等于1,交替使用Thread.Sleep(0)和Thread.Yield方法
// 再超过一定次数,Thread.Sleep(1)
// Sleep(0)实际上调用SleepEx系统函数
// Yield()调用SwitchToThread的系统函数
}
{
/*锁保护区
方法体*/
}
Interlocked.Exchange(ref _lock, 0);
}
/// <summary>
/// SpinLock用法
/// </summary>
private static SpinLock _spinLock = new SpinLock();
public static void SpinLockDemo()
{
bool lockTaken = false;
try
{
_spinLock.Enter(ref lockTaken);
{
/*锁保护区
方法体*/
}
}
finally
{
if (lockTaken)
{
_spinLock.Exit();
}
}
}
}
SPinLock就是对SpinWait的一个封装
优点
- 避免了上下文切换,效率高
缺点:
- 方法体不适用于长时间运行的操作,不然会影响其它线程运行
- 当前实现没有考虑到公平性,如果多个线程同时获取锁失败,按时间顺序第一个获取锁的线程不一定会在释放锁后第一个获取成功
官方文档:https://docs.microsoft.com/zh-cn/dotnet/api/system.threading.spinwait?view=netcore-3.1
4.3 互斥锁
基于原子操作和线程调度方式来实现。
互斥锁->是否被获取->获取失败,不进行重试->进入等待队列
锁释放->查看等待队列中是否存在线程->唤醒等待线程->调度运行(比自旋慢很多)
.NET提供对象
Mutex
使用示例
internal class Sample07
{
private static readonly Mutex _lock = new Mutex();
/// <summary>
/// 互斥锁简单使用
/// </summary>
public static void MutexDemo()
{
_lock.WaitOne();
try
{
{
/*锁保护区*/
/*方法体*/
}
}
catch (AggregateException ae)
{
throw ae;
}
finally
{
_lock.ReleaseMutex();//释放锁
//如果锁不在使用,立即释放资源
_lock.Dispose();
}
}
}
优点
- 支持跨进程,防止程序多开、共享系统资源
- 支持冲入,实现递归锁
缺点
- 效率低
官方文档:https://docs.microsoft.com/zh-cn/dotnet/api/system.threading.mutex?view=netcore-3.1
4.4 混合锁(Lock)
混合锁的特征是在获取锁失败后像自旋锁一样重试一定的次数,超过一定次数之后(.NET Core 2.1 是30次)再安排当前进程进入等待状态
.NET提供对象
Monitor
Lock
锁的对象
通常我们都会锁私有的引用对象,锁的是对象的内存引用地址
严格地说,提供给 lock 的对象只是用来唯一地标识由多个线程共享的资源,所以它可以是任意类实例。
- public,会导致死锁或者锁竞争
- string, 由于string具有不变性和字符串驻留,意味着整个程序中给定字符串只有一个实例。
使用演示
internal class Sample08
{
private static readonly object Lock = new object();
public static void MonitorDemo() {
var lockObj = Lock;
var lockTaken = false;
try
{
// 获取锁
Monitor.Enter(lockObj, ref lockTaken);
// 锁保护
{
//方法体
}
}
finally
{
// 释放锁
if (lockTaken) Monitor.Exit(lockObj);
}
}
}
官方文档:https://docs.microsoft.com/zh-cn/dotnet/api/system.threading.monitor?view=netcore-3.1
4.5 读写锁
ReaderWriterLock
定义支持单个写线程和多个读线程的锁。该锁的作用主要是解决并发读的性能问题,使用该锁,可以大大提高数据并发访问的性能,只有在写时,才会阻塞所有的读锁。
private static readonly ReaderWriterLockSlim Lock = new ReaderWriterLockSlim();
private static void WriteDemo()
{
// 获取写入锁
Lock.EnterWriteLock();
try
{
{
//IO操作
}
}
catch (Exception ex)
{
}
finally
{
// 释放写入锁
Lock.ExitWriteLock();
}
}