08-04 多线程之Task

目录

  • 前言
  • 一、Task开启线程的方式
    • 1、Task实例化
    • 2、Task.Run()静态方法
    • 3、TaskFactory
  • 二、Task由线程池管理
  • 三、父子线程
    • 1、默认情况
    • 2、线程附着
  • 四、线程优先级
  • 五、允许线程长时间执行
  • 六、常用api
    • 1、Delay延时
    • 2、Id 和CurrentId
    • 3、WaitAny和WaitAll
    • 4、ContinueWhenAny和ContinueWhenAll
  • 七、设置线程数量
  • 八、多线程应用场景和意义
    • 1、场景
    • 2、意义
  • 九、本文代码

前言

  1. Task诞生于.NETFramework 3.0,同时支持.NET Core
  2. Task被称为C# 中多线程的最佳实现

一、Task开启线程的方式

1、Task实例化

Task task = new Task(() =>
{
    Console.WriteLine($"==================new Task start {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH.mm.ss.fff")}========================");
    DoSomething("new Task");
});
task.Start();

2、Task.Run()静态方法

Task task = Task.Run(() =>
{
    Console.WriteLine($"==================Task.Run start {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH.mm.ss.fff")}========================");
    DoSomething("Task.Run");
});

3、TaskFactory

TaskFactory taskFactory = Task.Factory;
taskFactory.StartNew(()=>{
    Console.WriteLine($"==================TaskFactory start {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH.mm.ss.fff")}========================");
    DoSomething("TaskFactory");
});

上面的私有方法:

/// 
/// 模拟比较消耗资源的方法
/// 
/// 
private void DoSomething(string name)
{
    Console.WriteLine($"==================DoSomething start {name} {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH.mm.ss.fff")}========================");
    long result = 0;
    //循环一百万次
    for (int i = 0; i < 1000_000; i++)
    {
        result += i;
    }
    //Thread.Sleep(2000);
    Console.WriteLine($"==================DoSomething end   {name} {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH.mm.ss.fff")} {result}========================");
}

08-04 多线程之Task_第1张图片

二、Task由线程池管理

Task也是有线程池管理,并且开启的线程是可以被重复利用的

//设置线程池中最多有18个线程,全局唯一
ThreadPool.SetMaxThreads(18, 18);
List<Task> taskList = new List<Task>();
List<string> threadIds = new List<string>();

//循环二十次,理论上会开启二十个线程,但是因为设置了最大线程数,所以线程池中最多只能开启14个线程,而且可以重复使用
for (int i = 0; i < 20; i++)
{
    taskList.Add(Task.Run(() =>
    {
        threadIds.Add(Thread.CurrentThread.ManagedThreadId.ToString("00"));
        Console.WriteLine($"==================当前线程id为 {Thread.CurrentThread.ManagedThreadId.ToString("00")} ========================");
    }));
}
Task.WaitAll(taskList.ToArray());
Console.WriteLine($"线程总数为:{threadIds.Distinct().Count()}");

08-04 多线程之Task_第2张图片

三、父子线程

父子线程就是线程里面开启线程

1、默认情况

默认情况下,父线程不会等待子线程,父线程不阻塞

/// 
/// 父子线程
/// 
/// 
/// 
private void button2_Click(object sender, EventArgs e)
{
    Console.WriteLine($"==================父子线程 start  {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss:fff")}========================");
	//父线程
    Task task = Task.Run(() =>
    {
        Console.WriteLine($"+++++++++++++++父线程 task start  {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss:fff")}+++++++++++++++");
        //子线程1
        Task task1 = Task.Run(() =>
        {
            Console.WriteLine($"子线程 task1 id:  {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss:fff")}");
            Thread.Sleep(3000);
            Console.WriteLine($"子线程task1 {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss:fff")}");
        });
		//子线程2
        Task task2 = Task.Run(() =>
        {
            Console.WriteLine($"子线程 task2 id:  {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss:fff")}");
            Thread.Sleep(3000);
            Console.WriteLine($"子线程task2 {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss:fff")}");
        });

        //Thread.Sleep(3000);
        Console.WriteLine($"+++++++++++++++父线程 task end  {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss:fff")}+++++++++++++++");
    });
    //等待父线程执行完成
    task.Wait();  //这里会阻塞主线程(卡界面)

    Console.WriteLine($"==================父子线程 end  {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss:fff")}========================");
}

分析:

  1. 主线程01开启一个父线程 task 04,主(UI)线程01等待父线程task 04完成;
  2. task 04线程中,代码执行从上到下,所以会先打印“父线程 task start”
  3. 开启子线程 task1 05task2 06,但是这两个线程是异步的,父线程task 04也没有等待这两个线程,所以此时输出:"父线程 task end"
  4. 接下来执行两个子线程 task1 05task2 06,所以会分别输出子线程id;
  5. 两个子线程中分别休眠了3秒,而父线程和主线程都没有等待这两个线程,所以此时主线程01已经运行完成;
  6. 三秒休眠完成后,开始分别同时执行两个子线程中的随后一行代码。
    08-04 多线程之Task_第3张图片
    注意:
    在等待父线程task 04 的时候秒其实是会卡顿界面的(主线程阻塞),为了效果更加明显,设置3秒休眠时间(但是上图的运行结果也会有所差异)
    08-04 多线程之Task_第4张图片
    可以看出在等待父线程执行的时候,有大概3秒的卡顿界面,鼠标不能拖动窗口。
    08-04 多线程之Task_第5张图片

2、线程附着

如果父线程在等待的时候,也要等待子线程执行完毕,就需要用到线程附着TaskCreationOptions.AttachedToParent

/// 
/// 线程附着
/// 
/// 
/// 
private void button3_Click(object sender, EventArgs e)
{
    Console.WriteLine($"==================父子线程 start  {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss:fff")}========================");
    //开启父线程
    Task task = new Task(() =>
    {
        Console.WriteLine($"+++++++++++++++父线程 task start  {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss:fff")}+++++++++++++++");
        //开启子线程1
        Task task1 = new Task(() =>
        {
            Console.WriteLine($"子线程 task1 id:  {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss:fff")}");
            Thread.Sleep(3000);
            Console.WriteLine($"子线程task1 {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss:fff")}");
        },TaskCreationOptions.AttachedToParent);
        //开启子线程2
        Task task2 = new Task(() =>
        {
            Console.WriteLine($"子线程 task2 id:  {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss:fff")}");
            Thread.Sleep(3000);
            Console.WriteLine($"子线程task2 {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss:fff")}");
        },TaskCreationOptions.AttachedToParent);

        task1.Start();
        task2.Start();

        //Thread.Sleep(3000); //休眠三秒,界面卡顿会更明显
        Console.WriteLine($"+++++++++++++++父线程 task end  {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss:fff")}+++++++++++++++");
    });
    task.Start();
    //等待父线程,主线程(UI线程)会阻塞,造成界面卡顿
    task.Wait();

    Console.WriteLine($"==================父子线程 end  {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss:fff")}========================");
}

注意事项:
08-04 多线程之Task_第6张图片
运行结果:主线程会等待父线程执行完毕,再等待子线程执行完毕,最后主线程结束
08-04 多线程之Task_第7张图片

四、线程优先级

TaskCreationOptions.PreferFairness可以设置线程优先级,但这里的优先级只是提高了优先执行的概率,并不是绝对的优先。

/// 
/// 线程优先级
/// 
/// 
/// 
private void button4_Click(object sender, EventArgs e)
{
    Console.WriteLine($"==================父子线程 start  {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss:fff")}========================");
    //开启线程
    Task task1 = new Task(() =>
    {
        Console.WriteLine($"开启一个新的线程task1:{Thread.CurrentThread.ManagedThreadId.ToString("00")}");
    });
    Task task2 = new Task(() =>
    {
        Console.WriteLine($"开启一个新的线程task2:{Thread.CurrentThread.ManagedThreadId.ToString("00")}");
    }, TaskCreationOptions.PreferFairness);
    task1.Start();
    task2.Start();
    等待线程,主线程(UI线程)会阻塞,造成界面卡顿
    //task1.Wait();
    //task2.Wait();

    Console.WriteLine($"==================父子线程 end  {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss:fff")}========================");
}

08-04 多线程之Task_第8张图片
运行结果:可以看到,如果不设置优先级,task1肯定是先于task2执行的,但是给task2设置优先级之后,task2就有一定概率优先执行
08-04 多线程之Task_第9张图片

五、允许线程长时间执行

TaskCreationOptions.LongRunning可以设置允许当前线程消耗大量的资源或长时间运行。

/// 
/// TaskCreationOptions.LongRunning:允许在子线程中消耗大量资源或者长时间运行
/// 
/// 
/// 
private void button5_Click(object sender, EventArgs e)
{
    Console.WriteLine($"==================button5_Click start  {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss:fff")}========================");
    //开启线程
    Task task = new Task(() =>
    {
        Console.WriteLine($"开启一个新的线程task1:{Thread.CurrentThread.ManagedThreadId.ToString("00")}");
    },TaskCreationOptions.LongRunning); //在子线程中,如果消耗了大量的资源,默认是允许的,允许长时间在子线程中执行任务
    
    task.Start();
    等待线程,主线程(UI线程)会阻塞,造成界面卡顿
    //task.Wait();

    Console.WriteLine($"==================button5_Click end  {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss:fff")}========================");
}

08-04 多线程之Task_第10张图片

六、常用api

1、Delay延时

不阻塞线程,一般不单独使用,Delay相当于子线程延时一段时间,然后完成某个动作

{
    Stopwatch stopwatch = new Stopwatch();
    stopwatch.Start();
    Console.WriteLine("Sleep之前");
    Thread.Sleep(2000);
    Console.WriteLine("Sleep之后");
    stopwatch.Stop();
    Console.WriteLine($"Sleep消耗时间:{stopwatch.ElapsedMilliseconds}");
}
//Sleep就是主线程等待了,Delay相当于子线程定时多久再去做某件事
{
    //Delay相当于计时多久,然后做事
    Stopwatch stopwatch = new Stopwatch();
    stopwatch.Start();
    Console.WriteLine("Delay之前");
    Task.Delay(2000).ContinueWith(s=> {
        stopwatch.Stop();
        Console.WriteLine($"Delay消耗时间:{stopwatch.ElapsedMilliseconds}");
        Console.WriteLine("这是一个延时任务");
    });
    Console.WriteLine("Delay之后");
}

08-04 多线程之Task_第11张图片
可以看到,打印Sleep之前Sleep之后中间阻塞了2秒,但是打印Delay之前Delay之后没有阻塞
08-04 多线程之Task_第12张图片

2、Id 和CurrentId

在子线程中的Task.CurrentId与 task.Id其实是一样的,都是指当前任务id

{
    //线程id和任务id
    Task t1 = new Task(()=> {
        Console.WriteLine($"This thread(t1) id is {Thread.CurrentThread.ManagedThreadId}");
        Console.WriteLine($"This task(t1) id is {Task.CurrentId}");
    });
    t1.Start();
    t1.Wait();
    Console.WriteLine($"This thread(main thread) id is {Thread.CurrentThread.ManagedThreadId}");
    Console.WriteLine($"This task(t1) id is {t1.Id}");

    //线程id和任务id
    Task t2 = new Task(() => {
        Console.WriteLine($"This thread id(t2) is {Thread.CurrentThread.ManagedThreadId}");
        Console.WriteLine($"This task(t2) id is {Task.CurrentId}");
    });
    t2.Start();
    t2.Wait();
    Console.WriteLine($"This thread(main thread)  id is {Thread.CurrentThread.ManagedThreadId}");
    Console.WriteLine($"This task(t2) id is {t2.Id}");
}

08-04 多线程之Task_第13张图片

3、WaitAny和WaitAll

(1)现在有两个需求:
1、发起多个任务,任意任务完成,则继续执行主线程中的操作
2、发起多个任务,全部任务完成。则继续执行主线程中的操作

{
    TaskFactory taskFactory = new TaskFactory();
	taskFactory.StartNew(() =>{
    	DoSomething("task1");
	});
	taskFactory.StartNew(() => {
    	DoSomething("task2");
	});
	taskFactory.StartNew(() => {
    	DoSomething("task3");
	});
}

如果按上面,执行结果为下图,明显不符合需求
08-04 多线程之Task_第14张图片
因此需要用到WaitAnyWaitAll方法:

{
    List<Task> tasks = new List<Task>();
    TaskFactory taskFactory = new TaskFactory();
    tasks.Add(taskFactory.StartNew(() =>{
        DoSomething("task1");
    }));
    tasks.Add(taskFactory.StartNew(() => {
        DoSomething("task2");
    }));
    tasks.Add(taskFactory.StartNew(() => {
        DoSomething("task3");
    }));

    //等待任意任务完成,就往后继续执行,会阻塞主线程(卡界面)
    Task.WaitAny(tasks.ToArray());
    Console.WriteLine("任意任务完成即可执行!");
    //等待全部任务完成,就往后继续执行,会阻塞主线程(卡界面)
    Task.WaitAll(tasks.ToArray());
    Console.WriteLine("全部任务完成后执行!");
}

结果符合需求
08-04 多线程之Task_第15张图片
(2)WaitAll的应用场景:

  比如做一个主页,主页上有很多模块,假设这些模块组成一个复杂对象,
  在取数据的时候,不同的模块的数据来自不同的地方(如:数据库,内存,文件等),
  此时就可以开启多个线程并行的取数,然后等待所有的模块的数据都获取到,
  将所有模块数据组装成一个复杂对象返回到前端

(3)WaitAny的应用场景

用淘宝举例:商品类目(同一商品)信息可能来自:缓存、数据库、第三方接口等,
那么在获取数据的时候,从这些途径的任一途径获取到数据就可以返回到页面展示了,
那么就可以发起多个线程分别从不同的途径获取数据,
任意一个线程获取到数据,就返回到页面,此时就可以用`WaitAny`

4、ContinueWhenAny和ContinueWhenAll

本质:回调

需求升级:

  • 发起多个任务,任意任务完成,则继续执行主线程中的操作(不能阻塞主线程)
  • 发起多个任务,全部任务完成。则继续执行主线程中的操作(不能阻塞主线程)
    (1)ContinueWhenAny
{
    List<Task> tasks = new List<Task>();
    TaskFactory taskFactory = new TaskFactory();
    tasks.Add(taskFactory.StartNew(() =>{
        DoSomething("task1");
    }));
    tasks.Add(taskFactory.StartNew(() => {
        DoSomething("task2");
    }));
    tasks.Add(taskFactory.StartNew(() => {
        DoSomething("task3");
    }));
    
    //任意任务完成,则继续执行主线程中的操作(不能阻塞主线程)
    taskFactory.ContinueWhenAny(tasks.ToArray(), s =>
    {
       //这里可能是开启了新的线程,也可能是上面已经完成了的任务的线程,因为已经完成的任务的线程回到线程池可能会被(这个线程)重新使用,这样的线程叫做热线程
       Console.WriteLine($"==============={Thread.CurrentThread.ManagedThreadId.ToString("00")} =================");
        //Console.WriteLine("第一个任务完成,给与奖励!");
        Console.WriteLine("任意任务完成后执行!");
    });
}

08-04 多线程之Task_第16张图片
(2)ContinueWhenAll

//全部任务完成,则继续执行主线程中的操作(不能阻塞主线程)
taskFactory.ContinueWhenAll(tasks.ToArray(), s =>
{
 	//这里可能是开启了新的线程,也可能是上面已经完成了的任务的线程,因为已经完成的任务的线程回到线程池可能会被(这个线程)重新使用,这样的线程叫做热线程
    Console.WriteLine($"==============={Thread.CurrentThread.ManagedThreadId.ToString("00")} =================");
    Console.WriteLine("全部任务完成后执行!");
});

08-04 多线程之Task_第17张图片

七、设置线程数量

思考:开启线程的时候如何控制线程数量?循环n次就开启n个线程吗?
要求:1、不能通过ThreadPool设置
2、线程数控制在20个

{
    List<Task> taskList = new List<Task>();
    for (int i = 0; i < 100; i++)
    {
    	int k = i;
        //判断没有完成的线程数量是否大于20(限制线程数量最多为20个)
        if (taskList.Count(p => p.Status != TaskStatus.RanToCompletion) > 20)
        {
            //如果未完成的线程数量大于20,则等待至少一个线程完成
            Task.WaitAny(taskList.ToArray());
            //重置taskList,将上面完成的线程去除,留下未完成的线程
            taskList = taskList.Where(p => p.Status != TaskStatus.RanToCompletion).ToList();
        }
        //再新建一个线程
        taskList.Add(Task.Run(() =>
        {
            Console.WriteLine($"当前是第{k}次循环,线程id为:{Thread.CurrentThread.ManagedThreadId.ToString("00")}");
        }));
    }
}

结果满足需求:
08-04 多线程之Task_第18张图片

八、多线程应用场景和意义

1、场景

任务并发:即任务可以并行

2、意义

提高性能,改善用户体验:以资源换时间

九、本文代码

多线程Task

你可能感兴趣的:(.net,core,C#,多线程)