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.取消线程

涉及到的类是CancellationTokenSourceCancellationToken

直接看代码吧:

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.网络请求

等等还有其他的方面。

参考:

[Task 类 (System.Threading.Tasks) | Microsoft Learn]:

[C# 异步编程详解(Task,async/await)-CSDN博客]:

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐