栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > .Net

异步多线程之入Task详解

.Net 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

异步多线程之入Task详解

上一篇:异步多线程之入ThreadPool
下一篇:异步多线程之Parallel


简介

1.0 时代的 Thread 提供了太多太底层功能,在 2.0 时代的 Threadpool 切去了一些无用与不可控的 API ,并提供了线程重用与线程数量限制的功能,3.0 时代的 Task 基于 Threadpool 又做了个补救,增加了多个实用的 API,满足了工作中使用场景。

启动多线程方式

Task 对于启动一个新的线程,提供的 API 还是比较丰富的 Run、New Task、Task Factory 都可以启动一个新的线程

Task.Run

例如:启用一个线程

public static void DoSomethingLong(string name)
{
    Console.WriteLine($"{name} DoSomethingLong Start,ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}");

    Console.WriteLine($"{name} DoSomethingLong End,ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}");
}

static void Main(string[] args)
{
    Console.WriteLine($"Main Start,ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}");

    Task.Run(() => DoSomethingLong("张三"));

    Console.WriteLine($"Main End,ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}");

    Console.ReadLine();
}

New Task

例如:启用一个线程

public static void DoSomethingLong(string name)
{
    Console.WriteLine($"{name} DoSomethingLong Start,ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}");

    Console.WriteLine($"{name} DoSomethingLong End,ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}");
}

static void Main(string[] args)
{
    Console.WriteLine($"Main Start,ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}");

    new Task(() => DoSomethingLong("张三")).Start();

    Console.WriteLine($"Main End,ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}");

    Console.ReadLine();
}

Task Factory

例如:启用一个线程

public static void DoSomethingLong(string name)
{
    Console.WriteLine($"{name} DoSomethingLong Start,ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}");

    Console.WriteLine($"{name} DoSomethingLong End,ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}");
}

static void Main(string[] args)
{
    Console.WriteLine($"Main Start,ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}");

    TaskFactory taskFactory = Task.Factory;
    taskFactory.StartNew(() => DoSomethingLong("张三"));

    Console.WriteLine($"Main End,ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}");

    Console.ReadLine();
}


API、应用场景

什么时候可以用多线程?不要说什么提高效率、异步啊,这都是技术名词没有用的。首先任务可以并发执行,即多任务可以分开,满足了条件后用了并发后再说提升速度、异步啊。

我们以项目经理与开发者们,完成项目开发为例,进行讲解多线程(主线程想象成项目经理,子线程想象成开发人员)。

当项目开始的时候,项目经理需要启动一个项目,做一些准备工作,然后开发人员开始编程

例如:定义 Coding 方法,提给给开发者开项目模块使用,Main 为项目经理角色

public static void Coding(string name,string module)
{
    Console.WriteLine($"{name} Coding Start {module},ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}");

    Console.WriteLine($"{name} Coding End  {module},ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}");
}

static void Main(string[] args)
{
    Console.WriteLine($"项目经理启动一个项目,ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}");
    Console.WriteLine($"前置的准备工作,ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}");
    Console.WriteLine($"开始编程,ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}");

    Task.Run(() => Coding("张三","Web"));
    Task.Run(() => Coding("李四", "Service"));
    Task.Run(() => Coding("王五", "SQL"));

    Console.ReadLine();
}

启动程序,项目经理(线程 1)做准备工作,开发者们(子线程 3、4、5)并发完成 Coding

WaitAll

例如:当项目开发完成,项目经理需要进行通知甲方验收对吧。所以我们在 Main 方法项目添加通知甲方验收

public static void Coding(string name,string module)
{
    Console.WriteLine($"{name} Coding Start {module},ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}");

    Console.WriteLine($"{name} Coding End  {module},ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}");
}

static void Main(string[] args)
{
    Console.WriteLine($"项目经理启动一个项目,ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}");
    Console.WriteLine($"前置的准备工作,ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}");
    Console.WriteLine($"开始编程,ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}");

    Task.Run(() => Coding("张三","Web"));
    Task.Run(() => Coding("李四", "Service"));
    Task.Run(() => Coding("王五", "SQL"));

    Console.WriteLine($"通知甲方验收,ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}");

    Console.ReadLine();
}

启动程序,啥情况,开发人员没有 coding 完成,项目经理就通知,甲方验收了。所以,项目经理需要等待开发人员 coding 完成之后,再进行通知甲方验收。
接着我们对程序进行修改,Task 通过了线程等待的方法,不过是 Array 类型的。 我们定义 List 将 个个开发者的任务添加到集合中,使用 WaitAll 方法进行等待全部任务完成。

public static void Coding(string name, string module)
{
    Console.WriteLine($"{name} Coding Start {module},ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}");

    Console.WriteLine($"{name} Coding End  {module},ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}");
}

static void Main(string[] args)
{
    List tasks = new List();

    Console.WriteLine($"项目经理启动一个项目,ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}");
    Console.WriteLine($"前置的准备工作,ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}");
    Console.WriteLine($"开始编程,ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}");

    tasks.Add(Task.Run(() => Coding("张三", "Web")));
    tasks.Add(Task.Run(() => Coding("李四", "Service")));
    tasks.Add(Task.Run(() => Coding("王五", "SQL")));

    Task.WaitAll(tasks.ToArray()); // 会阻塞当前线程,所有任务完成后,才进入下一行 卡界面

    Console.WriteLine($"通知甲方验收,ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}");

    Console.ReadLine();
}

启动程序,可以看到这次在开发者们(子线程 3、4、6)完成 Coding 后,项目经理(主线程 1)再通知甲方进行验收

WaitAll 等待也可以超时

WaitAny

例如:有时项目经理不知道开发项目的进度,于是在项目中任何一个模块开发完成后进行记录。Task提供了对应得 API WaitAny 方法,即任何一个子线程完成工作即开始下一行代码。

public static void Coding(string name, string module)
{
    Console.WriteLine($"{name} Coding Start {module},ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}");

    Console.WriteLine($"{name} Coding End  {module},ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}");
}

static void Main(string[] args)
{
    List tasks = new List();

    Console.WriteLine($"项目经理启动一个项目,ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}");
    Console.WriteLine($"前置的准备工作,ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}");
    Console.WriteLine($"开始编程,ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}");

    tasks.Add(Task.Run(() => Coding("张三", "Web")));
    tasks.Add(Task.Run(() => Coding("李四", "Service")));
    tasks.Add(Task.Run(() => Coding("王五", "SQL")));

    Task.WaitAny(tasks.ToArray()); // 会阻塞当前线程,任何一个任务完成后,才进入下一行

    Console.WriteLine($"完成一个模块开发,ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}");

    Console.ReadLine();
}

启动程序,可以看到在李四完成开发任务后,项目经理立马进行一次记录

因为 CPU 分时分片的原因,WaitAny 不是绝对的只代表一个任务完成。如下

WhenAll

与 WaitAll 对应得还有一个 WhenAll,也代表全部子线程完成任务。但 WaitAll 返回值是 int 类型,WhenAll 返回值是 Task 类型,也就是使用 WhenAll 后还可以再进步进行操作,接着 ContinueWith 方法就出现了。Task.WhenAll(XX).ContinueWith(XX) 的意思是,当全部任务完成后,在执行一个任务,且新的任务线程是从线程池拿取的,不会阻塞主线程。

public static void Coding(string name, string module)
{
    Console.WriteLine($"{name} Coding Start {module},ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}");

    Console.WriteLine($"{name} Coding End  {module},ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}");
}

static void Main(string[] args)
{
    Console.WriteLine($"Main Start,ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}");

    List tasks = new List();

    Console.WriteLine($"项目经理启动一个项目,ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}");
    Console.WriteLine($"前置的准备工作,ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}");
    Console.WriteLine($"开始编程,ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}");

    tasks.Add(Task.Run(() => Coding("张三", "Web")));
    tasks.Add(Task.Run(() => Coding("李四", "Service")));
    tasks.Add(Task.Run(() => Coding("王五", "SQL")));

    Task.WhenAll(tasks.ToArray()).ContinueWith(t =>
    {
        Console.WriteLine($"部署环境,联调测试,ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}");
    }); 

    Console.WriteLine($"Main End,ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}");

    Console.ReadLine();
}

启动程序,看到当开发者们完成 Coding 后,进行环境部署,且并没有阻塞主线程

WhenAny

与 WaitAny 对应得还有一个 WhenAny,也代表任何一个线程任务完成。但 WaitAny返回值是 int 类型,WhenAny 返回值是 Task 类型,也就是使用 WhenAny 后还可以再进步进行操作,接着 ContinueWith 方法就出现了。Task.WhenAny(XX).ContinueWith(XX) 的意思是,当任何一个任务完成后,在执行一个任务,且新的任务线程是从线程池拿取的,不会阻塞主线程。

public static void Coding(string name, string module)
{
    Console.WriteLine($"{name} Coding Start {module},ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}");

    Console.WriteLine($"{name} Coding End  {module},ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}");
}

static void Main(string[] args)
{
    Console.WriteLine($"Main Start,ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}");

    List tasks = new List();

    Console.WriteLine($"项目经理启动一个项目,ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}");
    Console.WriteLine($"前置的准备工作,ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}");
    Console.WriteLine($"开始编程,ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}");

    tasks.Add(Task.Run(() => Coding("张三", "Web")));
    tasks.Add(Task.Run(() => Coding("李四", "Service")));
    tasks.Add(Task.Run(() => Coding("王五", "SQL")));

    Task.WhenAny(tasks.ToArray()).ContinueWith(t =>
    {
        Console.WriteLine($"部署环境,联调测试,ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}");
    }); 

    Console.WriteLine($"Main End,ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}");

    Console.ReadLine();
}

因为 CPU 分时分片的原因,WhenAny 不是绝对的只代表一个任务完成。如下


应用、总结

WaitAll 应用场景,如 API Get 操作 ,数据源来自于多方(数据库、Redis、service等),可以使用多线程并发同时取数据,这个纯粹是为了提高性能的。

WaitAny 应用场景,如电商首页搜索,热点数据可能放到多处 Redis、Service 等,且每份缓存都是一样的,任何一个拿到返回给前端即可。

无论是 WaitAll WaitAny 都是会开界面的,那多线程有什么意义呢?答案:提升速度与优化体验。

WhenAll WhenAny 使用于异步操作后,会有后续动作的场景,这两个方法都会会从线程池获取线程,所以不会卡界面。


控制线程数量

Task 是基于 ThreadPool ,所以设置 ThreadPool 线程会限制 Task 最大的线程数量,通常这种方法不好,ThreadPool 是全局的。

如何使用 Task 本身控制线程数量。如:我有很多个任务,但我只希望使用 10 线程,如何实现呢?接下来请看

例如:实现思路,创建 task 集合,当任务列表任务存在大于 10 个,就等待任务呢完成,然后移除任务列表内已完成的任务

static void Main(string[] args)
{
    Console.WriteLine($"Main Start,ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}");

    List list = new List();
    for (int i = 0; i < 10000; i++)
    {
        list.Add(i);
    }

    Action action = i =>
    {
        Console.WriteLine($"Task {i},ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}");
        Thread.Sleep(new Random(i).Next(100, 300));
    };

    List tasks = new List();
    foreach (var i in list)
    {
        int k = i;
        tasks.Add(Task.Run(() => action.Invoke(k)));
        if (tasks.Count > 10)
        {
            Task.WaitAny(tasks.ToArray());
            tasks = tasks.Where(x => x.Status != TaskStatus.RanToCompletion).ToList();
        }
    }
    Task.WaitAll(tasks.ToArray());

    Console.WriteLine($"Main End,ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}");

    Console.ReadLine();
}

启动程序,可以看到执行任务的,来来回回就那一个线程

识别任务

WhenAll、WhenAny 完成后,使用 ContinueWith 怎么知道是哪个线程完成的?其实基于 Task 本身的话就 TaskFactory 一种方式,但简介方式也有,通过任务子类完成

TaskFactory
public static void Coding(string name, string module)
{
    Console.WriteLine($"{name} Coding Start {module},ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}");

    Console.WriteLine($"{name} Coding End  {module},ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}");
}

static void Main(string[] args)
{
    Console.WriteLine($"Main Start,ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}");

    TaskFactory taskFactory = new TaskFactory();
    List tasks = new List();

    tasks.Add(taskFactory.StartNew(o => Coding("张三", "Web"),"张三"));
    tasks.Add(taskFactory.StartNew(o => Coding("李四", "Service"), "李四"));
    tasks.Add(taskFactory.StartNew(o => Coding("王五", "SQL"),"王五"));

    Task tResult = taskFactory.ContinueWhenAny(tasks.ToArray(), t => {
        Console.WriteLine(t.AsyncState);
    });

    Console.WriteLine($"Main End,ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}");

    Console.ReadLine();
}

static void Main(string[] args)
{
    Console.WriteLine($"Main Start,ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}");

    TaskFactory taskFactory = new TaskFactory();
    List tasks = new List();

    tasks.Add(taskFactory.StartNew(o => Coding("张三", "Web"), "张三"));
    tasks.Add(taskFactory.StartNew(o => Coding("李四", "Service"), "李四"));
    tasks.Add(taskFactory.StartNew(o => Coding("王五", "SQL"), "王五"));

    Task tResult = taskFactory.ContinueWhenAll(tasks.ToArray(), tList =>
    {
        foreach (var t in tList)
        {
            Console.WriteLine(t.AsyncState);
        }
    });

    tResult.Wait(); // 等待,回调 task 完成,会卡主线程

    Console.WriteLine($"Main End,ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}");

    Console.ReadLine();
}



Callback

Task 回调是最简单的,使用 ContinueWith 即可,委托里面的参数是任务执行完的结果, Task.Run 与 ContinueWith 都会启动新的线程。

static void Main(string[] args)
{
    Console.WriteLine($"Main Start,ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}");

    Task.Run(() => { Console.WriteLine("处理任务"); }).ContinueWith(t => { Console.WriteLine("Callback"); }).Wait() ;

    Console.WriteLine($"Main End,ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}");

    Console.ReadLine();
}


WaitXX 与 WhenXX 不同

WhenXX 当前方法不会阻塞线程,WaitXX 当前方法会阻塞线程。WhenXX 适用异步场景,发个邮件写个日志啥的 。WaitXX 必须等待执行的结果。

扩展:其实 WaitXX 也可以做到不卡主线程,包一层 Task 即可,只不过是卡哪个线程的问题。

public static void Coding(string name, string module)
{
    Console.WriteLine($"{name} Coding Start {module},ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}");

    Console.WriteLine($"{name} Coding End  {module},ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}");
}

static void Main(string[] args)
{
    List tasks = new List();

    Console.WriteLine($"项目经理启动一个项目,ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}");
    Console.WriteLine($"前置的准备工作,ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}");
    Console.WriteLine($"开始编程,ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}");

    tasks.Add(Task.Run(() => Coding("张三", "Web")));
    tasks.Add(Task.Run(() => Coding("李四", "Service")));
    tasks.Add(Task.Run(() => Coding("王五", "SQL")));

    Task.Run(() =>
    {
        Task.WaitAll(tasks.ToArray()); // 会阻塞当前线程,所有任务完成后,才进入下一行 卡界面
    });

    Console.WriteLine($"通知甲方验收,ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}");

    Console.ReadLine();
}


Task.Delay与 Thread.Sleep 区别

Delay 不卡线程,Sleep 会卡线程。Delay里面是个 Timer 延迟多长时间发生额外操作。

例如:Delay

static void Main(string[] args)
{
    Console.WriteLine($"Main Start,ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}");

    Task.Delay(1000).ContinueWith(t =>
    {
        Console.WriteLine($"Task,ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}");
    });

    Console.WriteLine($"Main End,ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}");

    Console.ReadLine();
}


扩展:Sleep 其实也可以实现,包一层就行,其实就是 sleep 哪个线程的问题

static void Main(string[] args)
{
    Console.WriteLine($"Main Start,ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}");

    Task.Run(() => {
        Thread.Sleep(1000);
        Console.WriteLine($"Task,ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}");
    });

    Console.WriteLine($"Main End,ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}");

    Console.ReadLine();
}

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/984410.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号