去年换工作时系统复习了一下.NET Core多线程相关专题,学习了一线码农老哥的《.NET 5多线程编程实战》课程,我将复习的知识进行了总结形成本专题。同时也特别推荐有兴趣的读者去学习一线码农老哥的《.NET 5多线程编程》课程。

本篇,我们来复习一下Thread与Task的相关知识点,预计阅读时间10分钟。

从时间和空间角度理解线程的开销

(1)多线程的优点

  • 提高响应能力

      • main thread:更新UI的东西

      • work thread:耗时的操作

  • 提高程序性能

      • 1个力工:1个月

      • 10个力工:3天~5天

(2)线程有哪些开销

  • 空间上的开销

    • 数据结构上的开销

      • C#:Thread

      • CLR:Thread(C++写的)

      • OS:Thread

    • 线程栈开销

      • 默认最大栈空间:1MB

      • 线程越多,栈空间越大

    • teb开销(thread enviornment block,线程环境块)

      • ThreadStatic

      • TLS

  • 时间上的开销

    • dllmain

      • 非托管dll 上面上游 dllmain

      • thread 在start时 会通知这些 非托管dll

      • thread 在exit时 也会通知这些非托管dll(资源清理)

    • switch context

      • Windows系统中大概30ms进行一次上下文切换,如果上下文切换非常频繁,会造成CPU暴高

      • 在上下文切换中涉及到CPU与thread的交互

        • 时间片到了,thread 暂停,涉及到数据保存(将高速缓存中的数据存到线程的本地存储中)

        • 时间片分配,thread 恢复,涉及到数据恢复(从线程的环境块中将当时的数据重新提取出来)

(3)总结

线程不是越多越好,线程有时间和空间上的开销,所以我们需要省着用。

线程的常用方法及生命周期管理

(1)Thread的基本操作

  • Start

    • 不带参数:new Thread(()=>{ xxxxxx }).Start();

    • 带参数:new Thread((obj)=>{ xxxxxx }).Start();

  • Join

    • 类似于Task.Wait()方法的作用

    • 不带超时参数:thread.Join();

    • 带超时参数:thread.Join(1000 * 5);

  • Sleep

    • 冻结当前线程指定时间:Thread.Sleep(1000 * 5);

  • IsBackground属性

    • 指明当前线程为 后台线程

    • 如果主线程退出,后台线程自动退出

    • 只有所有的前台线程都退出了,主线程才能退出

(2)对Thread的思考

现在实际开发中直接用thread的不多,因为它较为底层,很多程序员用不好。

  • 线程太多,造成上下文切换频繁(CPU暴高)

    • 比如创建了5000个thread,假设都在执行耗时任务,而运行主机只有6核12线程,必然会造成频繁的上下文切换

  • GC负担过大,徒增GC负担

    • 比如创建了5000个thread跑了任务后,虽然没有引用根了,但是GC还没有及时回收,因此这时它们就是dead thread,它们全都在托管堆上

(3)一些解决方案

  • ThreadPool:线程池

  • Task:基于ThreadPool的上层封装

线程池的使用及分析其设计思想

(1)为什么要使用线程池?

  • GC负担

  • 上下文切换

让thread得到更好的使用,提高利用率,减少不必要的创建和销毁。

(2)线程池的基本使用

  • 无参数

    • ThreadPool.QueueUserWorkItem(_ => { ........... });

  • object参数

    • ThreadPool.QueueUserWorkItem(obj => { p = obj as Person; ........... }, new Person() { Name = "Edison" });

    • 由于是object类型,涉及到多余的类型转换

  • 泛型参数

    • ThreadPool.QueueUserWorkItem

      (p => { ........... }, new Person() { Name = "Edison" }, true);

    • 第三个参数 bool preferLocal,一般建议传true,代表优先使用线程本地队列(Local Queue) 而不是 全局队列(Work Queue),降低锁竞争。

  • 其他方法

    • GetMinThreads, GetMaxThreads

    • ThreadCount、CompletedWorkItemCount

(3)ThreadPool的设计

  • WinDbg视角下的ThreadPool

  • ThreadPool的设计图如下:

在老版本的.NET Framework时代,只有一个全局队列,存在大量的锁竞争。

.NET Core中加入了本地队列,加入了本地队列,降低了锁竞争,并提高了线程的利用率。

具体实现思路是:

(1)每个线程优先从本地队列中取任务干活;

(2)如果本地队列中没有任务了,就从全局队列中取任务干活;

(3)当全局任务队列里面的任务没有的时候,CLR将会把其他有任务的线程中的未处理任务(比如上图中的WorkItem3),分配给这些空闲的线程(比如上图中的Thread3)去执行。这个机制也被称之为 偷窃机制。

这样做的其目的是每个线程都有事干,即提高线程池中的线程利用率

Task及如何运用其编排能力

(1)Task的设计思想

为什么会出现Task:

  • 获取Thread的返回值比较麻烦

  • 多个Thread的串行实现比较麻烦

  • Thread的父子关系实现比较麻烦(比如:所有的子Thread执行完后,才能结束父Thread)

本质问题:如何高效地对Thread进行编排?

本质理解:Task就是一个Thread的编排工具,它解决了任务之间如何串行、如何并行、如何嵌套、如何父子等关系的处理,让程序员可以重点关注任务,而不是Thread。

(2)Task的基本使用

方式一:new Task,不推荐使用

// 无参数
var task = new Task(()=>
{
Console.WriteLine($"Current ThreadId={Environment.CurrentManagedThreadId}");
});
task.Start();
// 有参
var task = new Task((obj)=>
{
Console.WriteLine($"Current ThreadId={Environment.CurrentManagedThreadId}, Current Content={obj}");
}, "Hello World");
task.Start()

方式二:Task.Factory.StartNew

// 无参数
var task = Task.Factory.StartNew(()=>
{
Console.WriteLine($"Current ThreadId={Environment.CurrentManagedThreadId}");
});
// 有参
var task = Task.Factory.StartNew((obj)=>
{
Console.WriteLine($"Current ThreadId={Environment.CurrentManagedThreadId}, Current Content={obj}");
}, "Hello World");

方式三:Task.Run

// 无参数
var task = Task.Run(()=>
{
Console.WriteLine($"Current ThreadId={Environment.CurrentManagedThreadId}");
});
// 有参
var task = Task.Run((obj)=>
{
Console.WriteLine($"Current ThreadId={Environment.CurrentManagedThreadId}, Current Content={obj}");
}, "Hello World");

Task串行、父子、并行等玩法

(1)串行玩法

var task1 = Task.Factory.StartNew(()=>
{
new Sheet1().WriteSheet();
}).ContinueWith(t =>
{
new Sheet2().WriteSheet();
}).ContinueWith(t =>
{
new Sheet0().WriteSheet();
});
task1.Wait();

(2)并行+串行玩法

var sheets = new List<Sheet> { new Sheet1(), new Sheet2() };
var tasks = new Task[2];
for(int i=0; i<sheets.Count; i++)
{
tasks[i] = Task.Factory.StartNew((index)=>
{
sheets[(int)index].WriteSheet();
}, i);
}
Task.WhenAll(tasks).ContinueWith(t=>
{
new Sheet[0].WriteSheet();
}).Wait();

(3)父子关系玩法

如果父Task中的任意一个子Task未完成,都不能继续。注意点:参数TaskCreationOptions.AttachedToParent

var sheets = new List<Sheet> { new Sheet1(), new Sheet2() };

//父task
var parent_task = Task.Factory.StartNew(() =>
{
//1. 子task1
var child_1_task = Task.Factory.StartNew(() =>
{
new Sheet1().WriteSheet();
}, TaskCreationOptions.AttachedToParent); //2. 子task2
var child_2_task = Task.Factory.StartNew(() =>
{
new Sheet2().WriteSheet();
}, TaskCreationOptions.AttachedToParent);
}); var continueTask= parent_task.ContinueWith(t =>
{
new Sheet0().WriteSheet();
}); Task.WhenAll(continueTask);

最后等待可以有几种写法:

continueTask.Wait();
Task.WaitAll(continueTask);
Task.WaitAny(continueTask);

以上三种会阻塞主线程。而下面这种方式不会阻塞主线程。

Task.WhenAll(continueTask);

解析:WaitAll/WaitAny方法阻塞了当前线程直到全完。WhenAll方法会开启个新监控线程去判读括号里的所有线程执行情况并立即返回,等都完成了就退出监控线程并返回监控数据

任务取消CTS机制的使用

CTS = CancellationTokenSource,它主要是帮助开发者实现优雅退出(Graceful Exit)。

(1)没有CTS之前如何处理的

一是Thread.Abort()

二是增加临时变量如isStop来判断(hard cod)

(2)理解框架中的CTS使用

namespace EDT.MultiThread.Demo
{
class Program
{
static void Main(string[] args)
{
CTSDemo();
} static void CTSDemo()
{
var source = new CancellationTokenSource(); var task = Task.Factory.StartNew(() =>
{
for (int i = 0; i < 5; i++)
{
Console.WriteLine($"当前线程:{Environment.CurrentManagedThreadId}, {DateTime.Now} 执行时间需要5s"); Thread.Sleep(1000);
}
}).ContinueWith(t =>
{
Console.WriteLine($"当前线程:{Environment.CurrentManagedThreadId}, 我是延续任务!");
}, source.Token); Thread.Sleep(3000);
source.Cancel(); Console.WriteLine("主线程要取消你啦。。");
Console.ReadLine();
} /// <summary>
/// 业务方法
/// </summary>
/// <param name="token"></param>
static void Run(CancellationToken token)
{
while (!token.IsCancellationRequested)
{
Thread.Sleep(1000);
Console.WriteLine("1. 正在处理 redis 业务"); Thread.Sleep(1000);
Console.WriteLine("2. 正在处理 mongodb 业务"); Thread.Sleep(1000);
Console.WriteLine("3. 正在处理 sqlserver 业务"); Thread.Sleep(1000);
Console.WriteLine("4. 正在处理 mysql 业务");
}
}
}
}

(3)其他功能

  • 延迟取消 CancelAfter

      • source.CancelAfter(1000 * 5);

  • 注册取消通知 Register

      • source.Token.Register(() => {.......});

任务调度机制及其自定义

(1)TaskScheduler是什么

TaskScheduler决定了将Task调度到什么地方去执行,即TaskScheduler决定了Task如何被调度。

(2)BCL中现存的TaskScheduler

  • ThreadPoolTaskScheduler

      • 如果不特别指定,默认就是 ThreadPoolTaskScheduler

      • 内部有两种处理逻辑,一种是针对LongRunning需求的Task,会单独走后台Thread路径;另一种是非LongRunning需求的Task,直接走ThreadPool线程池路径。

      • Why?针对LongRunning的Task,如果长时间运行占用着ThreadPool的线程,这时候ThreadPool为了保证线程充足,会再次开辟一些Thread,如果耗时任务此时释放了,会导致ThreadPool线程过多,上下文切换频繁,所以这种情况下让Task在Thread中执行还是非常不错的选择

  • SynchronizationContextTaskScheduler

      • 适用于GUI程序:耗时操作一般不会放到UI线程处理,而是放到工作线程去处理,处理完之后通过发送消息到Queue,GUI程序就可以从Queue中取出来消费,更新UI内容。

      • How?在Task.Factory.StartNew方法中传参:TaskScheduler.FromCurrentSynchronizationContext()

(3)自己实现一个TaskScheduler

自己实现一个单个Thread处理所有Task的TaskScheduler:

namespace EDT.MultiThread.Demo
{
public class CustomTaskScheduler : TaskScheduler
{
Thread thread = null; BlockingCollection<Task> collection = new BlockingCollection<Task>(); public CustomTaskScheduler()
{
thread = new Thread(() =>
{
foreach (var task in collection.GetConsumingEnumerable())
{
TryExecuteTask(task);
}
}); thread.Start();
} protected override IEnumerable<Task> GetScheduledTasks()
{
return collection.ToArray();
} protected override void QueueTask(Task task)
{
collection.Add(task);
} protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued)
{
throw new NotImplementedException();
}
}
}

调用端示例代码:

var scheduler = new CustomTaskScheduler();
for (int i = 0; i < 100; i++)
{
var task = Task.Factory.StartNew(() =>
{
Console.WriteLine($"当前线程:{Environment.CurrentManagedThreadId}");
}, CancellationToken.None, TaskCreationOptions.None, scheduler);
} Console.ReadLine();

小结

本篇,我们复习了Thread与Task的基础知识。

下一篇,我们复习面试常考的重点-异步(async/await)相关知识。

参考资料

一线码农,腾讯课堂《.NET 5多线程编程实战

不明作者,《Task调度与await》

作者:周旭龙

出处:https://edisonchou.cnblogs.com

本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文链接。

.NET Core多线程 (1) Thread与Task的更多相关文章

  1. c#中@标志的作用 C#通过序列化实现深表复制 细说并发编程-TPL 大数据量下DataTable To List效率对比 【转载】C#工具类:实现文件操作File的工具类 异步多线程 Async .net 多线程 Thread ThreadPool Task .Net 反射学习

    c#中@标志的作用   参考微软官方文档-特殊字符@,地址 https://docs.microsoft.com/zh-cn/dotnet/csharp/language-reference/toke ...

  2. C#异步和多线程以及Thread、ThreadPool、Task区别和使用方法

    本文的目的是为了让大家了解什么是异步?什么是多线程?如何实现多线程?对于当前C#当中三种实现多线程的方法如何实现和使用?什么情景下选用哪一技术更好? 第一部分主要介绍在C#中异步(async/awai ...

  3. NET 异步多线程,THREAD,THREADPOOL,TASK,PARALLEL

    .NET 异步多线程,THREAD,THREADPOOL,TASK,PARALLEL,异常处理,线程取消 今天记录一下异步多线程的进阶历史,以及简单的使用方法 主要还是以Task,Parallel为主 ...

  4. 多线程调用有参数的方法---c# Thread 与 Task

    C#实现多线程的方式:Task——任务   简介 .NET 4包含新名称空间System.Threading.Tasks,它 包含的类抽象出了线程功能. 在后台使用ThreadPool. 任务表示应完 ...

  5. C#中 Thread,Task,Async/Await,IAsyncResult 的那些事儿!

    说起异步,Thread,Task,async/await,IAsyncResult 这些东西肯定是绕不开的,今天就来依次聊聊他们 1.线程(Thread) 多线程的意义在于一个应用程序中,有多个执行部 ...

  6. 从Thread,ThreadPool,Task, 到async await 的基本使用方法解读

    记得很久以前的一个面试场景: 面试官:说说你对JavaScript闭包的理解吧? 我:嗯,平时都是前端工程师在写JS,我们一般只管写后端代码. 面试官:你是后端程序员啊,好吧,那问问你多线程编程的问题 ...

  7. Thread,ThreadPool,Task, 到async await 的基本使用方法和理解

    很久以前的一个面试场景: 面试官:说说你对JavaScript闭包的理解吧? 我:嗯,平时都是前端工程师在写JS,我们一般只管写后端代码. 面试官:你是后端程序员啊,好吧,那问问你多线程编程的问题吧. ...

  8. C#中 Thread,Task,Async/Await,IAsyncResult 的那些事儿![转载]

    说起异步,Thread,Task,async/await,IAsyncResult 这些东西肯定是绕不开的,今天就来依次聊聊他们 1.线程(Thread) 多线程的意义在于一个应用程序中,有多个执行部 ...

  9. 详解C#中 Thread,Task,Async/Await,IAsyncResult的那些事儿

    说起异步,Thread,Task,async/await,IAsyncResult 这些东西肯定是绕不开的,今天就来依次聊聊他们 1.线程(Thread) 多线程的意义在于一个应用程序中,有多个执行部 ...

  10. 多进程和多线程,Thread模块 GIL全局解释锁, 进程池与线程池,协程

    1.多进程实现TCP服务端并发: import socket from multiprocessing import Process def get_server(): server = socket ...

随机推荐

  1. 在循环内调用 size() 方法的开销大吗?

    for (int i = 0; i < buildings.size(); i++) {} 和 int n = buildings.size(); for (int i = 0; i < ...

  2. LeetCode 双周赛 103(2023/04/29)区间求和的树状数组经典应用

    本文已收录到 AndroidFamily,技术和职场问题,请关注公众号 [彭旭锐] 提问. 大家好,我是小彭. 这场周赛是 LeetCode 双周赛第 103 场,难得在五一假期第一天打周赛的人数也没 ...

  3. Centos7 部署Django项目 uwsgi + nginx

    启动 首先确保你的django项目是可以在虚拟环境中跑起来的,环境管理窝用的是pyenv,pyenv不知道什么东西的可以参考窝之前写过的Pyenv环境管理的安装文. 项目启动 python manag ...

  4. [OS/Linux] Linux核心参数:net.core.somaxconn(高并发场景核心参数)

    0 序言 近期工作在搞压力测试,我负责开发维护的.基于sring-cloud-gateway的大数据网关微服务,其底层是基于spring-webflux-->reactor-netty--> ...

  5. 2023-03-14:读取摄像头,并且显示视频。代码用go语言编写。

    2023-03-14:读取摄像头,并且显示视频.代码用go语言编写. 答案2023-03-14: 大体流程如下: 导入所需的库和包. 初始化 ffmpeg 和 SDL2 库. 打开摄像头并创建 AVF ...

  6. 2022-11-09:给定怪兽的血量为hp 第i回合如果用刀砍,怪兽在这回合会直接掉血,没有后续效果 第i回合如果用毒,怪兽在这回合不会掉血, 但是之后每回合都会掉血,并且所有中毒的后续效果会叠加 给

    2022-11-09:给定怪兽的血量为hp 第i回合如果用刀砍,怪兽在这回合会直接掉血,没有后续效果 第i回合如果用毒,怪兽在这回合不会掉血, 但是之后每回合都会掉血,并且所有中毒的后续效果会叠加 给 ...

  7. 2021-02-21:手写代码:高性能路由,也就是一个字符串和多个匹配串进行模糊匹配。一个数组arr里是["*a*","moonfdd"],字符串"moonfdd"能匹配到,理由是arr里有。字符串"xayy"也能匹配到,理由是arr里的"*a*",第1个星对应"x",第2个星对应"yy"。

    2021-02-21:手写代码:高性能路由,也就是一个字符串和多个匹配串进行模糊匹配.一个数组arr里是["a","moonfdd"],字符串"moo ...

  8. 2021-11-30:给定一个数组arr,当拿走某个数a的时候,其他所有的数都+a, 请返回最终所有数都拿走的最大分数。 比如: [2,3,1], 当拿走3时,获得3分,数组变成[5,4]; 当拿走5

    2021-11-30:给定一个数组arr,当拿走某个数a的时候,其他所有的数都+a, 请返回最终所有数都拿走的最大分数. 比如: [2,3,1], 当拿走3时,获得3分,数组变成[5,4]: 当拿走5 ...

  9. es笔记四之中文分词插件安装与使用

    本文首发于公众号:Hunter后端 原文链接:es笔记四之中文分词插件安装与使用 前面我们介绍的操作及演示都是基于英语单词的分词,但我们大部分使用的肯定都是中文,所以如果需要使用分词的操作肯定也是需要 ...

  10. 【原创】浅谈EtherCAT主站EOE(上)-EOE网络

    这篇文章的标题虽然是关于EtherCAT EOE,但其实主要内容是关于整个EOE网络结构,属于计算机网络原理.而EtherCAT EoE只是简单介绍,并不是文章的重点.需要注意的是,我们的描述主要基于 ...