异步多线程之入Task详解

1,129 阅读4分钟

!!! 本文已参与「新人创作礼」活动,一起开启掘金创作之路。更多干货文章,可以访问 菜鸟厚非

上一篇:异步多线程之入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<Task> tasks = new List<Task>();

    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<Task> tasks = new List<Task>();

    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<Task> tasks = new List<Task>();

    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<Task> tasks = new List<Task>();

    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<int> list = new List<int>();
    for (int i = 0; i < 10000; i++)
    {
        list.Add(i);
    }

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

    List<Task> tasks = new List<Task>();
    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<Task> tasks = new List<Task>();

    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<Task> tasks = new List<Task>();

    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<Task> tasks = new List<Task>();

    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();
}

在这里插入图片描述