C#异步编程
C#异步编程笔记
1.异步概念
- 异步(async):当一个方法被调用时立即返回,并获取一个线程执行该方法的逻辑,调用者不用等待方法执行完毕。
- **为什么使用异步:**将一些可能阻塞线程的任务,分配到别的线程中,这样当前线程不会被阻塞,可以继续执行逻辑。
2.Thread(线程)
-
前台线程:(Thread类默认创建的是前台线程)
-
private static void Main(string[] args) { Thread t1 = new Thread(() => { Console.WriteLine("线程1"); Console.WriteLine("等待3秒"); Thread.Sleep(3000);//阻塞当前线程 Console.WriteLine("线程1结束"); }); Thread t2 = new Thread(() => { Console.WriteLine("线程2"); Console.WriteLine("等待4秒"); Thread.Sleep(4000);//阻塞当前线程 Console.WriteLine("线程2结束"); }); t1.Start(); t2.Start(); Console.WriteLine("主线程任务"); //主线程结束,但是进程没有结束,Thread默认创建的是前台线程,执行完才会结束进程。 }
-
-
使用Thread频繁创建和销毁线程很消耗性能。Task创建的不是新的线程,是使用线程池管理的线程,用完的线程会回到线程池。
3.Task(异步)
1.后台线程:(Task创建的线程都是后台线程(从系统管理的线程池中分配线程))
private static void Main(string[] args)
{
Task t1 = new Task(() =>
{
Console.WriteLine("线程1");
Console.WriteLine("等待3秒");
Thread.Sleep(3000);//阻塞当前线程
Console.WriteLine("线程1结束");
});
Task t2 = new Task(() =>
{
Console.WriteLine("线程2");
Console.WriteLine("等待3秒");
Thread.Sleep(3000);//阻塞当前线程
Console.WriteLine("线程2结束");
});
t1.Start();
t2.Start();
Console.WriteLine("主线程结束");
//Task是后台线程,主线程结束,进程立刻结束,不会等待后台线程执行完。
//使用Console.ReadKey();等待键盘输入可以阻止主线结束,看到后台线程输出。
}
2. Task创建和启动线程的方法
使用Task创建线程不是创建一个新线程,是由线程池管理的线程中分配的。
线程池中有两种线程:workerThreads线程和completionPortThreads线程。
Task创建线程默认使用 workerThreads 线程处理逻辑。
当使用异步I/O操作(如 File.ReadAllTextAsync、HttpClient.GetAsync)时,系统会自动使用 completionPortThreads 线程处理I/O完成回调,这些操作在等待期间不占用线程。
private static void Main(string[] args)
{
// 通过new线程池分配一个线程,使用Start开始
Task<string> task1 = new Task<string>(() =>
{
Thread.Sleep(3000);//阻塞当前线程
return $"task1的线程ID是{Thread.CurrentThread.ManagedThreadId}";
});
task1.Start();
//使用Task.Run(Func func)线程池分配一个线程,并启动线程
Task<string> task3 = Task.Run<string>(() =>
{
Thread.Sleep(3000);//阻塞当前线程
return $"task1的线程ID是{Thread.CurrentThread.ManagedThreadId}";
});
//使用Task.Factory.StartNew(Func func)线程池分配一个线程,并启动线程
Task<string> task2 = Task.Factory.StartNew<string>(() =>
{
Thread.Sleep(3000);//阻塞当前线程
return $"task1的线程ID是{Thread.CurrentThread.ManagedThreadId}";
});
Console.WriteLine("主线程逻辑");
//task.Result获取结果时会阻塞主线程,调用了task.Wait()
Console.WriteLine(task1.Result);//阻塞主线程
Console.WriteLine(task2.Result);//阻塞主线程
Console.WriteLine(task3.Result);//阻塞主线程
//所有线程结束才会输出
Console.WriteLine("主线程结束");
}
3.Task等待和延续
-
Task<string> task = new Task<string>(null!); task.Wait();//用于Task对象,task.wait会等待线程完成(阻塞主线程)。 Task.WaitAny();//等待传入的线程列表中任意一个线程执行完,返回最先完成的线程索引,传入空返回-1。(阻塞主线程)。 Task.WaitAll();//等待传入的线程列表中所有线程执行完(阻塞主线程)。 task.ContinueWith(Delete);//当一个Task对象完成时,接收调用方的状态信息,在目标Task完成时执行。(不阻塞主线程)。 Task.WhenAny();//返回传入的最先完成的线程。可以与ContinueWith配合,线程列表中任何一个任务执行完毕,则继续ContinueWith中的任务(不阻塞主线程) Task.WhenAll();//返回所有线程的结果数组。可以与ContinueWith配合,线程列表中所有任务执行完毕,则继续ContinueWith中的任务(不阻塞主线程) Task.Factory.ContinueWhenAny();//类似Task.WhenAny().ContinueWith Task.Factory.ContinueWhenAll();//类似Task.WhenAll().ContinueWith
private static void Main(string[] args)
{
List<Task<int>> tasks = new List<Task<int>>
{
Task.Run(() => { Thread.Sleep(4000); return 1; }),
Task.Run(() => { Thread.Sleep(3000); return 2; }),
Task.Run(() => { Thread.Sleep(1000); return 3; }),
};
for(int i=0; i < tasks.Count; i++)
{
Console.WriteLine($"第{i + 1}个任务ID:{tasks[i].Id}");
}
int n = Task.WaitAny(tasks.ToArray());//阻塞主线程一秒
Console.WriteLine($"最先完成的线程在数组中的索引:{n}");
Task.WhenAny(tasks).ContinueWith(x =>
{
Task<int> firstTask = x.Result;
Console.WriteLine($"第一个完成的任务:任务{firstTask.Result},任务ID:{firstTask.Id}");
});
Task.WhenAll(tasks).ContinueWith(x =>
{
int[] allTasks = x.Result;
Console.WriteLine($"所有结果:[{string.Join(",", allTasks)}], 包装任务ID:{x.Id}");
});
Task.Factory.ContinueWhenAny(tasks.ToArray(), firstCompletedTask =>
Console.WriteLine($"第一个完成的任务:任务{firstCompletedTask.Id}"));
Task.Factory.ContinueWhenAll(tasks.ToArray(),completedTasks =>
{
Console.Write("所有结果:[");
foreach (var n in completedTasks)
{
Console.Write(n.Result +" ");
}
Console.WriteLine("]");
});
}
注:对于主线程来说是异步,但是使用Thread.Sleep(),线程池中被分配的线程是被阻塞的,这需要使用await关键字。
4.async与await
- async
- 修饰方法,lambda表达式或匿名方法,指定为异步。
- async修饰的方法若不包含await表达式或语句,则该方法同步。
- await
- 只能用在async修饰的方法中
private static void Main(string[] args)
{
Console.WriteLine("开始主线程");
Task t = DoSomethingAsync();
task.ContinueWith(t => Console.WriteLine("异步方法完成后执行"));
Console.WriteLine("主线程执行其他逻辑");
t.Wait();
}
private static async Task DoSomethingAsync()
{
Task<int> task = Task.Run<int>(async () =>
{
Console.WriteLine("开始任务");
await Task.Delay(10000);//异步等待
Console.WriteLine("任务结束");
return 10;
});
Console.WriteLine("主线程执行");
int result = await task;// await task.ContinueWith(t => Console.WriteLine($"任务返回值:{t.Result}"));
Console.WriteLine($"后台线程返回值:{result}");
await Task.Delay(3000);
}
Task.Delay()会返回一个线程,如果不用await修饰,就不会等待(虽然返回的线程在后台执行但是没有等待他)。```
Task dt = Task.Delay(10000);
await dt;//等价与await Task.Delay(10000);
-
再看一看使用await Task.Delay()与Thread.Sleep()的对比
-
Task.Delay:
-
private static void Main(string[] args) { Task t = DoSomethingAsync(); t.Wait(); } private static async Task DoSomethingAsync() { // workerThreads:可用的工作线程数(用于CPU密集型任务) //completionPortThreads:可用的端口线程数(用于I/O操作) //我们只关注工作线程数 ThreadPool.GetAvailableThreads(out int workerThreads, out int completionPortThreads); Console.WriteLine($"开始时可用工作线程: {workerThreads}"); //启动异步任务 Console.WriteLine("启动1000个异步任务(不阻塞线程):"); List<Task> asyncTasks = new List<Task>(); for (int i = 0; i < 1000; i++) { asyncTasks.Add(Task.Run(async () => await Task.Delay(10000))); } await Task.Delay(1000); // 给一点时间分配线程 ThreadPool.GetAvailableThreads(out workerThreads, out completionPortThreads); Console.WriteLine($"启动后可用工作线程: {workerThreads}"); await Task.WhenAll(asyncTasks);//等待所有线程完成 }
-
-
Thread.Sleep():
-
private static async Task DoSomethingAsync() { ThreadPool.GetAvailableThreads(out int workerThreads, out int completionPortThreads); Console.WriteLine($"开始时可用工作线程: {workerThreads}"); //启动同步阻塞任务 Console.WriteLine("启动1000个同步阻塞任务:"); List<Task> syncTasks = new List<Task>(); for (int i = 0; i < 1000; i++) { syncTasks.Add(Task.Run(() => Thread.Sleep(10000))); } await Task.Delay(1000); ThreadPool.GetAvailableThreads(out workerThreads, out completionPortThreads); Console.WriteLine($"启动后可用工作线程: {workerThreads}"); await Task.WhenAll(syncTasks); }
-
-
从输出结果中可以看出,使用Thread.Sleep()会使线程被阻塞占用, 无法被释放,使用Task.Delay()时当进入了异步等待,会释放该线程,去执行其他任务,当等待结束时会执行等待后的逻辑。所以异步操作要使用await,释放正在等待的线程。
-
5.取消线程
涉及到的类是CancellationTokenSource 和 CancellationToken。
直接看代码吧:
private static void Main(string[] args)
{
Task t = DoSomethingAsync();
t.Wait();
}
private static async Task DoSomethingAsync()
{
CancellationTokenSource cts = new CancellationTokenSource();//创建取消指令的发出者
CancellationToken token = cts.Token;//创建取消指令的接收者
//注册取消时的回调函数
cts.Token.Register(() =>
{
Console.WriteLine("取消回调函数被执行");
});
Task<int> task = Task.Run<int>(async () =>
{
for (int i = 0; i < 10; i++)
{
//下面的if也可以改成这个token.ThrowIfCancellationRequested();
//效果一样,用来检查取消状态,取消时会抛出异常
if (token.IsCancellationRequested)
{
Console.WriteLine("线程被取消");
break;
}
Console.WriteLine($"循环{i}");
await Task.Delay(1000, token);//传入取消请求,取消时会响应取消请求,再取消进程。
}
Console.WriteLine("线程结束");
return 1;
}, token);
await Task.Delay(3000);//等待3秒,
cts.Cancel();//手动取消
//等待三秒和取消可以合并,只需要在创建时调用CancellationTokenSource的有参构造函数。
//CancellationTokenSource cts = new CancellationTokenSource(3000);
try
{
int result = await task;
Console.WriteLine($"任务结果: {result}");
}
catch (OperationCanceledException)
{
Console.WriteLine("任务已取消");
}
}
只循环了3次,执行了回调函数,捕获了异常。输出结果是循环0 循环1 循环2 取消回调函数被执行 任务已取消。
6.异步在游戏开发中的应用
-
1. AI行为和控制
-
public abstract class ABehaviorTree : MonoBehaviour { private Node root = null; private CancellationTokenSource evaluationCTS; private CancellationToken token; protected virtual void Start() { root = SetUpTree(); token = evaluationCTS.Token; } protected virtual void Update() { if (root != null) root.Evaluate(); } private async void StartAsyncEvaluation() { evaluationCTS = new CancellationTokenSource(); while (!evaluationCTS.Token.IsCancellationRequested) { try { SetAIState(AIState.Idle); if (root != null) { var result = await root.EvaluateAsync(token);// 异步评估 HandleEvaluationResult(result); } await Task.Delay(100, evaluationCTS.Token); } catch (OperationCanceledException) { break; } catch (Exception ex) { Debug.LogError($"行为树评估错误: {ex.Message}"); await Task.Delay(1000, evaluationCTS.Token); } } } protected abstract Node SetUpTree(); }//在进行评估时,可以保证敌人的行为流畅
-
-
2. 资源加载
-
3. 场景切换
-
4.网络请求
等等还有其他的方面。
参考:
更多推荐


所有评论(0)