Task和async/await详解

本文介绍了C#中异步编程的基础知识,包括Task的创建与运行、阻塞方法以及延续操作,同时对比了Thread和ThreadPool的差异。文章还讲解了async/await关键字如何简化异步编程,通过示例展示了异步读取文件内容的过程,强调了async/await基于Task并提高了任务执行效率。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、什么是异步

同步和异步主要用于修饰方法。当一个方法被调用时,调用者需要等待该方法执行完毕并返回才能继续执行,我们称这个方法是同步方法;当一个方法被调用时立即返回,并获取一个线程执行该方法内部的业务,调用者不用等待该方法执行完毕,我们称这个方法为异步方法。

异步的好处在于非阻塞(调用线程不会暂停执行去等待子线程完成),因此我们把一些不需要立即使用结果、较耗时的任务设为异步执行,可以提高程序的运行效率。net4.0在ThreadPool的基础上推出了Task类,微软极力推荐使用Task来执行异步任务,现在C#类库中的异步方法基本都用到了Task;net5.0推出了async/await,让异步编程更为方便。本篇主要介绍Task、async/await相关的内容,其他异步操作的方式会在下一篇介绍。

二、Task介绍

Task是在ThreadPool的基础上推出的,我们简单了解下ThreadPool。ThreadPool中有若干数量的线程,如果有任务需要处理时,会从线程池中获取一个空闲的线程来执行任务,任务执行完毕后线程不会销毁,而是被线程池回收以供后续任务使用。当线程池中所有的线程都在忙碌时,又有新任务要处理时,线程池才会新建一个线程来处理该任务,如果线程数量达到设置的最大值,任务会排队,等待其他任务释放线程后再执行。线程池能减少线程的创建,节省开销,看一个ThreadPool的栗子吧

 static void Main(string[] args)
        {
            for (int i = 1; i <=10; i++)
            {
                //ThreadPool执行任务
                ThreadPool.QueueUserWorkItem(new WaitCallback((obj) => {
                    Console.WriteLine($"第{obj}个执行任务");
                }),i);
            }
            Console.ReadKey();
        }

上边的代码通过ThreadPool执行了10个任务,执行结果为:
在这里插入图片描述ThreadPool相对于Thread来说可以减少线程的创建,有效减小系统开销;但是ThreadPool不能控制线程的执行顺序,我们也不能获取线程池内线程取消/异常/完成的通知,即我们不能有效监控和控制线程池中的线程。

  • 1、Task创建和运行

我们知道了ThreadPool的弊端:我们不能控制线程池中线程的执行顺序,也不能获取线程池内线程取消/异常/完成的通知。net4.0在ThreadPool的基础上推出了Task,Task拥有线程池的优点,同时也解决了使用线程池不易控制的弊端。

首先看一下怎么去创建并运行一个Task,Task的创建和执行方式有如下三种:

 static void Main(string[] args)
        {
            //1.new方式实例化一个Task,需要通过Start方法启动
            Task task = new Task(() =>
            {
                Thread.Sleep(100);
                Console.WriteLine($"hello, task1的线程ID为{Thread.CurrentThread.ManagedThreadId}");
            });
            task.Start();

            //2.Task.Factory.StartNew(Action action)创建和启动一个Task
            Task task2 = Task.Factory.StartNew(() =>
              {
                  Thread.Sleep(100);
                  Console.WriteLine($"hello, task2的线程ID为{ Thread.CurrentThread.ManagedThreadId}");
              });

            //3.Task.Run(Action action)将任务放在线程池队列,返回并启动一个Task
            Task task3 = Task.Run(() =>
              {
                  Thread.Sleep(100);
                  Console.WriteLine($"hello, task3的线程ID为{ Thread.CurrentThread.ManagedThreadId}");
              });
            Console.WriteLine("执行主线程!");
            Console.ReadKey();
        }

执行结果如下:
在这里插入图片描述 我们看到先打印"执行主线程",然后再打印各个任务,说明了Task不会阻塞主线程。上边的栗子Task都没有返回值,我们也可以创建有返回值的Task,用法和没有返回值的基本一致,我们简单修改一下上边的栗子,代码如下:

  static void Main(string[] args)
        {
            ////1.new方式实例化一个Task,需要通过Start方法启动
            Task<string> task = new Task<string>(() =>
            {
                return $"hello, task1的ID为{Thread.CurrentThread.ManagedThreadId}";
            });
            task.Start();

            ////2.Task.Factory.StartNew(Func func)创建和启动一个Task
           Task<string> task2 =Task.Factory.StartNew<string>(() =>
            {
                return $"hello, task2的ID为{ Thread.CurrentThread.ManagedThreadId}";
            });

            ////3.Task.Run(Func func)将任务放在线程池队列,返回并启动一个Task
           Task<string> task3= Task.Run<string>(() =>
            {
                return $"hello, task3的ID为{ Thread.CurrentThread.ManagedThreadId}";
            });

            Console.WriteLine("执行主线程!");
            Console.WriteLine(task.Result);
            Console.WriteLine(task2.Result);
            Console.WriteLine(task3.Result);
            Console.ReadKey();
        }

注意task.Resut获取结果时会阻塞线程,即如果task没有执行完成,会等待task执行完成获取到Result,然后再执行后边的代码,程序运行结果如下:
在这里插入图片描述
上边的所有栗子中Task的执行都是异步的,不会阻塞主线程。有些场景下我们想让Task同步执行怎么办呢?Task提供了 task.RunSynchronously() 用于同步执行Task任务,代码如下:

static void Main(string[] args)
        {
            Task task = new Task(() =>
            {
                Thread.Sleep(100);
                Console.WriteLine("执行Task结束!");
            });
            //同步执行,task会阻塞主线程
            task.RunSynchronously();
            Console.WriteLine("执行主线程结束!");
            Console.ReadKey();
        }

执行结果如下:
在这里插入图片描述

2、Task的阻塞方法(Wait/WaitAll/WaitAny)

1.Thread阻塞线程的方法

使用Thread时,我们知道用thread.Join()方法即可阻塞主线程。看一个例子:

  static void Main(string[] args)
        {
            Thread th1 = new Thread(() => {
                Thread.Sleep(500);
                Console.WriteLine("线程1执行完毕!");
            });
            th1.Start();
            Thread th2 = new Thread(() => {
                Thread.Sleep(1000);
                Console.WriteLine("线程2执行完毕!");
            });
            th2.Start();
            //阻塞主线程
            th1.Join();
            th2.Join();
            Console.WriteLine("主线程执行完毕!");
            Console.ReadKey();
        }

如果注释掉两个Join,执行结果是:先打印【主线程执行完毕】,而添加两个Join方法后执行结果如下,实现了线程阻塞:
 在这里插入图片描述

2.Task的Wait/WaitAny/WaitAll方法

Thread的Join方法可以阻塞调用线程,但是有一些弊端:
 ①如果我们要实现很多线程的阻塞时,每个线程都要调用一次Join方法;
 ②如果我们想让所有的线程执行完毕(或者任一线程执行完毕)时,立即解除阻塞,使用Join方法不容易实现。Task提供了 Wait/WaitAny/WaitAll 方法,可以更方便地控制线程阻塞。

task.Wait() 表示等待task执行完毕,功能类似于 thead.Join(); Task.WaitAll(Task[] tasks) 表示只有所有的task都执行完成了再解除阻塞; Task.WaitAny(Task[] tasks) 表示只要有一个task执行完毕就解除阻塞,看一个栗子:

static void Main(string[] args)
        {
            Task task1 = new Task(() => {
                Thread.Sleep(500);
                Console.WriteLine("线程1执行完毕!");
            });
            task1.Start();
            Task task2 = new Task(() => {
                Thread.Sleep(1000);
                Console.WriteLine("线程2执行完毕!");
            });
            task2.Start();
            //阻塞主线程。task1,task2都执行完毕再执行主线程
       //执行【task1.Wait();task2.Wait();】可以实现相同功能
            Task.WaitAll(new Task[]{ task1,task2});
            Console.WriteLine("主线程执行完毕!");
            Console.ReadKey();
        }

执行结果如下:
在这里插入图片描述
如果将栗子中的WaitAll换成WaitAny,那么任一task执行完毕就会解除线程阻塞,执行结果是:先打印【线程1执行完毕】,然后打印【主线程执行完毕】,最后打印【线程2执行完毕】

3、Task的延续操作(WhenAny/WhenAll/ContinueWith)

上边的Wait/WaitAny/WaitAll方法返回值为void,这些方法单纯的实现阻塞线程。我们现在想让所有task执行完毕(或者任一task执行完毕)后,开始执行后续操作,怎么实现呢?这时就可以用到WhenAny/WhenAll方法了,这些方法执行完成返回一个task实例。
  task.WhenAll(Task[] tasks) 表示所有的task都执行完毕后再去执行后续的操作, task.WhenAny(Task[] tasks) 表示任一task执行完毕后就开始执行后续操作。看一个栗子:

 static void Main(string[] args)
        {
            Task task1 = new Task(() => {
                Thread.Sleep(500);
                Console.WriteLine("线程1执行完毕!");
            });
            task1.Start();
            Task task2 = new Task(() => {
                Thread.Sleep(1000);
                Console.WriteLine("线程2执行完毕!");
            });
            task2.Start();
            //task1,task2执行完了后执行后续操作
            Task.WhenAll(task1, task2).ContinueWith((t) => {
                Thread.Sleep(100);
                Console.WriteLine("执行后续操作完毕!");
            });

            Console.WriteLine("主线程执行完毕!");
            Console.ReadKey();
        }

执行结果如下,我们看到WhenAll/WhenAny方法不会阻塞主线程,当使用WhenAll方法时所有的task都执行完毕才会执行后续操作;如果把栗子中的WhenAll替换成WhenAny,则只要有一个线程执行完毕就会开始执行后续操作,这里不再演示。
在这里插入图片描述
 上边的栗子也可以通过
  Task.Factory.ContinueWhenAll(Task[] tasksAction continuationAction)Task.Factory.ContinueWhenAny(Task[] tasks, Action continuationAction) 来实现 ,修改上边栗子代码如下,执行结果不变。

static void Main(string[] args)
        {
            Task task1 = new Task(() => {
                Thread.Sleep(500);
                Console.WriteLine("线程1执行完毕!");
            });
            task1.Start();
            Task task2 = new Task(() => {
                Thread.Sleep(1000);
                Console.WriteLine("线程2执行完毕!");
            });
            task2.Start();
            //通过TaskFactroy实现
            Task.Factory.ContinueWhenAll(new Task[] { task1, task2 }, (t) =>
            {
                Thread.Sleep(100);
                Console.WriteLine("执行后续操作");
            });

            Console.WriteLine("主线程执行完毕!");
            Console.ReadKey();
        }

4、Task的任务取消(CancellationTokenSource)

1.Thread取消任务执行

在Task前我们执行任务采用的是Thread,Thread怎么取消任务呢?一般流程是:设置一个变量来控制任务是否停止,如设置一个变量isStop,然后线程轮询查看isStop,如果isStop为true就停止,代码如下:

 static void Main(string[] args)
        {
            bool isStop = false;
            int index = 0;
            //开启一个线程执行任务
            Thread th1 = new Thread(() =>
              {
                  while (!isStop)
                  {
                      Thread.Sleep(1000);
                      Console.WriteLine($"第{++index}次执行,线程运行中...");
                  }
              });
            th1.Start();
            //五秒后取消任务执行
            Thread.Sleep(5000);
            isStop = true;
            Console.ReadKey();
        }

2.Task取消任务执行

Task中有一个专门的类 CancellationTokenSource 来取消任务执行,还是使用上边的例子,我们修改代码如下,程序运行的效果不变。

  static void Main(string[] args)
        {
            CancellationTokenSource source = new CancellationTokenSource();
            int index = 0;
            //开启一个task执行任务
            Task task1 = new Task(() =>
              {
                  while (!source.IsCancellationRequested)
                  {
                      Thread.Sleep(1000);
                      Console.WriteLine($"第{++index}次执行,线程运行中...");
                  }
              });
            task1.Start();
            //五秒后取消任务执行
            Thread.Sleep(5000);
            //source.Cancel()方法请求取消任务,IsCancellationRequested会变成true
            source.Cancel();
            Console.ReadKey();
        }

CancellationTokenSource的功能不仅仅是取消任务执行,我们可以使用 source.CancelAfter(5000) 实现5秒后自动取消任务,也可以通过 source.Token.Register(Action action) 注册取消任务触发的回调函数,即任务被取消时注册的action会被执行。 看一个栗子:

 static void Main(string[] args)
        {
            CancellationTokenSource source = new CancellationTokenSource();
            //注册任务取消的事件
            source.Token.Register(() =>
            {
                Console.WriteLine("任务被取消后执行xx操作!");
            });

            int index = 0;
            //开启一个task执行任务
            Task task1 = new Task(() =>
              {
                  while (!source.IsCancellationRequested)
                  {
                      Thread.Sleep(1000);
                      Console.WriteLine($"第{++index}次执行,线程运行中...");
                  }
              });
            task1.Start();
            //延时取消,效果等同于Thread.Sleep(5000);source.Cancel();
            source.CancelAfter(5000);
            Console.ReadKey();
        }

执行结果如下,第5次执行在取消回调后打印,这是因为,执行取消的时候第5次任务已经通过了while()判断,任务已经执行中了:
在这里插入图片描述
最后看上一篇跨线程的栗子,点击按钮启动一个任务,给tetxtbox赋值,我们把Thread改成Task,代码如下:

 public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void mySetValueBtn_Click(object sender, EventArgs e)
        {
            Task.Run(() =>
            {
                Action<int> setValue = (i) => { myTxtbox.Text = i.ToString(); };
                for (int i = 0; i < 1000000; i++)
                {
                    myTxtbox.Invoke(setValue,i);
                }
            });
        }
    }

运行界面如下,赋值的task不会阻塞UI线程:
在这里插入图片描述

三、异步方法(async/await)

在C#5.0中出现的 asyncawait ,让异步编程变得更简单。我们看一个获取文件内容的栗子:

class Program
    {
        static void Main(string[] args)
        {
            string content = GetContentAsync(Environment.CurrentDirectory + @"/test.txt").Result;
            //调用同步方法
            //string content = GetContent(Environment.CurrentDirectory + @"/test.txt");
            Console.WriteLine(content);
            Console.ReadKey();
        }
        //异步读取文件内容
        async static Task<string> GetContentAsync(string filename)
        {
            
            FileStream fs = new FileStream(filename, FileMode.Open);
            var bytes = new byte[fs.Length];
            //ReadAync方法异步读取内容,不阻塞线程
            Console.WriteLine("开始读取文件");
            int len = await fs.ReadAsync(bytes, 0, bytes.Length);
            string result = Encoding.UTF8.GetString(bytes);
            return result;
        }
        //同步读取文件内容
        static string GetContent(string filename)
        {
            FileStream fs = new FileStream(filename, FileMode.Open);
            var bytes = new byte[fs.Length];
            //Read方法同步读取内容,阻塞线程
            int len =  fs.Read(bytes, 0, bytes.Length);
            string result = Encoding.UTF8.GetString(bytes);
            return result;
        }
    }

test.txt内容是【hello world!】执行结果为:
在这里插入图片描述

上边的栗子也写出了同步读取的方式,将main函数中的注释去掉即可同步读取文件内容。我们可以看到异步读取代码和同步读取代码基本一致。async/await让异步编码变得更简单,我们可以像写同步代码一样去写异步代码。注意一个小问题:异步方法中方法签名返回值为Task,代码中的返回值为T。上边栗子中GetContentAsync的签名返回值为Task,而代码中返回值为string。牢记这一细节对我们分析异步代码很有帮助。

异步方法签名的返回值有以下三种:
① Task:如果调用方法想通过调用异步方法获取一个T类型的返回值,那么签名必须为Task;
② Task:如果调用方法不想通过异步方法获取一个值,仅仅想追踪异步方法的执行状态,那么我们可以设置异步方法签名的返回值为Task;
③ void:如果调用方法仅仅只是调用一下异步方法,不和异步方法做其他交互,我们可以设置异步方法签名的返回值为void,这种形式也叫做“调用并忘记”。

小结:到这里Task,async/await的简单使用已经基本结束了,一些高级特性等到工作遇到了再去研究。通过上边的介绍,我们知道async/await是基于Task的,而Task是对ThreadPool的封装改进,主要是为了更有效的控制线程池中的线程(ThreadPool中的线程,我们很难通过代码控制其执行顺序,任务延续和取消等等);ThreadPool基于Thread的,主要目的是减少Thread创建数量和管理Thread的成本。async/await Task是C#中更先进的,也是微软大力推广的特性,我们在开发中可以尝试使用Task来替代Thread/ThreadPool,处理本地IO和网络IO任务是尽量使用async/await来提高任务执行效率。

出处:https://www.cnblogs.com/wyy1234/p/9172467.html

<think>我们正在讨论async/await的工作原理使用方法。根据用户的要求,我们需要使用中文回答,并且遵守LaTeX格式规范。同时,我们需要生成相关问题。首先,我们根据提供的引用内容来整理回答: 引用[1]提到:- async函数返回一个Promise对象。 -await只能用在async函数内部,等待一个Promise对象,并直接得到返回值。 引用[2]提到: -async/await是处理异步操作的语法糖,基于Promise实现。 -async函数自动将返回值包装为Promise。- await会暂停async函数的执行,等待Promise完成,然后恢复执行。-错误处理通过try/catch。-优化并行操作可以使用Promise.all。 -底层原理是Generator+自动执行器。引用[3]提到:- async函数本身不会阻塞当前线程,它立即返回一个Promise。-函数内部await会阻塞等待异步任务完成再继续执行函数内的后续代码。 -返回一个直接量会被封装成Promise.resolve(直接量),没有返回值则相当于返回Promise.resolve(undefined)。-可以通过.then.catch处理async函数返回的Promise。综合以上信息,我们可以组织回答如下:###async/await详解####工作原理async/await是JavaScript中处理异步操作的语法糖,它基于Promise实现,旨在使异步代码的书写阅读更接近同步方式,提高代码的可读性可维护性。 -**async函数**:使用`async`关键字声明的函数会隐式返回一个Promise对象。如果函数返回一个直接量(或没有返回值),则该直接量会被包装成Promise对象(通过`Promise.resolve`)。例如:```javascriptasyncfunction foo() {return42;}//等价于function foo() {returnPromise.resolve(42);}``` -**await表达式**:只能在async函数内部使用。`await`后面通常跟一个Promise对象(也可以跟非Promise值,此时会被立即转换为一个已解决的Promise)。当遇到`await`时,async函数的执行会暂停,直到等待的Promise状态变为解决(fulfilled)或拒绝(rejected)。在等待期间,JavaScript引擎可以处理其他任务(如UI渲染、事件处理等),通过事件循环机制,当Promise解决后,async函数从暂停处恢复执行。底层原理上,async/await可以看作是Generator函数自动执行器的结合。Generator函数可以暂停执行,而自动执行器负责在Promise解决后继续执行Generator函数。####使用方法1.**声明async函数**:在函数声明前添加`async`关键字。2.**使用await等待异步操作**:在async函数内部,使用`await`等待Promise的结果。3.**错误处理**:使用`try/catch`捕获await表达式可能抛出的错误(即Promise被拒绝的情况)。示例: ```javascriptasync functionfetchData() {try{const response= awaitfetch('https://api.example.com/data');const data= awaitresponse.json();console.log(data);}catch (error){console.error('请求失败:',error);}} fetchData();//调用async函数,返回Promise,但不会阻塞主线程```####注意事项-async函数执行时,遇到await会暂停函数内后续代码的执行,但不会阻塞主线程(因为async函数返回Promise后,主线程可以继续执行其他任务)。 -避免在循环中顺序使用await,这可能导致不必要的等待。对于可以并行执行的异步操作,应使用`Promise.all`:```javascript//顺序执行(不推荐)asyncfunction sequential() {constresult1= awaittask1();constresult2= awaittask2();// ...}//并行执行(推荐)asyncfunction parallel() {const[result1,result2] =await Promise.all([task1(), task2()]);// ...}```- async函数返回的Promise对象,可以通过`.then()``.catch()`进行链式处理,也可以在另一个async函数中使用await等待其结果。####总结async/await通过同步的写法实现异步操作,使得代码更易于理解维护。其核心在于async函数返回Promise,而await则等待Promise的结果并暂停函数内部执行(不阻塞主线程)。正确使用错误处理机制并行优化,可以编写高效的异步代码。 §§相关问题§§1. async/await与Promise相比有哪些优势?2.在async函数中,如何同时触发多个异步操作而不需要等待上一个完成?3.为什么说async/await是基于Generator实现的? 4. async函数中抛出错误会怎样?如何处理?</think>### async/await 详解 #### 一、工作原理 async/await 是 JavaScript 处理异步操作的语法糖,基于 Promise 实现,通过同步代码的书写方式实现异步逻辑。 1. **async 函数** - 声明方式:`async function myFunc() {...}` - 始终返回 Promise 对象: - 返回非 Promise 值 ⇒ 自动包装为 `Promise.resolve(值)` - 抛出错误 ⇒ 返回 `Promise.reject(错误)` - 显式返回 Promise ⇒ 直接使用该 Promise $$ \text{async 函数返回值} = \begin{cases} \text{Promise.resolve(x)} & \text{若返回 } x \text{ (非Promise)} \\ \text{Promise.reject(e)} & \text{若抛出错误 } e \\ P & \text{若返回 Promise } P \end{cases} $$ 2. **await 表达式** - 只能在 async 函数内部使用 - 暂停当前 async 函数的执行,等待右侧表达式的完成: - 若右侧是 Promise ⇒ 等待其状态变为 resolved/rejected - 若右侧是非 Promise 值 ⇒ 立即转换为 resolved Promise - 执行机制: - 遇到 `await` 时释放主线程(不阻塞 JS 主线程) - 通过事件循环机制,在 Promise 完成后恢复执行 $$ \text{await } P \text{ 等价于 } P.\text{then}(v \Rightarrow \text{恢复执行}) $$ 3. **底层实现** 基于 Generator + 自动执行器(类似 co 库),将异步操作转化为同步执行流程: ```javascript // 伪代码实现 function _async(generator) { const gen = generator(); function step(nextFn) { const { value, done } = nextFn(); if (done) return Promise.resolve(value); return Promise.resolve(value).then( v => step(() => gen.next(v)), e => step(() => gen.throw(e)) ); } return step(() => gen.next()); } ``` #### 二、使用方法 1. **基础用法** ```javascript async function fetchData() { const response = await fetch('/api/data'); // 等待网络请求 const data = await response.json(); // 等待数据解析 return data; } // 调用 fetchData().then(data => console.log(data)); ``` 2. **错误处理** ```javascript async function safeFetch() { try { const res = await fetch('/api'); return await res.json(); } catch (error) { console.error('请求失败:', error); return { status: 'fallback' }; } } ``` 3. **并行优化** ```javascript // 顺序执行 (不推荐) async function sequential() { const a = await task1(); // 等待完成 const b = await task2(); // 再开始 } // 并行执行 (推荐) async function parallel() { const [a, b] = await Promise.all([task1(), task2()]); } ``` #### 三、关键特性 1. **非阻塞性** - async 函数调用立即返回 Promise,不阻塞主线程[^3] - 函数内部 await 会暂停当前函数执行,但不阻塞界面渲染[^3] 2. **执行流程** ```mermaid graph LR A[调用 async 函数] --> B[返回 Promise 对象] B --> C[主线程继续执行] C --> D{遇到 await} D -->|Promise 未完成| E[暂停 async 函数] E --> F[主线程处理其他任务] F --> G[Promise 完成] G --> H[恢复 async 函数执行] ``` 3. **返回值处理** ```javascript async func() { return 42; // 等价于: return Promise.resolve(42) } func().then(v => console.log(v)); // 输出 42 ``` #### 四、最佳实践 1. 始终用 `try/catch` 包裹可能出错的 `await` 2. 避免在循环中顺序使用 `await`,改用 `Promise.all()` 3. 浏览器中 async 函数不会阻塞 UI 渲染 4. Node.js 中可用 `util.promisify` 转换回调函数 > 示例:文件读取的 async/await 实现 > ```javascript > const fs = require('fs'); > const util = require('util'); > const readFile = util.promisify(fs.readFile); > > async function readConfig() { > try { > const data = await readFile('config.json'); > return JSON.parse(data); > } catch (err) { > console.error('配置文件读取错误', err); > return defaultConfig; > } > } > ```
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值