文档章节

C#:异步编程和线程的使用(.NET 4.5 )

葡萄城控件技术团队
 葡萄城控件技术团队
发布于 2015/06/09 11:26
字数 3519
阅读 1821
收藏 8
点赞 0
评论 1

异步编程和线程处理是并发或并行编程非常重要的功能特征。为了实现异步编程,可使用线程也可以不用。将异步与线程同时讲,将有助于我们更好的理解它们的特征。

本文中涉及关键知识点

1. 异步编程

2. 线程的使用

3. 基于任务的异步模式

4. 并行编程

5. 总结

  • 异步编程

什么是异步操作?异步操作是指某些操作能够独立运行,不依赖主流程或主其他处理流程。通常情况下,C#程序从Main方法开始,当Main方法返回时结束。所有的操作都是按顺序执行的。执行操作是有序列的,一个操作必须等到其前面的操作完成才能够执行。如以下代码示例:

   1:  static void Main(string[] args)
   2:
   3:  {
   4:
   5:  DoTaskOne();
   6:
   7:  DoTaskTwo();
   8:
   9:  }

“DoTaskOne”方法结束后,DoTaskTwo()才能够执行。

异步编程中常用后台运行的方法体现,主调用线程不会被阻塞。调用后台运行的方法后,执行流程会立即返回到调用的线程并继续执行其他任务。后台运行方法通常是用线程或任务来实现。

在上面的例子中,在“DoTaskOne”方法调用成功后,如果“DoTaskOne”是异步调用,,执行流程立即返回到Main方法中,并继续执行“DoTaskTwo” 方法。

C#提供了Thread类创建线程实现异步编程,或者使用.NET提供的异步模式实现异步编程。.NET中提供了三种不同的异步模式:

1. 异步编程模型(APM)模式

2. 基于事件的异步模式(EAP)

3. 基于任务的异步模式(TAP)

前两种模型微软官方并不推荐使用,本文不再详细描述。我们将详细讨论基于任务的异步模式(TAP):

  • 线程的使用

在.NET 4.5中引入了异步编程模式,大部分情况下都不需要我们手动创建线程。编译器已经替代了开发人员来完成这项工作。

创建新线程是非常耗时的。一般情况下,异步和并行编程使用 “基于任务的异步模式(TAP)”和“任务并行库(TPL)”就够了。如果需要控制线程的功能则需要使用其他模式。

TAP和TPL都是基于任务。一般来说任务是从线程池中调用线程( 线程池.NET框架创建的和维护的线程集。如果我们使用任务,就不需要直接调用线程池。

任务可以在以下情况运行:

1. 在正在运行的线程中

2. 在新线程中

3. 从线程池中的某一线程中

4. 没有线程也可以运行

如果使用任务机制,开发人员就不必担心线程的创建或使用,.NET框架已经为我们解决了这一难题。

有时候需要控制线程,执行以下操作:

1. 设置线程名称

2. 设置线程优先级

3. 设置线程是前端或后端运行

我们可以使用线程类来创建线程。

使用Thread类创建线程

Thread类的构造函数接收委托类型的参数

1. ThreadStart:定义了返回值为空的方法,且不带参数的方法。

2. ParameterizedThreadStart:定义了返回值为空且有一个object类型的参数。

下面是一个简单的例子,使用 Start方法启动一个新线程:

   1:  static void Main(string[] args)
   2:
   3:  {
   4:
   5:  Thread thread = new Thread(DoTask);
   6:
   7:  thread.Start();// Start DoTask method in a new thread
   8:
   9:  //Do other tasks in main thread
  10:
  11:  }
  12:
  13:  static public void DoTask() {
  14:
  15:  //do something in a new thread
  16:
  17:  }

可以用Lamda表达式代替线程名称:

   1:  static void Main(string[] args)
   2:
   3:  {
   4:
   5:  Thread thread = new Thread(() => {
   6:
   7:  //do something in a new thread
   8:
   9:  });
  10:
  11:  thread.Start();// Start a new thread
  12:
  13:  //Do other tasks in main thread
  14:
  15:  }

如果不需要引用变量,可如下直接启动线程:

   1:  static void Main(string[] args)
   2:
   3:  {
   4:
   5:  new Thread(() => {
   6:
   7:  //do something in a new thread
   8:
   9:  }).Start();// Start a new thread
  10:
  11:  //Do other tasks in main thread
  12:
  13:  }

但是,如果想控制线程对象,对线程设置一些属性,需要在线程创建后引用线程变量。如下可给线程对象的不同属性设值:

   1:  static void Main(string[] args)
   2:
   3:  {
   4:
   5:  Thread thread = new Thread(DoTask);
   6:
   7:  thread.Name = "My new thread";// Asigning name to the thread
   8:
   9:  thread.IsBackground = false;// Made the thread forground
  10:
  11:  thread.Priority = ThreadPriority.AboveNormal;// Setting thread priority
  12:
  13:  thread.Start();// Start DoTask method in a new thread
  14:
  15:  //Do other task in main thread
  16:
  17:  }

调用引用变量,可以执行一些操作如中止线程或通过调用join方法等待阻塞线程。

如果需要通过函数传值,可以给Start方法传值。由于该方法的参数为Object类型,因此需要强制转换类型。

   1:  static void Main(string[] args)
   2:
   3:  {
   4:
   5:  Thread thread = new Thread(DoTaskWithParm);
   6:
   7:  thread.Start("Passing string");// Start DoTaskWithParm method in a new thread
   8:
   9:  //Do other task in main thread
  10:
  11:  }
  12:
  13:  static public void DoTaskWithParm(object data)
  14:
  15:  {
  16:
  17:  //we need to cast the data to appropriate object
  18:
  19:  }

“async”和“await”关键字

.NET框架引入了两个新的关键字来实现异步编程:“async”和“await”。使用 “await”的异步方法必须由“async”修饰符来声明方法。“await”关键字修饰调用异步方法。await 运算符应用于一个异步方法中的任务以挂起该方法的执行,直到等待任务完成.如下: 

   1:  private async static void CallerWithAsync()// async modifier is used
   2:
   3:  {
   4:
   5:  string result = await GetSomethingAsync();// await is used before a method call. It suspends
   6:     //execution of CallerWithAsync() method and control returs to the calling thread that can
          //perform other task.
   7:
   8:  Console.WriteLine(result);
   9:     // this line would not be executed before GetSomethingAsync() //method completes
  10:
  11:  }

而“ async ”修饰符只能用于返回值为Task类型或Void的方法。它不能用于主程序的切入点。

所有的方法之前不能使用await关键字,使用“await”关键字方法必须返回 “可等待”类型。以下属于“可等待”类型:

1. Task

2. Task<T>

3. 自定义“可等待”类型。

  • 基于任务的异步模式

首先我们需要声明一个返回类型为Task或Task<T>的异步方法。可以通过以下几种方式创建任务:

1. Task.Factory.StartNew方法:在之前的.NET版本(在.NET 4中),是创建和启动任务的主要方法。

2. Task.Run或Task.Run <T>方法:从.NET 4.5这个方法已经被使用。此方法足以满足常见情况。

3. Task.FromResult方法:如果结果是已计算,就可以用这个方法来创建任务。

创建并等待一个任务

使用Task.Run <T>方法创建Task。该方法将特定工作按顺序排列在线程池中运行,并返回工作的任务句柄。需要以下步骤从同步方法中创建异步任务:

1. 假设下面方法是同步的,但需要一定的时间来完成:

   1:  static string Greeting(string name)
   2:
   3:  {
   4:
   5:  Thread.Sleep(3000);
   6:
   7:  return string.Format("Hello, {0}", name);
   8:
   9:  }

2. 要以异步方式访问此方法,必须以异步方式封装。命名为“GreetingAsync”。增加“Async”的后缀命名异步方法。

 

   1:  static Task<string> GreetingAsync(string name)
   2:
   3:  {
   4:
   5:  return Task.Run<string>(() =>
   6:
   7:  {
   8:
   9:  return Greeting(name);
  10:
  11:  });
  12:
  13:  }

3.现在,可通过使用的await关键字调用异步方法GreetingAsync

 

   1:  private async static void CallWithAsync()
   2:
   3:  {
   4:
   5:  //some other tasks
   6:
   7:  string result = await GreetingAsync("Bulbul");
   8:
   9:  //We can add multiple “await” in same “async” method
  10:
  11:  //string result1 = await GreetingAsync(“Ahmed”);
  12:
  13:  //string result2 = await GreetingAsync(“Every Body”);
  14:
  15:  Console.WriteLine(result);
  16:
  17:  }

当“CallWithAsync”方法被调用时,与常规的同步方法一样执行,直到遇到“await”的关键字。当它执行到 await的关键字会处理执行,并开始等待“GreetingAsync(” Bulbul “)” 方法被完成。同时,程序流将返回” CallWithAsync “方法的调用者,并继续执行调用者的任务。

当“GreetingAsync(" Bulbul ") 方法完成,“CallWithAsync”的方法恢复 “await关键字后的其他任务。在本实例中,将继续执行的代码“Console.WriteLine(result)”

4. 使用任务持续:Task类 “ContinueWith”的方法定义了Task完成后被调用的代码。

   1:  private static void CallWithContinuationTask()
   2:
   3:  {
   4:
   5:  Task<string> t1 = GreetingAsync("Bulbul");
   6:
   7:  t1.ContinueWith(t =>
   8:
   9:  {
  10:
  11:  string result = t.Result;
  12:
  13:  Console.WriteLine(result);
  14:
  15:  });
  16:
  17:  }

如果使用“ContinueWith”的方法就不需要使用“await“关键字,编译器会自动在合适的位置中添加“await”关键字。

等候多个异步方法。

看看下面的代码:

   1:  private async static void CallWithAsync()
   2:
   3:  {
   4:
   5:  string result = await GreetingAsync("Bulbul");
   6:
   7:  string result1 = await GreetingAsync(&ldquo;Ahmed&rdquo;);
   8:
   9:  Console.WriteLine(result);
  10:
  11:  Console.WriteLine(result1);
  12:
  13:  }

有两个正在等待调用函数序列。“GreetingAsync(” Ahmed “)” 会在完成第一个呼叫“GreetingAsync(” Bulbul “)” 之后启动。如果“result”和上面的代码“result1”是独立的,那么连续的“awiating”并不是一个好的做法。

在这种情况下,我们可以简化调用方法,不需要添加多个“await”关键字,只在一个地方添加await关键字,如下所示,这种情况下,该方法的调用都可以并行执行。

   1:  private async static void MultipleAsyncMethodsWithCombinators()
   2:
   3:  {
   4:
   5:  Task<string> t1 = GreetingAsync("Bulbul");
   6:
   7:  Task<string> t2 = GreetingAsync("Ahmed");
   8:
   9:  await Task.WhenAll(t1, t2);
  10:
  11:  Console.WriteLine("Finished both methods.\n " +
  12:
  13:  "Result 1: {0}\n Result 2: {1}", t1.Result, t2.Result);
  14:
  15:  }

在这里,我们使用Task.WhenAll连接器。Task.WhenAll创建一个任务,将完成所有的提供的任务。Task类也有其他的结合器。Task.WhenAny,当所任务链中所有的任务完成时,结束使用。

处理异常

必须把“await的代码块放在try块内捕获异常。

   1:  private async static void CallWithAsync()
   2:
   3:  {
   4:
   5:  try
   6:
   7:  {
   8:
   9:  string result = await GreetingAsync("Bulbul");
  10:
  11:  }
  12:
  13:  catch (Exception ex)
  14:
  15:  {
  16:
  17:  Console.WriteLine(&ldquo;handled {0}&rdquo;, ex.Message);
  18:
  19:  }
  20:
  21:  }

如果try块中有多个“await”,只有第一个” await“异常会被处理,其他“await”将无法被捕捉。如果希望所有的方法都能捕获异常,不能使用“await”关键字调用方法,使用Task.WhenAll来执行任务。

   1:  private async static void CallWithAsync()
   2:
   3:  {
   4:
   5:  try
   6:
   7:  {
   8:
   9:  Task<string> t1 = GreetingAsync("Bulbul");
  10:
  11:  Task<string> t2 = GreetingAsync("Ahmed");
  12:
  13:  await Task.WhenAll(t1, t2);
  14:
  15:  }
  16:
  17:  catch (Exception ex)
  18:
  19:  {
  20:
  21:  Console.WriteLine(&ldquo;handled {0}&rdquo;, ex.Message);
  22:
  23:  }
  24:
  25:  }

捕获所有任务的错误一种方法是在try块之外声明任务,这样可以从try块进行访问,并检查任务的“IsFaulted”属性。如果它存在异常那么“IsFaulted”属性值为True,就可捕获任务实例的内部异常。

还有另一个更好的办法:

   1:  static async void ShowAggregatedException()
   2:
   3:  {
   4:
   5:  Task taskResult = null;
   6:
   7:  try
   8:
   9:  {
  10:  Task<string> t1 = GreetingAsync("Bulbul");
  11:
  12:  Task<string> t2 = GreetingAsync("Ahmed");
  13:
  14:  await (taskResult = Task.WhenAll(t1, t2));
  15:
  16:  }
  17:
  18:  catch (Exception ex)
  19:
  20:  {
  21:
  22:  Console.WriteLine("handled {0}", ex.Message);
  23:
  24:  foreach (var innerEx in taskResult.Exception.InnerExceptions)
  25:
  26:  {
  27:  Console.WriteLine("inner exception {0}", nnerEx.Message); }
  28:  }
  29:
  30:  }

取消任务

在此之前,如果从线程池中调用线程,线程是不可能取消。现在,Task类提供了一个方法基于CancellationTokenSource类能够取消已启动的任务,取消任务步骤:

1. 异步方法应该除外 “ CancellationToken” 参数类型

2. 创建CancellationTokenSource类实例:

    var cts =new CancellationTokenSource();

3. 传递CancellationToken,如:

   1:    Task<string> t1 = GreetingAsync("Bulbul", cts.Token);

4. 长时间运行的方法中,必须调用CancellationToken ThrowIfCancellationRequested()方法

 

   1:   static string Greeting(string name, CancellationToken token){
   2:
   3:  Thread.Sleep(3000);
   4:
   5:  token. ThrowIfCancellationRequested();
   6:
   7:  return string.Format("Hello, {0}", name);
   8:
}

5. 从等待的Task中捕获 OperationCanceledException异常。

 6. 如果通过调用CancellationTokenSource的实例的方法执行取消操作,将从长时间运行操作中抛出OperationCanceledException异常。也可以设置取消的时间。以下是完整的代码,一秒后执行取消操作:

   1:   static void Main(string[] args)
   2:
   3:   {
   4:   CallWithAsync();
   5:
   6:  Console.ReadKey();
   7:
   8:   }
   9:
  10:
  11:  async static void CallWithAsync()
  12:
  13:  {
  14:
  15:  try
  16:
  17:   {
  18:
  19:  CancellationTokenSource source = new CancellationTokenSource();
  20:
  21:  source.CancelAfter(TimeSpan.FromSeconds(1));
  22:
  23:   var t1 = await GreetingAsync("Bulbul", source.Token);
  24:   }
  25:
  26:   catch (OperationCanceledException ex)
  27:
  28:  {
  29:
  30:   Console.WriteLine(ex.Message);
  31:
  32:   }
  33:
  34:   }
  35:
  36:  static Task<string> GreetingAsync(string name, CancellationToken token)
  37:
  38:   {
  39:
  40:   return Task.Run<string>(() =>
  41:
  42:   {
  43:
  44:   return Greeting(name, token);
  45:
  46:   });
  47:  }
  48:
  49:
  50:   static string Greeting(string name, CancellationToken token)
  51:
  52:   {
  53:
  54:  Thread.Sleep(3000);
  55:   token.ThrowIfCancellationRequested();
  56:
  57:   return string.Format("Hello, {0}", name);
  58:
  59:   }
  60:
  • 并行编程

.NET 4.5及以上版本推出“Parallel类,是线程类的抽象。使用“Parallel”类,我们可以实现并行。并行与线程不同,它使用所有可用的CPU或内核的。以下两种类型的并行是可行:

  1. 数据并行:如果我们有数据的大集合,我们希望在每个数据的某些操作进行并行使用,那么就可以使用数据并行。Parallel类有静态For或ForEach来执行数据并行行,如

   1:  ParallelLoopResult result =
   2:                      Parallel.For(0, 100, async (int i) =>
   3:                      {
   4:                          Console.WriteLine("{0}, task: {1}, thread: {2}", i,
   5:                          Task.CurrentId, Thread.CurrentThread.ManagedThreadId);
   6:                          await Task.Delay(10);
   7:
   8:                });
      

For或ForEach方法可以在多线程中和且索引无序可以是无序的。

如果想停止并行For或ForEach方法,可通过ParallelLoopState作为参数,并根据需要打破循环的状态,跳出循环。

   1:  ParallelLoopResult result =
   2:                      Parallel.For(0, 100, async (int i, ParallelLoopState pls) =>
   3:                      {
   4:                          Console.WriteLine("{0}, task: {1}, thread: {2}", i,
   5:                          Task.CurrentId, Thread.CurrentThread.ManagedThreadId);
   6:                          await Task.Delay(10);
   7:                          if (i > 5) pls.Break();
   8:                });

     2. 任务并行:如果想要同时运行多个任务的,我们可以通过调用Parallel类的invoke方法使用任务并行Parallel.Invoke方法接收委托行为的数组。例如:

   1:  static void ParallelInvoke()
   2:
   3:  {
   4:
   5:  Parallel.Invoke(MethodOne, MethodTwo);
   6:
   7:  }
   8:
  • 结论

本文详细介绍了.NET Framework 4.5提供的异步编程技术及细节。

原文链接:http://www.codeproject.com/Articles/996857/Asynchronous-programming-and-Threading-in-Csharp-N


© 著作权归作者所有

共有 人打赏支持
葡萄城控件技术团队

葡萄城控件技术团队

粉丝 321
博文 487
码字总数 694630
作品 13
西安
高级程序员
加载中

评论(1)

noonoo
noonoo
围观!!
C#网络编程系列文章(二)之Socket实现同步TCP服务器

原创性声明 本文作者:小竹zz 本文地址http://blog.csdn.net/zhujunxxxxx/article/details/44258719 转载请注明出处 文章系列目录 C#网络编程系列文章(一)之Socket实现异步TCP服务器 C#网络编...

zhujunxxxxx ⋅ 2015/03/14 ⋅ 0

.NET Framework 4.5 Beta 发布

Microsoft .NET Framework 4.5 Beta 是一个针对 .NET Framework 4 的高度兼容的就地更新。通过 将 .NET Framework 4.5 Beta 与 C# 或 Visual Basic 编程语言结合使用,您可以编 写 Windows ...

红薯 ⋅ 2012/03/06 ⋅ 9

C# WinForm开发系列 - Thread/Delegate/Event

C#是一门支持多线程的语言,因此线程的使用也是比较常见的。由于线程的知识在Win32编程的时候已经说得过多,所以在.Net中很少介绍这部分(可能.Net不觉得这部分是它所特有的)。那我们为什么...

长征2号 ⋅ 2017/10/11 ⋅ 0

充分发挥异步在 ASP.NET 中的强大优势

作者:Brij Bhushan Mishra 最近几年,异步编程受到极大关注,主要是出于两个关键原因:首先,它有助于提供更好的用户体验,因为不会阻塞 UI 线程,避免了处理结束前出现 UI 界面挂起。其次,...

OneAPM蓝海讯通 ⋅ 2016/04/13 ⋅ 0

Visual Studio 11开发指南(7)NET 4.5的改善

性能 对.NET Framework 4.5来说,在性能改进上下了很多功夫,无论是在改进现有代码性能方面,还是在编写代码方面都更快速,更灵活,可扩展性更强, 公共语言运行库(CLR)开发团队十分关注改...

junwong ⋅ 2012/03/09 ⋅ 0

C#~异步编程再续~async异步方法与同步方法的并行

今天晚上没事写了个测试的代码,又看了看.net的并行编程,两个方法,一个是异步async修饰的,另一个是普通的方法,在控制台程序的Main方法里去调用这两个方法,会有什么结果呢? 首先我们看一...

mcy247 ⋅ 2017/12/05 ⋅ 0

C#中多线程和异步

C#中异步和多线程的区别是什么呢?异步和多线程两者都可以达到避免调用线程阻塞的目的,从而提高软件的可响应性。甚至有些时候我们就认为异步和多线程是等同的概念。但是,异步和多线程还是有...

bigSoul ⋅ 2017/06/08 ⋅ 0

C#~异步编程再续~大叔所理解的并行编程(Task&Parallel)

并行这个概念出自.net4.5,它被封装在System.Threading.Tasks命名空间里,主要提供一些线程,异步的方法,或者说它是对之前Thread进行的二次封装,为的是让开发人员更方便的调用它,对于异步...

mcy247 ⋅ 2017/12/05 ⋅ 0

c#服务器在Linux上性能测试

http://git.oschina.net/liyonghelpme/GameServerCsharp 在Ubuntu 14.04上使用自己编译的Mono 4.2.3,运行.net framework 4.5 框架的c# 服务器程序。 工作性能正常,测试了游戏服务器每秒发送...

李勇2 ⋅ 2016/05/24 ⋅ 3

对协程的一些理解

协程 协程(coroutine)最早由Melvin Conway在1963年提出并实现,一句话定义:协程是用户态的轻量级的线程 线程和协程 线程和协程经常被放在一起比较;线程一旦被创建出来,编写者是无法决定什...

ksfzhaohui ⋅ 2016/12/09 ⋅ 16

没有更多内容

加载失败,请刷新页面

加载更多

下一页

对于程序员的招聘问题,作为软件人的一些吐槽和建议

作为软件人,找工作有时候似乎挺苦逼的。 说真的,让我去掉前面这句中“似乎”二字吧。就是苦逼!很多人都曾抱怨处在招聘的一方很糟糕——我们没有任何可靠的方式来甄别会写代码并且写得好的...

老道士 ⋅ 29分钟前 ⋅ 0

HDFS原理学习

一、概述 1、 Hadoop整合了众多的文件系统,首先提供了一个高层的文件系统抽象org.apache.hadoop.fs.FileSystem。然后有各个文件系统的实现类。 2、Hadoop是JAVA编写的,不同文件系统之间的交...

cjxcloud ⋅ 33分钟前 ⋅ 0

Linux下MySQL表名不区分大小写的设置方法(抄袭别人的)

Linux下MySQL表名不区分大小写的设置方法 MySQL表名不区分大小写的设置方法 在用centox安装mysql后,把项目的数据库移植了过去,发现一些表的数据查不到,排查了一下问题,最后发现是表名的大...

随风而浮沉 ⋅ 38分钟前 ⋅ 0

ubuntu下安装宋体simsun

sudo cp simsun.ttc /usr/share/fonts cd /usr/share/fonts sudo chmod 644 simsun.ttc 更新字体缓存: 代码: sudo mkfontscale 代码: sudo mkfontdir 代码: sudo fc-cache -fsv 安装chrome扩......

wangxuwei ⋅ 39分钟前 ⋅ 0

利用 ssh 传输文件

Linux 下一般可以用 scp 命令通过 ssh 传送文件: #把服务器上的 /home/user/a.txt 发送到本机的 /var/www/local_dir 目录下scp username@servername:/home/user/a.txt /var/www/local_dir...

大灰狼时间 ⋅ 49分钟前 ⋅ 0

web3j教程:android和java程序员如何使用web3j开发区块链以太坊

如何使用web3j为Java应用或Android App增加以太坊区块链支持,本教程内容即涉及以太坊中的核心概念,例如账户管理包括账户的创建、钱包创建、交易转账,交易与状态、智能合约开发与交互、过滤...

智能合约 ⋅ 今天 ⋅ 0

web3j开发java或android以太坊智能合约快速入门

web3j简介 web3j是一个轻量级、高度模块化、响应式、类型安全的Java和Android类库提供丰富API,用于处理以太坊智能合约及与以太坊网络上的客户端(节点)进行集成。 可以通过它进行以太坊区块链...

笔阁 ⋅ 今天 ⋅ 0

一起读书《深入浅出nodejs》-异步I/O

异步I/O “异步”这个名词其实很早就诞生了,但它大规模流行却是在Web 2.0浪潮中,它伴随着AJAX的第一个A(Asynchronous)席卷了Web。 为什么要异步I/O 关于异步I/O为何在Node里如此重要,这与...

小草先森 ⋅ 今天 ⋅ 0

JVM各种问题

1、如果启动什么都不设,会怎样? 先来看一个命令 [root@localhost bin]# java -XX:+PrintCommandLineFlags -version -XX:InitialHeapSize=29899008 -XX:MaxHeapSize=478384128 -XX:+PrintCo......

算法之名 ⋅ 今天 ⋅ 0

SAS笔记-宏2

宏是一种文本,一般来说其编译是在程序执行之前。 宏变量的创建 %let语句 %let macro_variables = text; %let是常见的宏变量建立方式,其编译就在执行前。如下例中,想要宏变量test等于数据集...

tonorth123 ⋅ 今天 ⋅ 0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

返回顶部
顶部