C#控制台应用中如何禁止Task并行运行(单线程异步)?
控制台应用中实现Task非并行并发的方案
在控制台应用里,默认异步Task会运行在ThreadPool上,多核环境下会出现并行执行的情况。要实现同一时间仅运行一个任务(并发但不并行),且无需手动加锁处理公平性问题,可采用以下几种方案:
方案一:使用TPL Dataflow的ActionBlock
TPL Dataflow的ActionBlock默认采用单线程调度逻辑,所有提交的任务会按顺序执行,天然避免并行。
using System.Threading.Tasks.Dataflow; // 初始化ActionBlock,默认配置为单线程处理任务 var actionBlock = new ActionBlock<Func<Task>>(async func => await func()); // 提交异步任务 actionBlock.Post(async () => { Console.WriteLine("任务1启动"); await Task.Delay(1000); Console.WriteLine("任务1完成"); }); actionBlock.Post(async () => { Console.WriteLine("任务2启动"); await Task.Delay(1000); Console.WriteLine("任务2完成"); }); // 停止接收新任务并等待所有任务执行完毕 actionBlock.Complete(); await actionBlock.Completion;
优势:无需手动维护调度逻辑,封装完善,支持异步任务自动按序执行。
方案二:自定义单线程TaskScheduler
实现自定义TaskScheduler,强制所有任务在同一个线程上执行,严格保证单线程运行。
自定义调度器代码
public class SingleThreadTaskScheduler : TaskScheduler { private readonly Thread _execThread; private readonly BlockingCollection<Task> _taskQueue = new BlockingCollection<Task>(); public SingleThreadTaskScheduler() { _execThread = new Thread(() => { foreach (var task in _taskQueue.GetConsumingEnumerable()) { TryExecuteTask(task); } }); _execThread.IsBackground = true; _execThread.Start(); } protected override IEnumerable<Task>? GetScheduledTasks() { return _taskQueue.ToArray(); } protected override void QueueTask(Task task) { _taskQueue.Add(task); } protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued) { return Thread.CurrentThread == _execThread && TryExecuteTask(task); } }
使用方式
var singleThreadScheduler = new SingleThreadTaskScheduler(); var taskFactory = new TaskFactory(singleThreadScheduler); // 提交异步任务 await taskFactory.StartNew(async () => { Console.WriteLine("任务A启动"); await Task.Delay(1000); Console.WriteLine("任务A完成"); }).Unwrap(); await taskFactory.StartNew(async () => { Console.WriteLine("任务B启动"); await Task.Delay(1000); Console.WriteLine("任务B完成"); }).Unwrap();
优势:完全掌控任务执行线程,适合对单线程执行有严格要求的场景。
方案三:自定义单线程同步上下文
通过自定义SynchronizationContext,让异步任务的回调逻辑都在同一个线程上执行,间接实现非并行并发。
自定义同步上下文代码
public class SingleThreadSyncContext : SynchronizationContext { private readonly BlockingCollection<(SendOrPostCallback Callback, object? State)> _callbackQueue = new(); private readonly Thread _execThread; public SingleThreadSyncContext() { _execThread = new Thread(RunLoop); _execThread.IsBackground = true; _execThread.Start(); } private void RunLoop() { SetSynchronizationContext(this); foreach (var item in _callbackQueue.GetConsumingEnumerable()) { item.Callback(item.State); } } public override void Post(SendOrPostCallback d, object? state) { _callbackQueue.Add((d, state)); } public override void Send(SendOrPostCallback d, object? state) { using var resetEvent = new ManualResetEventSlim(false); Post(_ => { d(state); resetEvent.Set(); }, null); resetEvent.Wait(); } }
使用方式
var syncContext = new SingleThreadSyncContext(); SynchronizationContext.SetSynchronizationContext(syncContext); // 提交异步任务 await Task.Run(async () => { Console.WriteLine("任务X启动"); await Task.Delay(1000); Console.WriteLine("任务X完成"); // 此回调会在自定义同步上下文线程执行 }); await Task.Run(async () => { Console.WriteLine("任务Y启动"); await Task.Delay(1000); Console.WriteLine("任务Y完成"); // 此回调会在自定义同步上下文线程执行 });
注意:Task.Run本身仍会使用ThreadPool线程,但await后的回调逻辑会切换到自定义单线程执行;若需整个任务全程单线程,建议结合方案二的TaskFactory使用。
为什么修改ThreadPool不可行
ThreadPool.SetMaxThreads(1, 1)无法生效的核心原因是:ThreadPool的最小工作线程数默认等于CPU核心数,即使手动设置最小线程数为1,ThreadPool也会根据系统负载动态调整线程数量,且这种方式会影响整个应用的线程池使用,风险较高,不推荐。
内容的提问来源于stack exchange,提问作者cherrypicked
相关产品推荐
相关产品推荐

