第十三章:调度
第十三章:调度
我们都知道代码的执行需要被安排到某个线程上,而调度器则负责决定这些代码的执行位置和方式。C# 提供了多种调度工具和机制,例如任务调度器 (TaskScheduler
)、同步上下文 (SynchronizationContext
) 以及各种线程池策略。这些工具为开发者实现自定义调度逻辑提供了灵活性,同时也增强了并发程序的可控性和性能优化能力。
通常情况下,建议尽量使用默认调度器的行为,因为它们的默认设置已被设计得足够高效。例如,异步代码中的 await
运算符会自动在相同的同步上下文中恢复方法执行,除非显式覆盖这种默认行为(参见相关章节)。类似地,响应式编程中的触发事件也有合理的默认上下文,可以通过 ObserveOn
来修改。
然而,有时需要在特定的上下文中执行代码,比如在 UI
线程上下文或 ASP.NET
请求上下文中。在这些场景下,开发者可以使用调度器来灵活地控制代码的执行方式。本章将深入探讨调度相关的机制和最佳实践。
13.1 任务调度器 (TaskScheduler) 基础
在 .NET 中,TaskScheduler
是任务调度的核心组件,它负责决定任务 (Task) 何时、如何以及在哪个线程上执行。可以把 TaskScheduler
看作是一个桥梁,它连接了高层的任务模型(Task
和 Task<T>
)与底层的线程管理(如线程池或用户定义的线程)。
TaskScheduler
的作用与核心机制
-
任务排队与执行:
当你通过Task
提交一个任务时,任务会被传递给调度器。调度器可以决定立即执行任务、将其排队,或者以其他策略(如优先级)调度任务。 -
选择线程:
调度器可以选择使用线程池线程、创建新的线程、或绑定到特定上下文(如 UI 线程)来运行任务。 -
异步与同步调度:
调度器可以决定任务是否异步执行(在线程池线程上运行)还是同步执行(在调用线程上运行)。 -
任务依赖:
调度器负责处理任务依赖链,确保父任务和子任务按照正确的顺序或上下文调度。
默认任务调度器 TaskScheduler.Default
默认调度器返回的是一个线程池任务调度器ThreadPoolTaskScheduler
的实例,因此会将任务分派到线程池线程上运行,自动处理父子任务的关系。例如,子任务默认继承父任务的调度器。TaskScheduler.Default
尽量公平地调度任务,但不支持显式的优先级管理(如高优先级任务优先运行)。
使用默认调度器将任务调度到线程池线程上
默认调度器通常通过 Task.Run
和 Task.Factory.StartNew
提交任务。
代码示例:Task.Run
的使用
Task.Run
是最常用的创建任务的方式,适合将独立的异步工作提交给默认调度器。
using System;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
Console.WriteLine($"Main thread: {Environment.CurrentManagedThreadId}");
Task task = Task.Run(() =>
{
Console.WriteLine($"Task running on thread: {Environment.CurrentManagedThreadId}");
});
await task;
Console.WriteLine("Task completed.");
}
}
输出示例:
Main thread: 1
Task running on thread: 5
Task completed.
解释:
Task.Run
使用线程池线程执行任务。- 适用于轻量级异步操作,尤其是 CPU 密集型任务。
代码示例:Task.Factory.StartNew
的使用
Task.Factory.StartNew
提供更多的任务创建选项,例如任务状态、调度器、任务附加选项等,如非特殊情况,Task.Run
已经足够,且更安全。
using System;
using System.Threading.Tasks;
class Program
{
static void Main()
{
Console.WriteLine($"Main thread: {Environment.CurrentManagedThreadId}");
Task task = Task.Factory.StartNew(() =>
{
Console.WriteLine($"Task running on thread: {Environment.CurrentManagedThreadId}");
});
task.Wait(); // 等待任务完成
Console.WriteLine("Task completed.");
}
}
输出示例:
Main thread: 1
Task running on thread: 6
Task completed.
Task.Factory.StartNew
不指定调度器的话,默认使用的是TaskScheduler.Default
,当然也可以显示指定:
Task.Factory.StartNew(() =>
{
Console.WriteLine($"Task running on thread: {Environment.CurrentManagedThreadId}");
}, CancellationToken.None, TaskCreationOptions.None, scheduler: TaskScheduler.Default);
再次强调一下,优先使用简单而高效的
Task.Run
,仅在需要自定义选项或特殊场景时才考虑使用Task.Factory.StartNew
。
捕获与恢复同步上下文
TaskScheduler.FromCurrentSynchronizationContext
TaskScheduler.FromCurrentSynchronizationContext
可捕获当前的 SynchronizationContext
,根据捕获的同步上下文创建调度器,通过该调度器可以将代码调度回捕获的同步上下文。例如,在 UI 应用中,这可用于确保任务在 UI 线程中执行。
var scheduler = TaskScheduler.FromCurrentSynchronizationContext();
Task.Factory.StartNew(() =>
{
Console.WriteLine("Running on captured context.");
btn1.Text="我是一个按钮";
}, CancellationToken.None, TaskCreationOptions.None, scheduler);
提示:如果当前线程的执行环境,无特定的同步上下文,比如线程池线程,执行
TaskScheduler.FromCurrentSynchronizationContext()
会抛出异常。
SynchronizationContext 的作用
- 这是一个通用型调度上下文,用于不同平台抽象调度逻辑。
- 常见用法包括恢复到 UI 线程、HTTP 请求上下文(在 ASP.NET 中)等。
- 避免直接使用平台特定的类型(如
Dispatcher
或CoreDispatcher
),以防代码与特定平台耦合。
详细用法参考后续小节
TaskScheduler
的核心方法
要深入理解 TaskScheduler
,需要了解它的三个核心方法。这些方法是自定义调度器的基础。
1. QueueTask(Task task)
- 将任务加入调度队列,准备执行。
- 默认实现会将任务排队到线程池。
- 自定义调度器可以在此方法中实现自定义的任务排队逻辑,比如优先级队列。
2. TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued)
- 尝试在线程的上下文中立即执行任务。
- 如果任务无法在当前线程执行,则返回
false
,任务会被正常排队。 - 用于优化场景,例如:避免线程池线程的切换。
3. GetScheduledTasks()
- 返回当前调度器排队的任务集合(用于调试和诊断)。
- 由于性能原因,通常这个方法只在调试模式下实现。
13.2 默认调度器(TaskScheduler.Default
)源码解读
TaskScheduler.Default
是 .NET 中的默认任务调度器,它负责将任务调度到线程池 (ThreadPool
) 上执行。默认调度器的实现由 ThreadPoolTaskScheduler
类完成,该类继承自 TaskScheduler
并封装了线程池的任务队列和执行逻辑,通过Task.Run
启动的任务自动由默认调度器进行调度。
默认任务调度器具有以下特点:
-
基于线程池:
默认调度器使用线程池来执行任务,线程池会自动管理线程的创建、销毁以及复用,确保任务调度的高效性。 -
任务的公平调度:
默认调度器尽量公平地调度任务,但不支持具体的任务优先级管理。 -
父子任务关系:
默认调度器会自动处理父子任务关系,子任务默认继承父任务的调度器。 -
支持长时间运行任务 (
LongRunning
):
如果任务使用了TaskCreationOptions.LongRunning
,默认调度器会为其创建独立的线程,而不是使用线程池线程。
TaskScheduler.Default
的定义
TaskScheduler.Default
是一个 ThreadPoolTaskScheduler
的单例实例,其定义如下:
TaskScheduler.cs
// AppDomain-wide默认任务调度器
private static readonly TaskScheduler s_defaultTaskScheduler = new ThreadPoolTaskScheduler();
public static TaskScheduler Default => s_defaultTaskScheduler;
s_defaultTaskScheduler
是一个静态只读字段,表示全局唯一的默认任务调度器实例。ThreadPoolTaskScheduler
是默认任务调度器的核心实现。
核心实现:ThreadPoolTaskScheduler
ThreadPoolTaskScheduler
是默认调度器的具体实现,以下是它的核心方法分析:
ThreadPoolTaskScheduler
完整源码请参考:ThreadPoolTaskScheduler.cs
1. QueueTask(Task task)
QueueTask
是调度器将任务提交到线程池的入口方法。代码如下:
protected internal override void QueueTask(Task task)
{
TaskCreationOptions options = task.Options;
if (Thread.IsThreadStartSupported && (options & TaskCreationOptions.LongRunning) != 0)
{
// 如果任务使用了 LongRunning 选项,则为其创建一个独立线程
new Thread(s_longRunningThreadWork)
{
IsBackground = true,
Name = ".NET Long Running Task"
}.UnsafeStart(task);
}
else
{
// 将普通任务提交到线程池
ThreadPool.UnsafeQueueUserWorkItemInternal(task, (options & TaskCreationOptions.PreferFairness) == 0);
}
}
逻辑说明:
-
LongRunning
任务:
如果任务设置了TaskCreationOptions.LongRunning
,调度器会为其创建一个独立的后台线程。这是因为长时间运行的任务可能会阻塞线程池线程,而线程池线程的数量是有限的。 -
普通任务:
对于没有特殊选项的任务,调度器直接调用线程池的UnsafeQueueUserWorkItemInternal
方法将任务提交到线程池队列中。 -
公平调度:
如果任务设置了TaskCreationOptions.PreferFairness
,则会尽量保证任务按提交顺序被调度(公平性);否则,调度器可能选择性能更优的方式来调度任务。
2. TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued)
TryExecuteTaskInline
方法用于尝试在当前线程直接执行任务,而不是将任务排队等待调度:
protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued)
{
// 如果任务之前已经被排队,但现在无法从队列中移除,则返回 false
if (taskWasPreviouslyQueued && !ThreadPool.TryPopCustomWorkItem(task))
return false;
try
{
// 直接执行任务
task.ExecuteEntryUnsafe(threadPoolThread: null);
}
finally
{
if (taskWasPreviouslyQueued)
NotifyWorkItemProgress(); // 通知任务进度
}
return true;
}
逻辑说明:
-
任务从队列移除失败:
如果任务之前已经被排队,但当前无法从队列中移除(可能是因为任务已经在执行),则返回false
。 -
直接执行任务:
调用task.ExecuteEntryUnsafe()
方法执行任务。 -
通知任务进度:
如果任务之前被排队,则在执行完成后调用NotifyWorkItemProgress
通知任务队列更新。
3. TryDequeue(Task task)
TryDequeue
方法尝试从任务队列中移除一个任务:
protected internal override bool TryDequeue(Task task)
{
return ThreadPool.TryPopCustomWorkItem(task);
}
- 直接调用线程池的
TryPopCustomWorkItem
方法,从任务队列中移除指定的任务。
4. GetScheduledTasks()
GetScheduledTasks
返回当前调度器中已排队的任务列表,通常用于调试:
protected override IEnumerable<Task> GetScheduledTasks()
{
return FilterTasksFromWorkItems(ThreadPool.GetQueuedWorkItems());
}
private static IEnumerable<Task> FilterTasksFromWorkItems(IEnumerable<object> tpwItems)
{
foreach (object tpwi in tpwItems)
{
if (tpwi is Task t)
{
yield return t;
}
}
}
- 通过线程池的
GetQueuedWorkItems
方法获取所有已排队的工作项,并筛选出其中的任务对象。
5. 长时间运行任务支持
对于长时间运行的任务,调度器会为其创建一个独立线程,而不是使用线程池线程:
private static readonly ParameterizedThreadStart s_longRunningThreadWork = static s =>
{
Debug.Assert(s is Task);
((Task)s).ExecuteEntryUnsafe(threadPoolThread: null);
};
- 使用
Thread.Start
方法创建一个独立的后台线程。 - 线程的任务执行逻辑由
Task.ExecuteEntryUnsafe
实现。
总结
TaskScheduler.Default
是 .NET 中的默认任务调度器,基于 ThreadPoolTaskScheduler
实现,其核心逻辑包括:
- 将普通任务提交到线程池队列。
- 为长时间运行任务创建独立线程。
- 支持公平调度,但不支持显式优先级。
- 提供父子任务关系的自动管理。
默认调度器是大多数任务调度的首选,结合线程池提供了高效、灵活的任务执行机制。如果需要更细粒度的控制,可以通过自定义 TaskScheduler
来实现更复杂的调度逻辑。
13.3 自定义任务调度器
在 .NET 中,TaskScheduler
提供了一个灵活的机制,可以通过自定义调度器实现特殊的任务调度需求。例如,在某些场景下,你可能希望:
- 按任务的优先级执行任务;
- 强制某些任务在特定线程运行(如 UI 线程);
- 控制任务的并发度;
- 对任务执行过程进行监控和限制。
通过继承 TaskScheduler
并重写其核心方法,可以实现自定义调度器来满足这些需求。
自定义任务调度器的核心原理
自定义任务调度器需要继承 TaskScheduler
类,并至少实现以下三个方法:
-
QueueTask(Task task)
将任务添加到调度器的内部队列中,稍后进行处理。 -
TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued)
尝试立即在当前线程内执行任务。如果任务不满足条件(例如不允许在当前线程运行),则返回false
。 -
GetScheduledTasks()
返回当前调度器中已排队的任务集合(用于调试和诊断)。如果不需要调试,可以抛出NotSupportedException
。
使用场景
-
任务优先级管理
某些任务需要根据优先级先执行,例如高优先级任务抢占低优先级任务。 -
任务绑定到特定线程
在某些实时性要求较高的场景下,需要将任务固定到指定线程执行,例如UI线程、游戏循环或硬件交互程序。 -
并发控制
限制并发任务的数量,例如数据库操作、文件访问等场景。 -
调试和任务监控
需要对任务调度的行为进行详细的日志记录或监控。
代码示例 1:实现优先级任务调度器
以下代码展示了一个基于优先级队列的任务调度器。
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
public class PriorityTaskScheduler : TaskScheduler
{
private readonly SortedList<int, Queue<Task>> _tasks = new SortedList<int, Queue<Task>>();
private readonly Thread[] _threads;
public PriorityTaskScheduler(int maxConcurrency)
{
// 限制并发线程数
_threads = new Thread[maxConcurrency];
for (int i = 0; i < maxConcurrency; i++)
{
_threads[i] = new Thread(ExecuteTasks);
_threads[i].IsBackground = true;
_threads[i].Start();
}
}
protected override IEnumerable<Task> GetScheduledTasks()
{
lock (_tasks)
{
return _tasks.Values.SelectMany(queue => queue).ToList();
}
}
protected override void QueueTask(Task task)
{
if (task.AsyncState is not int priority)
{
priority = 0; // 默认优先级
}
lock (_tasks)
{
if (!_tasks.TryGetValue(priority, out var queue))
{
queue = new Queue<Task>();
_tasks[priority] = queue;
}
queue.Enqueue(task);
Monitor.PulseAll(_tasks);
}
}
protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued)
{
// 不支持内联执行
return false;
}
private void ExecuteTasks()
{
while (true)
{
Task task = null;
lock (_tasks)
{
while (!_tasks.Any())
{
Monitor.Wait(_tasks);
}
var highestPriority = _tasks.Keys.Max();
var queue = _tasks[highestPriority];
task = queue.Dequeue();
if (!queue.Any())
{
_tasks.Remove(highestPriority);
}
}
TryExecuteTask(task);
}
}
}
使用优先级调度器
class Program
{
static void Main()
{
var scheduler = new PriorityTaskScheduler2);
var factory = new TaskFactory(scheduler);
factory.StartNew(() => Console.WriteLine("Priority 1"), 1);
factory.StartNew(() => Console.WriteLine("Priority 3"), 3);
factory.StartNew(() => Console.WriteLine("Priority 2"), 2);
Thread.Sleep(1000); // 等待任务完成
}
}
输出示例:
Priority 3
Priority 2
Priority 1
代码说明
- 优先级队列:
使用SortedList<int, Queue<Task>>
维护任务队列,int
表示优先级。 - 多线程支持:
Thread[]
数组限制了最大并发任务数。 - 任务执行顺序:
任务会按照优先级从高到低依次执行。
代码示例 2:将任务绑定到特定线程
以下代码展示了一个将任务固定到特定线程运行的调度器。
class SingleThreadTaskScheduler : TaskScheduler
{
private readonly Thread _thread;
private readonly BlockingCollection<Task> _tasks = new();
public SingleThreadTaskScheduler()
{
_thread = new Thread(Execute);
_thread.IsBackground = true;
_thread.Start();
}
protected override void QueueTask(Task task)
{
_tasks.Add(task);
}
protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued)
{
return false; // 禁止内联执行
}
protected override IEnumerable<Task> GetScheduledTasks()
{
return _tasks.ToArray();
}
private void Execute()
{
foreach (var task in _tasks.GetConsumingEnumerable())
{
TryExecuteTask(task);
}
}
}
class Program
{
static void Main()
{
var scheduler = new SingleThreadTaskScheduler();
var factory = new TaskFactory(scheduler);
factory.StartNew(() => Console.WriteLine($"Task 1 on thread: {Thread.CurrentThread.ManagedThreadId}"));
factory.StartNew(() => Console.WriteLine($"Task 2 on thread: {Thread.CurrentThread.ManagedThreadId}"));
Thread.Sleep(1000); // 等待任务完成
}
}
输出示例:
Task 1 on thread: 4
Task 2 on thread: 4
最佳实践
- 避免复杂性: 自定义调度器需要正确处理多线程同步,尽量避免过于复杂的逻辑。
- 使用线程池: 除非明确需要绑定到特定线程或任务优先级,否则推荐基于线程池实现调度器。
- 性能测试: 自定义调度器可能带来额外的性能开销,应通过测试评估对任务执行性能的影响。
- 调试模式支持: 如果需要调试任务队列状态,可以实现
GetScheduledTasks
方法。
通过自定义 TaskScheduler
,可以根据需求构建灵活的任务调度方案,同时避免滥用,并发编程基础功不扎实的酌情尝试,请始终优先考虑内置调度器是否能够满足需求。
13.4 并行调度 (Parallel Scheduling)
在并行编程中,任务调度器 (TaskScheduler
) 通过限制并行程度和控制执行上下文,可以显著优化资源利用率和性能。本节探讨如何在并行操作中应用任务调度器,以实现高效的并行代码调度。
并行调度的基本概念
并行调度是指在并行操作(如 Parallel.ForEach
或 Parallel.Invoke
)中控制代码执行的方式,包括以下关键点:
-
限制并行度
控制并行任务的数量,以防止资源过度消耗或竞争。 -
任务上下文管理
通过调度器指定任务在哪种上下文中执行(例如线程池、专用线程、或特定上下文)。 -
代码片段的分层调度
在嵌套的并行操作中,为每一层提供独立的调度策略。
通过 ParallelOptions
配置调度
ParallelOptions
是用于配置 Parallel
操作的核心类,支持传入自定义任务调度器 (TaskScheduler
) 和并行度限制。
代码示例:限制并行度
以下代码展示如何为 Parallel.ForEach
配置任务调度器和并行度限制:
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
class Program
{
static void RotateMatrices(IEnumerable<IEnumerable<Matrix>> collections, float degrees)
{
// 创建一个受限并发调度器,限制最大并行任务数
var schedulerPair = new ConcurrentExclusiveSchedulerPair(
TaskScheduler.Default, maxConcurrencyLevel: 8);
TaskScheduler scheduler = schedulerPair.ConcurrentScheduler;
// 配置 ParallelOptions 使用该调度器
ParallelOptions options = new ParallelOptions
{
TaskScheduler = scheduler
};
// 对矩阵集合执行嵌套的并行操作
Parallel.ForEach(collections, options, matrices =>
{
Parallel.ForEach(matrices, options, matrix =>
{
matrix.Rotate(degrees);
});
});
}
static void Main()
{
// 示例矩阵集合
var collections = new List<List<Matrix>>
{
new List<Matrix> { new Matrix(), new Matrix() },
new List<Matrix> { new Matrix() }
};
RotateMatrices(collections, 90.0f);
Console.WriteLine("Matrix rotation completed.");
}
}
class Matrix
{
public void Rotate(float degrees)
{
Console.WriteLine($"Rotating matrix by {degrees} degrees on thread {Environment.CurrentManagedThreadId}.");
}
}
输出示例:
Rotating matrix by 90 degrees on thread 5.
Rotating matrix by 90 degrees on thread 7.
...
Matrix rotation completed.
说明
- 限制并行任务数:
maxConcurrencyLevel: 8
限制最多同时执行 8 个任务。 - 嵌套调度:外层和内层的
Parallel.ForEach
都使用相同的调度器。 - 调度器作用范围:通过
ParallelOptions
传递给Parallel
方法。
ConcurrentExclusiveSchedulerPair
是.Net运行时提供的并发/排他调度器对,提供两个调度器:一个支持并发任务执行(ConcurrentScheduler
),另一个支持排他性任务执行(ExclusiveScheduler
),用于高效地管理需要并发与互斥执行的任务场景。
在 Parallel.Invoke
中使用调度器
与 Parallel.ForEach
类似,Parallel.Invoke
也支持接收 ParallelOptions
来配置任务调度器。
代码示例:使用自定义调度器
ParallelOptions options = new ParallelOptions
{
TaskScheduler = scheduler
};
Parallel.Invoke(options,
() => Console.WriteLine("Task 1 executed."),
() => Console.WriteLine("Task 2 executed."));
动态并行中的调度器应用
动态并行指任务在运行过程中生成更多任务。例如,可以将调度器直接传递给 Task.Factory.StartNew
或 Task.ContinueWith
,确保动态生成的任务使用同一调度器。
代码示例:动态任务调度
var schedulerPair = new ConcurrentExclusiveSchedulerPair(TaskScheduler.Default, maxConcurrencyLevel: 4);
TaskScheduler scheduler = schedulerPair.ConcurrentScheduler;
Task.Factory.StartNew(() =>
{
Console.WriteLine($"Task 1 running on thread {Environment.CurrentManagedThreadId}");
Task.Factory.StartNew(() =>
{
Console.WriteLine($"Nested task running on thread {Environment.CurrentManagedThreadId}");
}, TaskCreationOptions.None, scheduler);
}, TaskCreationOptions.None, scheduler).Wait();
限制:PLINQ 与任务调度器
需要注意的是,任务调度器无法直接应用于 PLINQ(并行 LINQ)。PLINQ 使用自己的并行调度机制,无法通过 ParallelOptions
配置。
13.5 使用调度器实现数据流同步
问题背景
假设需要在数据流代码中控制独立代码片段的执行方式,例如在特定线程(如 UI TaskScheduler
允许我们对数据流块的执行进行更精细的控制。
解决方案
在 .NET 中,可以通过 ExecutionDataflowBlockOptions
为数据流块指定一个 TaskScheduler
实例。这样,数据流块会在指定的调度器上调度和执行任务。以下示例展示了如何在数据流网格的不同部分中使用调度器来控制任务的执行上下文。
代码示例1:在 WinForms 中使用数据流块更新 UI
以下代码创建了一个简单的数据流网格:
multiplyBlock
:将输入值乘以 2,在线程池中执行。displayBlock
:将结果添加到 UI 的ListBox
中,在 UI 线程中执行。
using System;
using System.Threading.Tasks;
using System.Threading.Tasks.Dataflow;
using System.Windows.Forms;
public class MainForm : Form
{
private Button addButton;
private ListBox resultListBox;
private TransformBlock<int, int> multiplyBlock;
private ActionBlock<int> displayBlock;
private int counter = 1;
public MainForm()
{
// 初始化控件
this.Text = "TaskScheduler 数据流示例";
this.Width = 400;
this.Height = 300;
addButton = new Button
{
Text = "添加数据",
Dock = DockStyle.Top,
Height = 50
};
addButton.Click += OnAddButtonClick;
resultListBox = new ListBox
{
Dock = DockStyle.Fill
};
this.Controls.Add(resultListBox);
this.Controls.Add(addButton);
// 配置数据流块
ConfigureDataflowBlocks();
}
private void OnAddButtonClick(object sender, EventArgs e)
{
// 每次点击按钮,向 multiplyBlock 发送一个数字
multiplyBlock.Post(counter++);
}
private void ConfigureDataflowBlocks()
{
// 创建 multiplyBlock:在后台线程中将输入值乘以 2
multiplyBlock = new TransformBlock<int, int>(item =>
{
Task.Delay(500).Wait(); // 模拟耗时操作
return item * 2;
});
// 创建 displayBlock:在 UI 线程中更新 ListBox
var uiOptions = new ExecutionDataflowBlockOptions
{
TaskScheduler = TaskScheduler.FromCurrentSynchronizationContext() // 确保在 UI 线程中执行
};
displayBlock = new ActionBlock<int>(result =>
{
resultListBox.Items.Add($"Processed: {result}"); // 更新 ListBox
}, uiOptions);
// 链接数据流块
multiplyBlock.LinkTo(displayBlock, new DataflowLinkOptions { PropagateCompletion = true });
}
protected override void OnFormClosing(FormClosingEventArgs e)
{
// 关闭窗口时标记数据流结束
multiplyBlock.Complete();
displayBlock.Completion.Wait(); // 确保所有任务完成
base.OnFormClosing(e);
}
[STAThread]
public static void Main()
{
Application.EnableVisualStyles();
Application.Run(new MainForm());
}
}
代码解析
-
UI 控件初始化:
- 创建一个按钮
addButton
,用于触发任务。 - 创建
ListBox
,用于展示处理结果。
- 创建一个按钮
-
数据流块配置:
multiplyBlock
:
执行一个模拟耗时的计算任务(如将输入值乘以 2),在后台线程(默认线程池)中运行。displayBlock
:
通过ExecutionDataflowBlockOptions
指定TaskScheduler.FromCurrentSynchronizationContext()
,确保在 UI 线程中执行任务,安全地更新ListBox
。
-
数据流块链接:
- 使用
LinkTo
将multiplyBlock
的输出连接到displayBlock
,实现数据在块之间的流动。 - 设置
PropagateCompletion = true
,确保任务完成时会正确关闭后续的数据流块。
- 使用
-
按钮事件处理:
- 每次点击按钮时,向
multiplyBlock
发送新的数据(递增的整数)。
- 每次点击按钮时,向
-
窗口关闭处理:
- 在关闭窗口时,调用
Complete()
标记数据流结束,并等待所有任务完成,避免后台任务未完成时强制退出的潜在问题。
- 在关闭窗口时,调用
代码示例2:同步与排他调度的结合
当需要协调数据流网格中不同块的执行行为时,TaskScheduler
的作用尤为重要。例如,可以利用 ConcurrentExclusiveSchedulerPair
来确保某些块不会同时执行,而其他块可以随时执行。
代码示例:使用 ConcurrentExclusiveSchedulerPair
控制数据流块的执行
using System;
using System.Threading.Tasks;
using System.Threading.Tasks.Dataflow;
class Program
{
static void Main()
{
// 创建调度器对:互斥调度器和并发调度器
var schedulerPair = new ConcurrentExclusiveSchedulerPair();
var exclusiveScheduler = schedulerPair.ExclusiveScheduler;
var concurrentScheduler = schedulerPair.ConcurrentScheduler;
// A 块:使用互斥调度器
var blockA = new ActionBlock<int>(
item => Console.WriteLine($"Block A processing {item}"),
new ExecutionDataflowBlockOptions { TaskScheduler = exclusiveScheduler });
// B 块:使用并发调度器
var blockB = new ActionBlock<int>(
item => Console.WriteLine($"Block B processing {item}"),
new ExecutionDataflowBlockOptions { TaskScheduler = concurrentScheduler });
// C 块:使用互斥调度器
var blockC = new ActionBlock<int>(
item => Console.WriteLine($"Block C processing {item}"),
new ExecutionDataflowBlockOptions { TaskScheduler = exclusiveScheduler });
// 向数据流块发送数据
for (int i = 1; i <= 5; i++)
{
blockA.Post(i);
blockB.Post(i);
blockC.Post(i);
}
// 标记完成
blockA.Complete();
blockB.Complete();
blockC.Complete();
Task.WaitAll(blockA.Completion, blockB.Completion, blockC.Completion);
}
}
代码说明:
exclusiveScheduler
:确保blockA
和blockC
互斥执行,即同一时间只能有一个块在处理数据。concurrentScheduler
:允许blockB
并发执行,不受blockA
和blockC
的限制。- 协调行为:
当blockA
或blockC
在运行时,另一块会等待,而blockB
可以随时执行。
输出示例:
Block A processing 1
Block B processing 1
Block B processing 2
Block C processing 1
Block A processing 2
Block C processing 2
Block A processing 3
Block B processing 3
Block C processing 3
...
注意事项
-
异步代码的调度限制:
通过TaskScheduler
实现的同步仅对代码的执行部分生效。如果数据流块中执行的是异步代码(例如await
操作),任务在等待时并不被视为正在执行,因此调度器的限制无法完全约束其行为。 -
数据流块的内部任务:
即使某些数据流块本身不执行代码(如BufferBlock<T>
),它们也需要处理内部任务,这些任务会使用指定的TaskScheduler
执行。 -
上下文恢复:
在使用数据流块时,尽量通过TaskScheduler.FromCurrentSynchronizationContext
或ExecutionDataflowBlockOptions
来显式指定需要恢复的上下文,避免潜在的同步上下文丢失问题。
13.6 同步上下文 (SynchronizationContext)
SynchronizationContext
是 .NET 中一个重要的抽象,定义了如何调度代码回到某个特定的上下文(如 UI 线程或请求线程),这一点跟TaskScheduler
有几分相似。它在并行、异步等编程场景中扮演着非常重要的角色,尤其是 async/await
操作背后的机制。
SynchronizationContext
的核心概念
-
抽象的调度上下文
它是一个抽象类,允许不同平台和框架定义自己的实现,如 WPF 的DispatcherSynchronizationContext
和 ASP.NET 的AspNetSynchronizationContext
。 -
线程与上下文分离
同步上下文并不直接等同于线程,它表示一个调度环境,调度代码如何执行,在那个线程上执行。例如,UI 的同步上下文会将代码调度回 UI 线程。 -
与异步的关系
默认情况下,async/await
会捕获当前线程的SynchronizationContext
并在异步操作完成后恢复到此上下文,这一点在后续章节async/await
原理解析中会详细介绍。
同步上下文的常见实现
-
UI 应用程序
- WPF、Windows Forms、UWP 等框架提供了专门的同步上下文,用于在异步操作后调度回 UI 线程。
- 示例:
DispatcherSynchronizationContext
(WPF)、WindowsFormsSynchronizationContext
(WinForms)。
-
ASP.NET 请求上下文
- 在传统的 ASP.NET 中,每个 HTTP 请求有其独立的同步上下文
AspNetSynchronizationContext
,用于确保操作回到原始请求线程。
- 在传统的 ASP.NET 中,每个 HTTP 请求有其独立的同步上下文
-
默认上下文
- 控制台应用和后台任务没有特定的同步上下文,通过
SynchronizationContext.Current
会返回一个null
。
- 控制台应用和后台任务没有特定的同步上下文,通过
示例 1:UI 应用中的同步上下文
以下代码展示如何利用 SynchronizationContext
在异步任务完成后返回到 UI 线程更新界面。
using System;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
class Program
{
static async Task Main()
{
Application.EnableVisualStyles();
var form = new Form();
var label = new Label { Text = "Initial text", Dock = DockStyle.Fill };
form.Controls.Add(label);
form.Shown += async (sender, e) =>
{
// 拿到 UI 的同步上下文
var syncContext = SynchronizationContext.Current;
await Task.Run(() =>
{
// 在后台线程中运行
Thread.Sleep(2000);
Console.WriteLine($"Background thread: {Environment.CurrentManagedThreadId}");
// 通过syncContext将操作调度回 UI 线程
syncContext.Post(_ =>
{
// 如下代码会在 UI 线程中执行
Console.WriteLine($"UI thread: {Environment.CurrentManagedThreadId}");
label.Text = "Updated text";
}, null);
});
};
Application.Run(form);
}
}
输出示例:
Background thread: 5
UI thread: 1
说明
-
当执行
new Form()
时候,会自动为当前线程安装一个WindowsFormsSynchronizationContext
上下文,Form
的基类Control
的构造函数中:/// <summary> /// Initializes a new instance of the <see cref="Control"/> class. /// </summary> public Control() : this(true) { } internal Control(bool autoInstallSyncContext) : base() { // 初始化工作 ... // 为当前线程设置同步上下文为`WindowsFormsSynchronizationContext`. if (autoInstallSyncContext) { WindowsFormsSynchronizationContext.InstallIfNeeded(); } }
-
SynchronizationContext.Current
捕获当前 UI 上下文。 -
syncContext.Post
将代码调度回 UI 线程。 -
在 UI 应用中,
async/await
会自动处理这个过程,无需手动调用SynchronizationContext
。
示例 2:后台服务中的同步上下文
控制台应用或后台服务默认没有特定的同步上下文。在这种情况下,await
会回到线程池线程,而不是原始线程。
using System;
using System.Threading;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
Console.WriteLine($"Main thread: {Environment.CurrentManagedThreadId}");
await Task.Run(() =>
{
Console.WriteLine($"Task running on thread: {Environment.CurrentManagedThreadId}");
});
Console.WriteLine($"Back to thread: {Environment.CurrentManagedThreadId}");
}
}
输出示例:
Main thread: 1
Task running on thread: 4
Back to thread: 4
说明
- 控制台应用默认没有同步上下文,异步操作不会回到原始线程。
- 可以通过设置自定义的
SynchronizationContext
来实现类似 UI 应用的行为。
自定义同步上下文
有时需要为特定的场景实现自定义调度逻辑,可以通过继承 SynchronizationContext
来完成。
最佳实践
-
UI 应用
- 避免手动使用
SynchronizationContext
,优先使用async/await
自动处理上下文切换。 - 确保耗时任务运行在线程池上,避免阻塞 UI 线程。
- 避免手动使用
-
后台服务
- 确保理解没有同步上下文的行为,必要时可引入自定义上下文以实现特定的调度逻辑。
-
避免绑定到平台特性
- 使用通用的
SynchronizationContext
抽象,而非特定平台的调度实现(如 WPF 的Dispatcher
)。
- 使用通用的
-
性能优化
- 在性能关键场景下,通过设置
ConfigureAwait(false)
来避免捕获上下文,提高异步代码的运行效率。
- 在性能关键场景下,通过设置
SynchronizationContext
是管理代码调度和线程上下文的强大工具,但需要合理使用。通过理解其机制和常见场景,可以有效编写高效、稳定的异步程序。
13.7 自定义同步上下文
上一节对同步上下文有个基本介绍,这一节整几个有特殊功能的自定义同步上下文加深理解。
自定义 SynchronizationContext
可以用来解决特殊场景下的线程调度需求,例如实现任务的优先级管理、针对特定线程的任务调度或处理消息队列等。以下是几个更实用的 SynchronizationContext
示例,它们展示了如何在不同场景中扩展或替换默认行为。
通过
SynchronizationContext.SetSynchronizationContext
可以设置当前线程的同步上下文,并通过SynchronizationContext.Current
返回线程的同步上下文实例
示例代码1:将任务调度到特定线程
在某些场景中,你可能需要将所有任务调度到一个指定的线程(例如专用的后台线程)进行处理。
using System;
using System.Collections.Concurrent;
using System.Threading;
using System.Threading.Tasks;
class SingleThreadSynchronizationContext : SynchronizationContext
{
private readonly Thread _workerThread;
private readonly BlockingCollection<(SendOrPostCallback, object?)> _taskQueue = new();
public SingleThreadSynchronizationContext()
{
_workerThread = new Thread(() =>
{
while (true)
{
var task = _taskQueue.Take();
task.Item1(task.Item2);
}
})
{
IsBackground = true
};
_workerThread.Start();
}
public override void Post(SendOrPostCallback d, object? state)
{
_taskQueue.Add((d, state));
}
public override void Send(SendOrPostCallback d, object? state)
{
if (Thread.CurrentThread == _workerThread)
{
d(state);
}
else
{
using var doneEvent = new ManualResetEvent(false);
_taskQueue.Add((s =>
{
d(s);
doneEvent.Set();
}, state));
doneEvent.WaitOne();
}
}
}
class Program
{
static async Task Main()
{
var syncContext = new SingleThreadSynchronizationContext();
SynchronizationContext.SetSynchronizationContext(syncContext);
Console.WriteLine($"Main thread: {Thread.CurrentThread.ManagedThreadId}");
await Task.Run(() => Console.WriteLine($"Task thread: {Thread.CurrentThread.ManagedThreadId}"));
SynchronizationContext.Current!.Post(_ =>
{
Console.WriteLine($"Posted to single thread: {Thread.CurrentThread.ManagedThreadId}");
}, null);
await Task.Delay(1000);
}
}
实现要点
- 使用
BlockingCollection
实现任务队列。 - 在
Post
方法中将任务添加到队列,后台线程逐个执行任务。 - 在
Send
方法中确保同步调用任务。 - 通过
SynchronizationContext.SetSynchronizationContext(syncContext)
将当前线程的同步上下文设置为syncContext,并通过SynchronizationContext.Current
返回设置的同步上下文实例syncContext
使用场景
- 在游戏开发中,调度任务到一个固定线程以操作游戏状态或渲染。
- 为资源密集型任务提供专用线程。
示例代码2:支持优先级的同步上下文
在某些场景中,任务可能需要按照优先级执行,例如高优先级任务应该先于低优先级任务运行。
using System;
using System.Collections.Concurrent;
using System.Linq;
using System.Threading;
class PrioritySynchronizationContext : SynchronizationContext
{
private readonly Thread _workerThread;
private readonly BlockingCollection<(int Priority, SendOrPostCallback Callback, object? State)> _taskQueue
= new(new PriorityComparer());
public PrioritySynchronizationContext()
{
_workerThread = new Thread(() =>
{
foreach (var task in _taskQueue.GetConsumingEnumerable())
{
task.Callback(task.State);
}
})
{
IsBackground = true
};
_workerThread.Start();
}
public override void Post(SendOrPostCallback d, object? state)
{
Post(d, state, 0);
}
public void Post(SendOrPostCallback d, object? state, int priority)
{
_taskQueue.Add((priority, d, state));
}
private class PriorityComparer : IComparer<(int Priority, SendOrPostCallback, object?)>
{
public int Compare((int Priority, SendOrPostCallback, object?) x, (int Priority, SendOrPostCallback, object?) y)
{
return y.Priority.CompareTo(x.Priority);
}
}
}
class Program
{
static void Main()
{
var syncContext = new PrioritySynchronizationContext();
SynchronizationContext.SetSynchronizationContext(syncContext);
Console.WriteLine("Scheduling tasks...");
syncContext.Post(_ => Console.WriteLine("Task 1: Low Priority"), null, priority: 1);
syncContext.Post(_ => Console.WriteLine("Task 2: High Priority"), null, priority: 10);
syncContext.Post(_ => Console.WriteLine("Task 3: Medium Priority"), null, priority: 5);
Thread.Sleep(1000); // Allow background thread to process tasks
}
}
实现要点
- 使用
BlockingCollection
和自定义IComparer
实现优先级队列。 - 扩展
Post
方法以支持优先级参数。 - 后台线程从高优先级任务开始依次处理。
使用场景
- 任务调度需要严格按照优先级顺序执行,例如实时系统或多任务操作系统模拟。
- 高优先级任务(如报警信号)优先于低优先级任务。
示例代码3:模拟异步消息处理的同步上下文
在消息驱动的系统中,可以使用同步上下文将消息处理调度到一个模拟的事件循环中。
using System;
using System.Collections.Concurrent;
using System.Threading;
using System.Threading.Tasks;
class EventLoopSynchronizationContext : SynchronizationContext
{
private readonly BlockingCollection<SendOrPostCallback> _queue = new();
private readonly Thread _loopThread;
public EventLoopSynchronizationContext()
{
_loopThread = new Thread(() =>
{
foreach (var callback in _queue.GetConsumingEnumerable())
{
callback(null);
}
})
{
IsBackground = true
};
_loopThread.Start();
}
public override void Post(SendOrPostCallback d, object? state)
{
_queue.Add(s => d(state));
}
public Task PostAsync(Func<Task> func)
{
var tcs = new TaskCompletionSource();
Post(async _ =>
{
try
{
await func();
tcs.SetResult();
}
catch (Exception ex)
{
tcs.SetException(ex);
}
}, null);
return tcs.Task;
}
}
class Program
{
static async Task Main()
{
var syncContext = new EventLoopSynchronizationContext();
SynchronizationContext.SetSynchronizationContext(syncContext);
Console.WriteLine("Posting async tasks...");
await syncContext.PostAsync(async () =>
{
Console.WriteLine($"Task 1 start on thread: {Thread.CurrentThread.ManagedThreadId}");
await Task.Delay(1000);
Console.WriteLine("Task 1 complete");
});
await syncContext.PostAsync(async () =>
{
Console.WriteLine($"Task 2 start on thread: {Thread.CurrentThread.ManagedThreadId}");
await Task.Delay(500);
Console.WriteLine("Task 2 complete");
});
}
}
实现要点
- 使用
BlockingCollection
作为消息队列。 - 通过
PostAsync
方法实现对异步操作的支持。 - 主线程模拟消息循环,处理异步消息。
使用场景
- 模拟事件驱动系统或消息总线。
- 异步任务需要有序地逐个执行,例如在后台线程中实现队列式的任务处理。
总结
自定义同步上下文 | 关键功能 | 使用场景 |
---|---|---|
SingleThreadSynchronizationContext |
调度任务到指定线程 | 游戏开发、专用线程处理 |
PrioritySynchronizationContext |
按任务优先级调度 | 实时系统、优先级任务 |
EventLoopSynchronizationContext |
异步消息处理 | 消息驱动系统、事件总线 |
这些示例展示了如何通过自定义同步上下文应对特定场景中的调度需求。根据实际需求,可以进一步调整任务处理的策略和方式。
13.7 线程池 (ThreadPool) 和调度策略
线程池通过复用线程来减少线程创建和销毁的开销。在现代并发编程中,线程池在任务调度和执行中起着核心作用。本节主要理解线程池的默认行为、调度策略。在不了解线程池工作原理的情况下,不建议手动优化线程池,默认情况下已经是经过了反复调优。
1. 线程池的基本概念
线程池是由系统管理的一组预先创建或动态分配的线程,用于执行短期任务。线程池的主要特点包括:
- 复用线程:减少线程创建和销毁的开销。
- 动态扩展:根据工作负载动态调整线程的数量。
- 任务调度:线程池内置了任务排队机制,以合理分配 CPU 资源。
2. 线程池的核心结构
线程池是由以下几个主要组件组成的:
-
工作线程(Worker Threads):
- 用于执行普通的 CPU 密集型任务。
- 线程池会自动管理工作线程的创建、销毁和复用。
-
I/O 线程(I/O Completion Port Threads):
- 专门处理异步 I/O 操作(如文件操作、网络请求等)。
- 这些线程由操作系统内核管理,通过 I/O 完成端口(I/O Completion Port,简称 IOCP)实现高效的异步完成通知。
-
任务队列(Task Queue):
- 每个任务有一个单独的任务队列。
- 线程池维护一个全局的任务队列,用于存储待执行的任务。
- 如果当前没有空闲线程,任务会被暂时放入队列,等待调度。
3. 工作线程(Worker Thread)的调度机制
任务提交与处理
-
任务提交:
- 当调用
ThreadPool.QueueUserWorkItem
或Task.Run
时,任务被添加到线程池的任务队列中。 - 如果有空闲线程,线程池会立即从队列中取出任务并执行。
- 如果没有空闲线程,任务会在队列中等待,直到线程池创建新线程或释放现有线程。
- 当调用
-
线程池的线程复用:
- 线程池会尝试复用已有的线程来执行任务,而不是每次都创建新线程,从而减少线程创建和销毁的开销。
线程扩展与收缩
-
线程扩展:
- 当任务量超过线程池当前的线程数量时,线程池会动态增加线程。
- 线程池的扩展速度是受限制的,默认每秒最多增加 2 个线程,以避免过度消耗系统资源。
-
线程回收:
- 如果线程长时间(默认 10 秒)没有处理任务,线程池会回收这些空闲线程以减少资源占用。
- 回收的线程并不会被销毁,而是进入一个备用状态,等待再次被复用。
4. I/O 线程与异步操作的调度机制
I/O 完成端口(IOCP)
.NET 的线程池通过操作系统的 I/O Completion Port (IOCP) 高效地管理异步 I/O 操作:
-
异步任务提交:
- 当应用程序发起异步 I/O 操作(如
FileStream.ReadAsync
或网络请求)时,操作系统会将任务提交到 IOCP,此时并没有任何线程去等待这个操作,详情参考There Is No Thread。
- 当应用程序发起异步 I/O 操作(如
-
I/O 异步完成:
- 操作系统会在 I/O 操作完成时,将完成通知放入 IOCP 队列,并唤醒一个 I/O 线程来处理完成事件此时才有线程去使用 I/O 操作的结果。
-
I/O 线程池管理:
- .NET 为 IOCP 提供了专门的线程池(即 I/O 线程池),以高效处理异步 I/O 操作的完成回调。
I/O 线程与工作线程的区别
特性 | 工作线程 | I/O 线程 |
---|---|---|
用途 | 执行普通任务 | 处理异步 I/O 操作的完成事件 |
来源 | 由 .NET 管理 | 由操作系统 IOCP 提供 |
线程数量 | 可配置(默认动态扩展) | 系统自动管理 |
使用场景 | CPU 密集型任务 | 网络、文件等异步 I/O 操作 |
5. 任务队列的实现
线程池的任务队列是一个线程安全的结构,用于存储待执行的任务。它的实现主要依赖于以下机制:
-
全局任务队列:
- 线程池维护一个全局的任务队列,用于存储所有未被处理的任务。
- 当工作线程空闲时,会从全局队列中取任务执行。
-
局部任务队列(Work-Stealing Queue):
- 每个线程还维护一个局部任务队列,用于存储本线程的任务。
- 如果一个线程的局部任务队列为空,它可以尝试从其他线程的队列中“窃取”(Work-Stealing)任务执行。
-
任务调度:
- 线程池优先处理局部队列中的任务,以减少线程间竞争。
- 如果局部队列为空,则会从全局任务队列中取任务。
- 如果全局任务队列也为空,线程会进入等待状态。
任务队列的优势
- 通过局部任务队列和 Work-Stealing 机制,线程池能有效减少线程间的任务争用,提高任务调度的效率。
- 任务队列的实现是线程安全的,确保多线程环境下的任务提交与调度不会出现数据竞争问题。
6. 线程池默认行为
1. 默认配置
- 线程池线程默认是后台线程,随进程退出而销毁。
- 使用工作队列管理任务,任务会按先进先出(FIFO)顺序调度。
- 最大线程数和最小线程数的默认值与操作系统和 .NET 运行时相关:
- 默认最小线程数:CPU 核心数。
- 默认最大线程数:约 32,767(具体值视运行时而定)。
2. 动态扩展策略
- 如果线程池中的线程不够用,它会动态创建线程。
- 新线程的创建有一定延迟,避免因短期任务暴增而频繁创建销毁线程。
3. 工作线程与 IO 线程
- 工作线程:处理 CPU 密集型任务。
- IO 线程:处理异步 IO 完成回调。
7. 调整线程池行为
1. 设置最小线程数
通过设置最小线程数,可以减少线程池在高负载下的扩展延迟。
ThreadPool.SetMinThreads(workerThreads: 8, completionPortThreads: 8);
2. 获取线程池状态
使用 ThreadPool.GetMinThreads
和 ThreadPool.GetMaxThreads
获取线程池配置:
ThreadPool.GetMinThreads(out int workerMin, out int ioMin);
ThreadPool.GetMaxThreads(out int workerMax, out int ioMax);
Console.WriteLine($"Min Threads: Worker={workerMin}, IO={ioMin}");
Console.WriteLine($"Max Threads: Worker={workerMax}, IO={ioMax}");
3. 调整最大线程数
可以通过 ThreadPool.SetMaxThreads
限制线程池的并发任务数量:
ThreadPool.SetMaxThreads(workerThreads: 16, completionPortThreads: 16);
8. 线程池的优缺点
优点
- 线程复用: 减少线程创建和销毁的开销。
- 自动管理: 动态调整线程数以适应任务负载。
- 线程安全: 内部实现了高效的任务队列和调度机制。
- I/O 异步支持: 通过 IOCP 高效处理异步操作。
缺点
- 不适合长时间任务: 长时间运行的任务会阻塞线程池线程,影响其他任务的执行。
- 调度延迟: 如果任务过多且线程池扩展速度跟不上,可能出现任务调度延迟。
9. ThreadPool.QueueUserWorkItem
与 Task.Run
的差异
1. 使用方式和目标
特性 | ThreadPool.QueueUserWorkItem |
Task.Run |
---|---|---|
抽象级别 | 低级别,直接向线程池提交任务 | 高级别,基于线程池实现的任务抽象 |
返回值 | 无返回值,无法捕获任务的结果 | 返回 Task 对象,可跟踪任务状态 |
异常处理 | 异常会导致程序崩溃 | 支持异常捕获和处理 |
适用场景 | 轻量级任务,无需结果或复杂管理 | 需要更强大的任务管理功能,如链式任务 |
2. 示例代码
使用 ThreadPool.QueueUserWorkItem
提交任务:
using System;
using System.Threading;
class Program
{
static void Main()
{
ThreadPool.QueueUserWorkItem(_ =>
{
Console.WriteLine($"ThreadPool Task: {Thread.CurrentThread.ManagedThreadId}");
});
Console.WriteLine("Main Thread Completed");
Thread.Sleep(100); // 等待任务完成
}
}
使用 Task.Run
提交任务:
using System;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
var task = Task.Run(() =>
{
Console.WriteLine($"Task Run: {Task.CurrentId} on Thread {Thread.CurrentThread.ManagedThreadId}");
});
await task;
Console.WriteLine("Main Task Completed");
}
}
关键区别:
ThreadPool.QueueUserWorkItem
更适合简单的、无需跟踪的任务。Task.Run
提供更高级的任务跟踪和组合能力,推荐在现代并发代码中使用。
若要将某个代码调度到线程池线程中执行,推荐使用
Task.Run
,不要再使用ThreadPool.QueueUserWorkItem
了,已经过时了
总结
线程池是 .NET 中一个高效的线程管理工具,其底层机制包括:
- 线程复用与动态扩展: 减少线程创建销毁开销,动态调整线程数。
- 任务队列与调度: 通过全局队列和局部队列的 Work-Stealing 机制提高效率。
- I/O 完成端口: 高效处理异步 I/O 操作。
- 线程回收: 空闲线程在一定时间后被回收,节省资源。