去年换工作时系统复习了一下.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. 2021-02-03:手写代码:KMP算法。

    福哥答案2021-02-03: Knuth-Morris-Pratt 字符串查找算法,简称为 KMP算法,常用于在一个文本串 S 内查找一个模式串 P 的出现位置.这个算法由 Donald Knuth ...

  2. 各类源码下载网址(u-boot,linux,openssl,文件系统)

    一.U-Boot源代码下载 1.U-Boot的官方网站: https://www.denx.de/wiki/U-Boot/http://ftp.denx.de/pub/u-boot/ftp://ftp ...

  3. Cobalt Strike 连接启动教程,制作图片🐎(2)

    扫描有两种方式:arp 和 icmp 查看进程列表 攻击----生成后门-----Payload 可以生成各类语言免杀牧马---(输出:选择C或者python或者php) go.咕.com 生成c语言 ...

  4. 代码随想录算法训练营Day36 贪心算法

    代码随想录算法训练营 代码随想录算法训练营Day36 贪心算法| 435. 无重叠区间 763.划分字母区间 56. 合并区间 435. 无重叠区间 题目链接:435. 无重叠区间 给定一个区间的集合 ...

  5. 《最新出炉》系列初窥篇-Python+Playwright自动化测试-1-环境准备与搭建

    1.简介 有很多人私信留言宏哥问能不能介绍一下Playwright这款自动化神器的相关知识,现在网上的资料太少了.其实在各大博客和公众号也看到过其相关的介绍和讲解.要不就是不全面.不系统,要不就是系统 ...

  6. CentOS Linux 7 配置 nginx 支持 CGI

    Nginx 本身不能执行外部程序,Nginx 处理 PHP 是通过 PHP 的 fastcgi 管理器(php-fpm)进行处理,然后 nginx 再将结果返回给用户:所以如果我们需要通过 cgi 程 ...

  7. 24 式加速你的 Python

    一,分析代码运行时间 第1式,测算代码运行时间 平凡方法 快捷方法(jupyter环境) 第2式,测算代码多次运行平均时间 平凡方法 快捷方法(jupyter环境) 第3式,按调用函数分析代码运行时间 ...

  8. 【技术积累】Java中的泛型【一】

    泛型是什么 Java中的泛型是一种能够让用户在编写代码时避免使用明确的类型而进行类型参数化的机制.Java中的泛型可以让编程者在代码编写时不必关心具体类型,只用关心类型之间的关系和相互转换,从而在编写 ...

  9. 【问题解决】 网关代理Nginx 301暴露自身端口号

    一般项目上常用Nginx做负载均衡和静态资源服务器,本案例中项目上使用Nginx作为静态资源服务器出现了很奇怪的现象,我们一起来看看. "诡异"的现象 部署架构如下图,Nginx作 ...

  10. 图书搜索领域重大突破!用Apache SeaTunnel、Milvus和OpenAI提高书名相似度搜索精准度和效率

    作者 | 刘广东,Apache SeaTunnel Committer 背景 目前,现有的图书搜索解决方案(例如公共图书馆使用的解决方案)十分依赖于关键词匹配,而不是对书名实际内容的语义理解.因此会导 ...