C#异步编程基础(线程的基础都在这里,内容比较多,认真看。)async/await

线程总是看,但却总是忘,今天跟着杨大佬的课,整理一下学习笔记,方便以后查阅。

创建线程、线程的一些相关操作

1. 线程的一些基本属性

在这里插入图片描述
在这里插入图片描述
方框表示进程,曲线表示线程。
在这里插入图片描述

在这里插入图片描述CurrentThread:返回当前正在执行的线程,通过Name获取。Console.WriteLine(Thread.CurrentThread.Name);

**

2. Thread.Join()&Thread.Sleep()

**

Join()例子:




    class Program
    {
        static void Main(string[] args)
        {
        
            Thread t = new Thread(Go);//开票一个新的线程
       
            t.Start();//运行go
            t.Join();
            Console.WriteLine("Thread t hsa ended!");

        

            Console.ReadKey();
        }

        static void Go()
        {
            Console.WriteLine(Thread.CurrentThread.Name);
            for (int i = 0; i < 1000; i++)
            {
                Console.Write("Y");
            }
        }

    }


在这里插入图片描述
在继续执行标准的 COM 和 SendMessage 消息泵处理期间,阻止调用线程,直到由该实例表示的线程终止。
大概意思就是t线程运行完后才会执行后面的代码。
在这里插入图片描述

 if (t.Join(2000))
                Console.WriteLine("t has termminated.");
            else
                Console.WriteLine("The timeOut has elapsed and Thread1 will resume");

大概意思就是2秒后如果t还没执行完对应的方法体,就表示超时了,就执行else。否则相反。
在这里插入图片描述

 Thread.Sleep(2000);

在这里插入图片描述大概意思就是如果设置为0,当前线程将不在执行了,将会执行其他线程。
在这里插入图片描述
这个方法和上面的基本相同,都是放弃当前线程的执行。
在这里插入图片描述
在这里插入图片描述

3. 阻塞Blocking

在这里插入图片描述
不懂按位或按位与的点这里学习一下。
在这里插入图片描述
状态如下:
在这里插入图片描述
State状态图:
在这里插入图片描述
在这里插入图片描述
代码如下:

  public static ThreadState SimpleThreadState(ThreadState ts)
        {
            return ts & (ThreadState.Unstarted | ThreadState.WaitSleepJoin | ThreadState.Stopped);
        }

在这里插入图片描述
一下不懂按位或和按位与的给你们解释一下上面代码执行的逻辑。
ThreadState.Unstarted对应的上面状态码为8对应的二进制是00001000
ThreadState.StopRequested对应的状态码为1对应二进制为00000001
当Unstarted传入SimpleThreadState函数后,ts参数会与(ThreadState.Unstarted | ThreadState.WaitSleepJoin | ThreadState.Stopped)里面任意满足条件的进行按位与的操作,并都将转换对应的二进制,
00001000&00001000后还是00001000对应的十进制是8所以返回的是Unstarted
而00000001和函数里面的值进行按位与后还是00000000对应的10进制是0 所以 返回Running

在这里插入图片描述
在这里插入图片描述
阻塞解除相当于上下文切换
在这里插入图片描述
白话解释大概就是
I/O-bound相当于干等着,什么事都不干。
Compute-bound相当于一直占着CPU特别忙。
在这里插入图片描述
在这里插入图片描述

4. 什么是线程安全

在这里插入图片描述
本地独立的例子如下:在这里插入图片描述大致意思就是2个线程独立调用了GO方法同时创建了2个cycles变量直译过来就是局部变量。
共享状态例子如下:
在这里插入图片描述
大概意思就是2个线程用了同一个实例化对象,所以共用了_done这个变量。

lambda例子和静态例子如下:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
线程锁例子如下:

 public class ThreadSafe
    {
        static bool _done;
        static readonly object _locker = new object();

      public  static void Main1()
        {
            new Thread(go).Start();
            go();
        }
        static void go()
        {
            lock (_locker) //加锁:每次只能一个线程进入
            {
                if (!_done)
                {
                    Console.WriteLine("Done");
                    _done = true;
                }
            }
        }
    }

5. 向线程传递数据&异常处理

在这里插入图片描述
例子:
在这里插入图片描述
在这里插入图片描述

例子:
在这里插入图片描述
在这里插入图片描述

例子:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
例子:
在这里插入图片描述
结果:
在这里插入图片描述
解决方案:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
主线程是无法捕获到新建线程的异常的。

解决:
在这里插入图片描述
捕获放在新线程执行的方法里面就没问题了。
在这里插入图片描述
人话解释:就是在主线程上(就是UI层)有未处理的异常,上面那2个异常处理事件就会被触发。
在这里插入图片描述

6. 前台线程VS后台线程

在这里插入图片描述

在这里插入图片描述
例子:
在这里插入图片描述
解释:默认情况下我们创建的线程都是前台线程,在不传参数的情况下去执行,worker线程会一直等待着输入,因为是前台线程。
传入参数后,执行worker.IsBackground = true;把worker线程设置为后台线程。当语句执行完后,程序就终止了,因为唯一的一个前台线程也设置为了后台线程,一旦所有前台线程都停止了那么应用程序也停止了。

在这里插入图片描述

7.线程优先级

在这里插入图片描述
在这里插入图片描述

8.信号简介

在这里插入图片描述
例子:

 var signal = new ManualResetEvent(false); //定义信号
        new Thread(() =>
        {
            Console.WriteLine("等待信号。。。");
            signal.WaitOne();//阻塞线程   收到打开信号的命令后才会执行后面代码
            signal.Dispose();
            Console.WriteLine("收到信号!");
        }).Start();

        Thread.Sleep(3000);
        signal.Set();  //打开信号
        Console.ReadKey();
    }`

在这里插入图片描述

9.富客户端应用处理耗时操作的一种办法

在这里插入图片描述
在这里插入图片描述

例子:

 public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        private void Button_Click(object sender, RoutedEventArgs e)
        {
            Work();
        }

        void Work()
        {
            Thread.Sleep(5000);

            this.textBox.Text = "The answer";

        }
    }

在这里插入图片描述
当我们点击开始的时候,主线程调用work()方法,并且睡五秒,此时,UI线程处于假死状态,什么也做不了。只能等5秒后,work方法工作结束ui线程才开继续工作。

另外子线程不能修改主线程UI上的元素值 例子:

  public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        private void Button_Click(object sender, RoutedEventArgs e)
        {
            new Thread(Work).Start();
        }

        void Work()
        {
            Thread.Sleep(5000);

            this.textBox.Text = "The answer";

        }
    }

在这里插入图片描述
点击开始后将会报错。报错意思就是其他线程不能更新主线程上的UI操作。

解决办法:

 public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        private void Button_Click(object sender, RoutedEventArgs e)
        {
            new Thread(Work).Start();
        }

        void Work()
        {
            Thread.Sleep(5000);

            UpdateMessage( "The answer");

        }

        void UpdateMessage(string message)
        {
            Action action = () => textBox.Text = message;
            Dispatcher.BeginInvoke(action);
        }
    }

BeginInvoke这个方法相当于把这个action委托排队发送到UI线程的消息队列里,而UI线程里的消息队列,就是处理一些键盘鼠标事件以及定时事件的消息队列,而委托里面就是更新UI的逻辑,所以这样写可以正常更新UI。
在这里插入图片描述

10.Synchronization Context(同步上下文)

在这里插入图片描述
Marshaling的意思:举个栗子:我们经常要把数据从一个地方移动到另一个地方,比如从一个网站移动到另一个网站,A网站可能C#编写的,里面的数据可能都是C#类表示的,B网站是GO语言编写的他里面的数据可能就是struct表示的,所以说我们需要把C#里面编写的数据移动到GO语言里面,而这2个网站之间没有共享的内存,那么怎么把C#数据变为GO的数据呢,就需要转换一下,转换为可发送的格式,这个过程就叫做Marshaling而B网站要装换为C#这个过程就叫UNMarshaling百度翻译为:解编 其实就是序列化和反序列化。

而Thread Marshaling意思:他就是把一些数据的所有权,从一个线程交给另外一个线程。

例子:

 public partial class MainWindow : Window
    {
        SynchronizationContext _uiSyncContext;

        public MainWindow()
        {
            InitializeComponent();
            //为当前UI线程捕获Synchronization Context
            _uiSyncContext = SynchronizationContext.Current;
            new Thread(Work).Start();
        }

       

        void Work()
        {
            Thread.Sleep(5000);

            UpdateMessage( "The answer");

        }

        void UpdateMessage(string message)
        {
            //把委托Marshal给UI线程
            _uiSyncContext.Post(_=>textBox.Text=message,null);
            //调用post相当于调用Dispather或者control上面的BeginInvoke方法
        }
    }

这和第九节解决方法一样,都可以解决子线程修改UI线程假死的状态。

11.线程池

在这里插入图片描述
短期操作可能意思就是运行时间还没线程启动时间长,,
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

12.开始一个Task

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
例子:

static void Main(string[] args)
        {

            Task.Run(() => { Console.WriteLine("Foo"); });

        }

在这里插入图片描述
上面那行代码执行后什么也不会打印,因为Task默认使用线程池,也就是后台线程,前面我们说过,当主线程里面的线程都设为后台线程后,当主线程跑完后,就会停止应用程序,不管后台线程任务有没有完成。所以RUN里面的委托已经被设置为后台线程了,等后台线程反应过来时候,主线程已经执行完了,所以就什么也不会打印。

可以加一句如下代码:
在这里插入图片描述
这样就达到了阻塞线程的状态,控制台就会打印Foo;
在这里插入图片描述
这里的热任务(hot task)意思就是相当于创建完,就立即执行。
在这里插入图片描述
在这里插入图片描述
例子:

 static void Main(string[] args)
        {

            Task task = Task.Run(()=> {
                Thread.Sleep(3000);
                WriteLine("Foo");
            });
            WriteLine(task.IsCompleted);  //false
            task.Wait(); //阻塞直至task完成操作
            WriteLine(task.IsCompleted); //True

            ReadLine();
          
         

        }

结果:
在这里插入图片描述
其中IsCompleted表示查看当前任务是否完成!
在这里插入图片描述
在这里插入图片描述
例子:
在这里插入图片描述
在这里插入图片描述
tasks表示的是多个任务。

13.Task的返回值

在这里插入图片描述
例子:

 class TaskDome
    {
       public static void M()
        {
           
            Task<int> task = Task.Run(()=> {
                Console.WriteLine("Foo");
                return 3;
            });
            int result = task.Result;  //如果task没完成,那么就阻塞
            Console.WriteLine(result); //3
        }
    }

这里有些人不知道为什么Lambda表达式里面为什么可以返回3,其实我们可以F12查看,会发现里面的委托是一个Func的委托,也就是无参,有一个T返回值的委托。
不懂Lambda演变的看这里。

再看一个例子:

  public static void M1()
        {

            Task<int> PrintNumberTask = Task.Run(() => 
                Enumerable.Range(2, 3000000).Count(n => Enumerable.Range(2, (int)Math.Sqrt(n) - 1).All(i => n % i > 0))
            //这句话意思是生成2-300000的整数。然后用Count统计一下这些数字符合Count条件的数,条件的意思就是在原有数目的基础上,再生成2-300000平方根的数-1,然后用2-3000000里的每个数去除2-300000平方根的每个数,如果余数大于0 count就+1;
            

            );
            Console.WriteLine("Task running....");
            Console.WriteLine("The answer is "+PrintNumberTask.Result);//会等待线程池里面的委托方法执行完成,阻塞当前线程,直到拿到结果。
         
        }

在这里插入图片描述

14.Task的异常

在这里插入图片描述
例子:

 Task task = Task.Run(()=> { throw null; });
            try
            {
                task.Wait();  //task抛出的异常在这接受
            }
            catch (AggregateException aex)
            {
                //判断是否为null异常
                if (aex.InnerException is NullReferenceException)
                {
                    Console.WriteLine("Null");
                }
                else
                {
                    throw;
                }

结果打印Null
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

15.Continuation

在这里插入图片描述
例子:


            Task<int> PrimeNumberTask = Task.Run(()=> Enumerable.Range(2,3000000).Count(n=>Enumerable.Range(2,(int)Math.Sqrt(n)-1).All(i=>n%i>0)));  //列出所有的质数
            var awaiter = PrimeNumberTask.GetAwaiter();//GetAwaiter获取用于等待此 Task<TResult> 的 awaiter。


            //OnCompleted将操作设置为当 TaskAwaiter<TResult> 对象停止等待异步任务完成时执行。  也就是PrimeNumberTask 执行完成后才会执行OnCompleted
            awaiter.OnCompleted(() =>
            {
                int result = awaiter.GetResult();//结束异步任务完成的等待。获得已完成任务的结果。
                Console.WriteLine(result); //writes result

            });

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
例子:

 Task<int> PrimeNumberTask = Task.Run(()=> Enumerable.Range(2,3000000).Count(n=>Enumerable.Range(2,(int)Math.Sqrt(n)-1).All(i=>n%i>0)));  //列出所有的质数
            var awaiter = PrimeNumberTask.ConfigureAwait(false).GetAwaiter(); //GetAwaiter获取用于等待此 Task<TResult> 的 awaiter。


            //OnCompleted将操作设置为当 TaskAwaiter<TResult> 对象停止等待异步任务完成时执行。  
            awaiter.OnCompleted(() =>
            {
                int result = awaiter.GetResult();//结束异步任务完成的等待。获得已完成任务的结果。
                Console.WriteLine(result); //writes result

            });

加了.ConfigureAwait(false)后就不会把返回的结果更新到UI线程上。
在这里插入图片描述
在这里插入图片描述
例子:

 Task<int> PrimeNumberTask = Task.Run(() => Enumerable.Range(2, 3000000).Count(n => Enumerable.Range(2, (int)Math.Sqrt(n) - 1).All(i => n % i > 0)));  //列出所有的质数
            var awaiter = PrimeNumberTask.ConfigureAwait(false).GetAwaiter(); //GetAwaiter获取用于等待此 Task<TResult> 的 awaiter。


            PrimeNumberTask.ContinueWith(task=> {
                int result = task.Result;
                Console.WriteLine(result);
            });

ContinueWith和OnCompleted是一样的作用,都是task结束后的回调,或者说延迟执行。
在这里插入图片描述

16.TaskCompletionSource

在这里插入图片描述
在这里插入图片描述
例子:

 var tcs = new TaskCompletionSource<int>();
            new Thread(() =>
            {
                Thread.Sleep(5000);
                tcs.SetResult(42);
            })
            {
                IsBackground = true
            }.Start();
            Task<int> task = tcs.Task;
            Console.WriteLine(task.Result);  //结果 42

例子二:

 public static void M1()
        {
            Task<int> task = Run(()=> {
                Thread.Sleep(5000);
                return 42;
            });

            Console.WriteLine(task.Result);

        }



        static Task<TResult> Run<TResult>(Func<TResult> function)
        {
            var tcs = new TaskCompletionSource<TResult>();
            new Thread(() =>
            {

                try
                {
                    tcs.SetResult(function());
                }
                catch (System.Exception ex)
                {

                    tcs.SetException(ex);
                }

            }).Start();
            return tcs.Task;
        }

这2个例子结果都是42第二个例子只是自定义了一下Run方法。例子二解释如下图:在这里插入图片描述
在这里插入图片描述

  public static void M3()
        {
            var awaiter = GetAnswerTolife().GetAwaiter();//等待Task执行完成   获取用于等待此 Task<TResult> 的 awaiter。
            awaiter.OnCompleted(()=> {
                //OnCompleted  task执行完成结束后的回调  延时执行
                Console.WriteLine(awaiter.GetResult()); //GetResult获取task执行结果
            });
        }


        static Task<int> GetAnswerTolife()
        {
            var tcs = new TaskCompletionSource<int>();
            //实例化一个计时器
            var timer = new System.Timers.Timer(5000) {
                AutoReset = false //自动重置   false表示只触发一次
            };
            //达到指定时间间隔后触发Elapsed事件执行后面的委托
            timer.Elapsed += delegate
            {
                timer.Dispose();//释放资源
                tcs.SetResult(42);//给Task写入结果。
            };
            timer.Start(); //开始执行计时器
            return tcs.Task;
        }

对上面方法进行封装:

 public static void M4()
        {
            //5秒之后  continuatuon 开始的时候,才占用线程   因为之前都是后台线程,OnCompleted后进行了阻塞
            Delay(5000).GetAwaiter().OnCompleted(()=> {
                Console.WriteLine(42);
            });
        }
        //注意: 没有非泛型版本的TaskCompletionSource
        static Task Delay(int milliseconds)
        {
            var tcs = new TaskCompletionSource<object>();
            var timer = new System.Timers.Timer(milliseconds)
            {
                AutoReset = false
            };
            timer.Elapsed += delegate {
                timer.Dispose();
                tcs.SetResult(null);
            };
            timer.Start();
            return tcs.Task;
        }

看下C#自带的在这里插入图片描述
例子:
在这里插入图片描述

17.同步和异步

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
这个调用图的意思就是一个函数接着一个函数调用。
在这里插入图片描述
在这里插入图片描述

18.异步和 continuation 以及语言的支持

在这里插入图片描述
先看同步执行的结果:

 DisplaPrmeCounts();
  static void DisplaPrmeCounts()
        {
            for (int i = 0; i < 10; i++)
            {
                Console.WriteLine(GetPrimesCount(i * 1000000 + 2, 1000000) + "primes between" + (i * 1000000) + "and" + ((i + 1) * 1000000 - 1));
                Console.WriteLine("Done!");
            }
 static int GetPrimesCount(int start, int count)
        {
            return ParallelEnumerable.Range(start, count).Count(n => Enumerable.Range(2, (int)Math.Sqrt(n) - 1).All(i => n % i > 0));
        }

        }

结果:
在这里插入图片描述
同步执行,我们可以看到,数据都是从大到小排列。因为都是一个线程排列同步执行的。

现在看异步执行。并行执行的例子:

 static void DisplaPrmeCounts()
        {

            for (int i = 0; i < 10; i++)
            {
                var awaiter = GetPrimesCountAsync(i * 1000000 + 2, 1000000).GetAwaiter();
                awaiter.OnCompleted(()=>
                Console.WriteLine(awaiter.GetResult()+"primes betwee...")
                );
        
            }



        }

        static Task<int> GetPrimesCountAsync(int start, int count)
        {
            return Task.Run(()=> ParallelEnumerable.Range(start, count).Count(n => Enumerable.Range(2, (int)Math.Sqrt(n) - 1).All(i => n % i > 0)));
        }

结果:
在这里插入图片描述
结果看上去很乱,这是因为线程池中的线程相当于分配了10个线程给他们,然后CUP的时间片相当于同步并行的执行了这些方法。这种并行执行效率高了,但是顺序很乱,如何让它顺序不乱了 看下面

在这里插入图片描述

调整后 的代码:

 DisplayPrimeCountsFrom(0);
   static void DisplayPrimeCountsFrom(int i)
        {
            var awaiter = GetPrimesCountAsync(i * 1000000 + 2, 1000000).GetAwaiter();
            awaiter.OnCompleted(()=> {
                Console.WriteLine(awaiter.GetResult()+"primes between ...");

                if (++i < 10)
                {
                    DisplayPrimeCountsFrom(i);
                }
                else
                {
                    Console.WriteLine("Done!");
                }

            });
        }

        static Task<int> GetPrimesCountAsync(int start, int count)
        {
            return Task.Run(()=> ParallelEnumerable.Range(start, count).Count(n => Enumerable.Range(2, (int)Math.Sqrt(n) - 1).All(i => n % i > 0)));
        }

这样改了后其实并不是异步的,而是同步的,因为采用的递归方式,如何改成异步的呢,看下面代码:

 public static async void M()
        {
        
            await  DisplaPrmeCountsAsync();
      

        }

        async static  Task DisplaPrmeCountsAsync()
        {
            for (int i = 0; i < 10; i++)
            {
                Console.WriteLine(await GetPrimesCountAsync(i * 1000000 + 2, 1000000) + "primes between" + (i * 1000000) + "and" + ((i + 1) * 1000000 - 1));
                Console.WriteLine("Done!");
            }


      public  static Task<int> GetPrimesCountAsync(int start, int count)
        {
            return Task.Run(()=> ParallelEnumerable.Range(start, count).Count(n => Enumerable.Range(2, (int)Math.Sqrt(n) - 1).All(i => n % i > 0)));
        }

在这里插入图片描述

19.await

在这里插入图片描述
在这里插入图片描述

例子:
在这里插入图片描述
await 接受的返回值必须是Task 或者Task不能接void需要await的函数体里面必须加上async关键字,不然编译时候会报错。

例子:

在这里插入图片描述
在这里插入图片描述
这2种写法是等效的。但第二种简介了不少,可以说第二种是第一种的简写吧。第二种写法,当运行时走到await表达式后,它会加一个continuation等待执行结果,又因为是异步函数,所以主线程也不存在卡死的情况,当await后面任务完成后,运行时会回到之前停止的continuation,然后完成打印操作。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
比如循环体里的i 每次循环到await这里后 他都会对每次的i进行捕获。
在这里插入图片描述
在这里插入图片描述
例子:
同步:
在这里插入图片描述

这种同步的执行后,由于UI线程需要进行计算,所以会有个假死的状态。

改成异步:
在这里插入图片描述
下载网络内容的例子:
在这里插入图片描述
这个代码的意思 就是当你点击下载的时候, 遇到await后,代码会和同步执行一样,接着走后面的代码,比如把foreach运行完,只不过await后面的表达式内容,长时间执行的任务,相当于被暂停了,编译器会自己给它那里加一个continuation等表达式里面的任务运行完后,再返回到停止的地方,把没执行完的代码执行完,这个没执行完的代码就好比向UI界面输入返回的结果。
在这里插入图片描述
粗粒度并发的例子:
在这里插入图片描述

在这里插入图片描述

20.编写异步函数

在这里插入图片描述

例子:
在这里插入图片描述
返回值为Viod的话,调用函数不能使用await关键字,只有Task才可以使用。
在这里插入图片描述
比如上面方法体并没有return关键字。
在这里插入图片描述
异步方法之间的调用就是调用链。
在这里插入图片描述

编译器进行扩展的代码大致如下:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
例子:
在这里插入图片描述
在这里插入图片描述
例子:
在这里插入图片描述
如果不加await 则返回的函数值就是Task 加了之后 就是int类型返回值。
在这里插入图片描述
可以和同步代码进行比较:
在这里插入图片描述
基本上差不多,只不过异步加了async/await;

在这里插入图片描述
需要自己手动创建Task的情况:
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

21.异步中的同步上下文

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

22.优化同步完成

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
下面这种不会引发警告。
在这里插入图片描述
在这里插入图片描述

23.ValueTask

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

  • 6
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值