文章目录
一、基本概念
1.什么是线程?
线程是操作系统中能够独立运行的最小单位,也是程序中可以并发执行的一段指令序列
线程是进程的一部分,一个进程可以包含多个线程
进程有入口线程,也可以创建更多线程
2.多线程的使用场景
批量重复任务希望同时进行
多个不同任务需要同时进行,互不干扰
总结:提高程序运行效率
3.什么是线程池?
一组预先创建好的线程
备注:因为一个线程的创建和销毁是占用系统开销的,用线程池里提前创建好的线程,可以避免频分的创建和销毁新线程,可以提高系统效率。小的且不会阻塞的任务可以用线程池里的线程,其他的时候还是自己创建新线程吧。
异步编程默认用的线程池里的线程。
4.什么是线程安全?
多个线程访问同一个资源,不会导致共享资源数据不一致或不可预期的结果就称为线程安全
5.如何保证线程安全?
同步机制:协调和控制多个线程之间的执行顺序、互斥访问共享资源
原子操作:在执行过程中不会被中断的操作。不可分割,要么完全执行,要么完全不执行,没有中间状态。使用原子操作得借助于.NET的System.Threading.Interlocked类,这里面提供了一些原子操作的方法。
5.1.协调和控制多个线程之间的执行顺序
可以使用的手法:
- Thread.Join
- ManualResetEvent 和 AutoResetEvent
- SemaphoreSlim
- Task 和 await
Thread.Join
Thread.Join 方法可让一个线程等待另一个线程执行完毕后再继续执行,常用于简单的线程顺序控制。
using System;
using System.Threading;
class Program
{
static void Main()
{
Thread thread1 = new Thread(Task1);
Thread thread2 = new Thread(Task2);
thread1.Start();
// 主线程等待 thread1 执行完毕
thread1.Join();
thread2.Start();
// 主线程等待 thread2 执行完毕
thread2.Join();
Console.WriteLine("All tasks are completed.");
}
static void Task1()
{
Console.WriteLine("Task 1 is starting.");
Thread.Sleep(1000);
Console.WriteLine("Task 1 is completed.");
}
static void Task2()
{
Console.WriteLine("Task 2 is starting.");
Thread.Sleep(1000);
Console.WriteLine("Task 2 is completed.");
}
}
在上述代码中,thread1.Join() 会使主线程阻塞,直到 thread1 执行完毕;接着 thread2.Start() 启动 thread2,thread2.Join() 又会使主线程等待 thread2 执行完毕,从而保证了 Task1 先于 Task2 执行。
ManualResetEvent 和 AutoResetEvent
ManualResetEvent 和 AutoResetEvent是用于线程间同步的事件类,前者需要手动重置事件状态,后者在信号被接收后会自动重置。
using System;
using System.Threading;
class Program
{
static ManualResetEvent event1 = new ManualResetEvent(false);
static ManualResetEvent event2 = new ManualResetEvent(false);
static void Main()
{
Thread thread1 = new Thread(Task1);
Thread thread2 = new Thread(Task2);
thread1.Start();
thread2.Start();
// 主线程等待两个任务完成
event2.WaitOne();
Console.WriteLine("All tasks are completed.");
}
static void Task1()
{
Console.WriteLine("Task 1 is starting.");
Thread.Sleep(1000);
Console.WriteLine("Task 1 is completed.");
// 通知 Task2 可以开始执行
event1.Set();
}
static void Task2()
{
// 等待 Task1 完成
event1.WaitOne();
Console.WriteLine("Task 2 is starting.");
Thread.Sleep(1000);
Console.WriteLine("Task 2 is completed.");
// 通知主线程任务完成
event2.Set();
}
}
在这个例子中,event1 用于控制 Task2 等待 Task1 完成,event2 用于通知主线程所有任务已完成。event1.WaitOne() 会使 Task2 线程阻塞,直到 event1.Set() 被调用,从而实现了线程执行顺序的控制。
SemaphoreSlim
SemaphoreSlim 是一个轻量级的信号量,可用于限制同时访问某个资源或执行某个代码段的线程数量,也可用于线程顺序控制。
using System;
using System.Threading;
using System.Threading.Tasks;
class Program
{
static SemaphoreSlim semaphore = new SemaphoreSlim(0, 1);
static async Task Main()
{
Task task1 = Task.Run(Task1);
Task task2 = Task.Run(Task2);
await Task.WhenAll(task1, task2);
Console.WriteLine("All tasks are completed.");
}
static async Task Task1()
{
Console.WriteLine("Task 1 is starting.");
await Task.Delay(1000);
Console.WriteLine("Task 1 is completed.");
// 释放信号量,允许 Task2 执行
semaphore.Release();
}
static async Task Task2()
{
// 等待信号量
await semaphore.WaitAsync();
Console.WriteLine("Task 2 is starting.");
await Task.Delay(1000);
Console.WriteLine("Task 2 is completed.");
}
}
SemaphoreSlim 初始计数为 0,意味着 Task2 会在 semaphore.WaitAsync() 处阻塞,直到 Task1 调用 semaphore.Release() 释放信号量,从而保证了 Task1 先于 Task2 执行。
Task 和 await
在异步编程中,可以使用 Task 和 await 关键字来协调任务的执行顺序。
using System;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
await Task1();
await Task2();
Console.WriteLine("All tasks are completed.");
}
static async Task Task1()
{
Console.WriteLine("Task 1 is starting.");
await Task.Delay(1000);
Console.WriteLine("Task 1 is completed.");
}
static async Task Task2()
{
Console.WriteLine("Task 2 is starting.");
await Task.Delay(1000);
Console.WriteLine("Task 2 is completed.");
}
}
await 关键字会使 Main 方法异步等待 Task1 执行完毕后再继续执行 Task2,保证了任务的顺序执行,且不会阻塞主线程。
5.2 互斥访问共享资源
互斥指的一个线程访问这个共享资源的时候,不允许其他线程访问
- lock 语句是最常用的线程同步方式,简单易用,适用于大多数场景。
- Monitor 类提供了更灵活的锁控制,与 lock 语句功能类似。
- Mutex 类可用于跨进程的线程同步,性能相对较低,适用于需要在多个进程之间共享锁的场景。
LOCK
lock 语句是 .NET 中最常用的线程同步机制之一,它基于 Monitor 类实现,用于确保在同一时间只有一个线程可以执行被锁定的代码块。
using System;
using System.Threading;
class Program
{
private static readonly object _lockObject = new object();
private static int _sharedResource = 0;
static void Main()
{
// 创建两个线程来访问共享资源
Thread thread1 = new Thread(IncrementSharedResource);
Thread thread2 = new Thread(IncrementSharedResource);
thread1.Start();
thread2.Start();
thread1.Join();
thread2.Join();
Console.WriteLine($"Final value of shared resource: {_sharedResource}");
}
static void IncrementSharedResource()
{
for (int i = 0; i < 100000; i++)
{
// 使用 lock 语句确保同一时间只有一个线程可以访问共享资源
lock (_lockObject)
{
_sharedResource++;
}
}
}
}
- _lockObject 是一个用于锁定的对象,通常使用 private static readonly 修饰,以确保所有线程都使用同一个锁对象。
- lock (_lockObject)语句块确保在同一时间只有一个线程可以进入该代码块,从而避免多个线程同时修改 _sharedResource 导致的数据竞争问题。
Monitor
Monitor 类提供了更灵活的线程同步机制,与 lock 语句类似,但可以更精细地控制锁的获取和释放。
using System;
using System.Threading;
class Program
{
private static readonly object _lockObject = new object();
private static int _sharedResource = 0;
static void Main()
{
Thread thread1 = new Thread(IncrementSharedResource);
Thread thread2 = new Thread(IncrementSharedResource);
thread1.Start();
thread2.Start();
thread1.Join();
thread2.Join();
Console.WriteLine($"Final value of shared resource: {_sharedResource}");
}
static void IncrementSharedResource()
{
for (int i = 0; i < 100000; i++)
{
// 获取锁
Monitor.Enter(_lockObject);
try
{
_sharedResource++;
}
finally
{
// 释放锁
Monitor.Exit(_lockObject);
}
}
}
}
- Monitor.Enter(_lockObject) 用于获取锁,如果锁已经被其他线程持有,则当前线程会被阻塞。
- Monitor.Exit(_lockObject) 用于释放锁,确保在 try 块中的代码执行完毕后,无论是否发生异常,锁都会被释放。
Mutex
Mutex 是一种互斥锁,可用于在多个线程或多个进程之间实现线程同步。
using System;
using System.Threading;
class Program
{
private static readonly Mutex _mutex = new Mutex();
private static int _sharedResource = 0;
static void Main()
{
Thread thread1 = new Thread(IncrementSharedResource);
Thread thread2 = new Thread(IncrementSharedResource);
thread1.Start();
thread2.Start();
thread1.Join();
thread2.Join();
Console.WriteLine($"Final value of shared resource: {_sharedResource}");
}
static void IncrementSharedResource()
{
for (int i = 0; i < 100000; i++)
{
// 请求获取互斥锁
_mutex.WaitOne();
try
{
_sharedResource++;
}
finally
{
// 释放互斥锁
_mutex.ReleaseMutex();
}
}
}
}
- _mutex.WaitOne() 用于请求获取互斥锁,如果锁已经被其他线程持有,则当前线程会被阻塞。
- _mutex.ReleaseMutex() 用于释放互斥锁,确保在 try 块中的代码执行完毕后,无论是否发生异常,锁都会被释放。
5.3 原子操作
说明:在执行过程中不会被中断的操作。不可分割,要么完全执行,要么完全不执行,没有中间状态。.NET底层提供了一些原子操作的方法,只能用这些方法进行原子操作。
.NET 在 System.Threading.Interlocked 类中提供了一系列用于执行原子操作的静态方法,常见的如下:
1.Increment 和 Decrement
用于对整数类型(int、long 等)进行原子的自增和自减操作。
如 Interlocked.Increment(ref num) 会将 num 的值原子地增加 1,Interlocked.Decrement(ref num) 会将 num 的值原子地减少 1。
2.Exchange
用于原子地将一个变量的值替换为另一个值,并返回该变量的原始值。
例如 Interlocked.Exchange(ref num, 10) 会将 num 的值原子地替换为 10,并返回 num 原来的值。
3.CompareExchange
用于原子地比较一个变量的值与一个期望值,如果相等,则将该变量的值替换为一个新值,并返回该变量的原始值;如果不相等,则不进行替换,直接返回该变量的当前值。
例如 Interlocked.CompareExchange(ref num, 20, 10) 会比较 num 的值是否为 10,如果是,则将 num 的值替换为 20,并返回 10;如果不是,则不进行替换,直接返回 num 的当前值。
6.NET自带的一些多线程方法
Parallel 是一个非常有用的类,翻译中文为“并行的”,它位于 System.Threading.Tasks 命名空间下,主要用于简化并行编程,让开发者可以更轻松地利用多核处理器的计算能力,提高程序的执行效率。
比如使用Parallel 的For来执行循环,它会将循环的迭代任务分配给多个线程同时执行。Parallel还有Foreach、Invoke等方法。
AsParallel 是一个扩展方法,它属于 PLINQ(Parallel Language Integrated Query,并行语言集成查询)的一部分,位于 System.Linq 命名空间下。AsParallel 方法的主要作用是将一个普通的顺序查询转换为并行查询,从而利用多核处理器的并行计算能力,提高查询操作的执行效率。
AsParallel 可以应用于任何实现了 IEnumerable 接口的集合,将其转换为可并行处理的查询。
using System;
using System.Linq;
class Program
{
static void Main()
{
// 创建一个包含 1 到 100 的整数集合
var numbers = Enumerable.Range(1, 100);
// 使用 AsParallel 将顺序查询转换为并行查询
var parallelResult = numbers.AsParallel()
.Where(n => n % 2 == 0)
.Select(n => n * n);
// 输出结果
foreach (var result in parallelResult)
{
Console.WriteLine(result);
}
}
}
在上述代码中,AsParallel 方法将 numbers 集合转换为并行查询,后续的 Where 和 Select 操作会并行执行,从而利用多核处理器的优势。
在使用 AsParallel 方法将顺序查询转换为并行查询后,查询结果的顺序可能会被打乱,因为并行处理时各个任务的完成顺序是不确定的。AsOrdered 方法的作用就是确保并行查询的结果保持与源序列相同的顺序。
二、线程的创建
1.线程的创建
不带参数的:
// 创建一个新的线程,传入要执行的方法
Thread newThread = new Thread(DoWork); //这里DoWork是一个没有参数的方法
// 启动线程
newThread.Start();
带参数的:
// 创建一个新的线程,传入带参数的方法
Thread newThread = new Thread(DoWorkWithParameter);
// 启动线程并传递参数
newThread.Start("Hello from parameter!");
static void DoWorkWithParameter(object parameter)//只能是一个参数,如果想传递多个参数的话,传一个类吧
{
string message = (string)parameter;
Console.WriteLine(message);
}
用线程池里的线程:
// 将方法排队到线程池队列中
ThreadPool.QueueUserWorkItem(DoWorkInThreadPool);
一般不推荐直接使用线程池里的线程,因为线程池里的线程进程自己还要干自己的事,比如web应用中线程池中的线程用于处理客户端的请求
1.1 前台线程与后台线程
前台线程:
只要有一个前台线程在运行,应用程序就不会退出。也就是说,前台线程会阻止应用程序的终止,它会保证自身执行完毕后才可能让程序结束。通常,创建的普通线程默认就是前台线程。
后台线程:
后台线程不会阻止应用程序的退出。当所有前台线程都执行完毕后,无论后台线程是否执行完,应用程序都会立即退出,后台线程也会随之终止。
前台线程、后台线程
前台线程创建的后台线程会跟着前台线程一起关闭
2.线程的终止
Thread.Join (详见第一章5.1)
说明:Thread.Join 方法可让一个线程等待另一个线程执行完毕后再继续执行,常用于简单的线程顺序控制。
Thread.Interrupt()
说明:Interrupt() 方法用于中断处于 WaitSleepJoin 状态(即线程正在等待、睡眠或加入)的线程。当调用 Interrupt() 方法时,会在目标线程中抛出 ThreadInterruptedException 异常。备注:如果线程里正在执行死循环,Interrupt()停不掉他。得写个Thread.Sleep(0)让他等待一下。
3.线程的挂起与恢复
3.1 Mutex(互斥体)
Mutex 是一种线程同步原语,用于确保在同一时间只有一个线程可以访问共享资源。它是跨进程的,可以在不同进程之间实现互斥访问。
3.2 Semaphore(信号量)
Semaphore 用于控制对有限资源的并发访问。它维护一个计数器,该计数器表示可用资源的数量。当线程请求访问资源时,计数器会减 1;当线程释放资源时,计数器会加 1。如果计数器为 0,则请求线程会被阻塞,直到有其他线程释放资源。
3.3 WaitHandle
WaitHandle 是一个抽象基类,Mutex、Semaphore 和 EventWaitHandle 等类都继承自它。它提供了一组方法,用于等待一个或多个同步对象变为有信号状态。
3.4 ReaderWriterLock
ReaderWriterLock 用于实现读写锁,允许多个线程同时进行读操作,但在写操作时会独占资源,确保数据的一致性。读操作可以并发执行,提高了并发性能;而写操作则需要独占资源,避免数据冲突。
4.不要自己造轮子
4.1 Lazy
Lazy 是一个非常有用的泛型类,它用于实现延迟初始化。延迟初始化是指在需要使用某个对象时才进行初始化,而不是在对象创建时就立即初始化,这样可以提高程序的性能和资源利用率,特别是对于那些初始化开销较大或者不一定会被使用的对象。
4.2 concurrentBag等
ConcurrentBag、ConcurrentStack、ConcurrentQueue 和 ConcurrentDictionary<TKey, TValue> 都是 .NET 中 System.Collections.Concurrent 命名空间下的线程安全集合类,它们用于在多线程环境中高效地存储和操作数据。
ConcurrentBag:允许多个线程同时进行添加和移除操作,无需额外的同步机制。
ConcurrentStack:遵循后进先出(LIFO)原则,多线程环境下可安全操作。
ConcurrentQueue:遵循先进先出(FIFO)原则,多线程环境下可安全操作。
ConcurrentDictionary:用于存储键值对,多线程环境下可安全进行添加、删除、查找等操作。
4.3 BlockingCollection
BlockingCollection本质上是一个支持阻塞操作的集合容器,它可以作为生产者线程和消费者线程之间的桥梁。生产者线程负责向集合中添加元素,而消费者线程则从集合中取出元素进行处理。当集合为空时,消费者线程会被阻塞,直到有新元素被添加进来;当集合达到最大容量时,生产者线程会被阻塞,直到有元素被消费者取出。
4.4 Channel
Channel 是一种用于在不同线程或异步操作之间进行高效、安全的数据传递的机制,类似于一个管道,一端可以发送数据,另一端可以接收数据。它在 System.Threading.Channels 命名空间下,是 .NET Core 3.0 及更高版本引入的功能,常用于构建异步数据流和实现生产者 - 消费者模式。