线程的概念

线程的职责是对CPU进行虚拟化。

CPU为每个进程都提供了该进程专用的线程(功能相当于cpu),应用程序如果进入死循环,那么所处的进程会"冻结",但其他进程不会冻结,它们会继续执行!

线程的开销

因为是虚拟化CPU,所以也会有空间(内存耗用)和时间(执行性能)上的开销。

具体的开销:

  • 线程内核对象(thread kernel object)

    • 操作系统为创建的每个线程都会分配并初始化这种数据结构。数据结构包含一组对线程进行描述的属性,还包含线程的上下文,包含模拟CPU寄存器的集合的内存块。
  • 线程环境块(thread environment block,TEB)
    • TEB是在用户模式下分配和初始化的内存块。
    • TEB包含线程的异常处理链首。线程进入每个try块都在链首插入一个节点,线程退出try块从链中删除该节点。
    • TEB还包含线程的“线程本地存储”数据,以及由GDI和OpenGL图形使用的一些数据结构。
  • 用户模式栈(user-mode stack)
    • 用户模式栈存储传给方法的局部变量和实参。包含一个地址,指出当前方法返回,线程应该从什么地方接着执行。
    • Windows默认为每个线程的用户模式栈分配1MB的空间。 
  • 内核模式栈(kernel-mode stack)
    • 应用程序代码向操作系统中的内核模式函数传递实参时,还会使用内核模式栈。针对传给内核的任何实参,都会从用户模式栈复制到内核模式栈。
  • DLL线程连接(attach)和线程分离(detach)通知
    • 任何时候在进程中创建线程,都会调用进程中加载的所有非托管DLL的DLLMain方法,并向该方法传递DLL_THREAD_ATTACH标志。
    • 任何时候在进程中线程终止,都会调用进程中加载的所有非托管DLL的DLLMain方法,并向该方法传递DLL_THREAD_DETACH标志

从上面这些开销可以看出,创建和销毁一个线程的开销虽然没有进程那么大,但是也不小了。

甚至减少线程的数量还会提高垃圾回收的性能,因为垃圾回收时会挂起所有线程。

线程的切换也会有性能损失:

单CPU计算机一次只能做一件事情,所以所有的线程实际上是共享物理CPU的。多个CPU的计算机或者多核CPU,可以真正同时运行几个线程。然而单个线程还是只能在一个内核上运行。

任何时刻,一个CPU都只会被分配给一个线程。那个线程占用CPU一段时间后(叫做时间片),就会切换到另一个线程。(如果时间片结束后,Windows决定再次调用同一线程,那么不会执行线程切换)

线程切换大概每30毫秒进行一次。

每次线程切换时进行的操作:

  1. 将CPU寄存器的值保存到当前正在运行的线程的线程内核对象(前面提到过)内部的一个上下文结构。
  2. 从现有线程集合中选出一个线程供调度。如果该线程由另一个进程拥有,Windows在开始执行任何代码或者接触任何数据之前,还必须切换CPU“看见”的虚拟地址空间。
  3. 将切换到的新线程中,线程内核对象中的模拟寄存器的值加载到CPU的寄存器中

线程切换虽然消耗性能,但是却提供了一个健壮灵敏的操作系统。如果某线程进入死循环,不会影响其它线程。

线程在等待IO操作,会使线程进入等待状态,让线程在任何CPU上都不再调度,直到发生下一次输入事件。

使用专用线程执行异步的计算限制操作

以下介绍使用专用线程执行异步的计算限制操作,但是建议避免使用此技术,而用线程池来执行异步的计算限制操作。

如果执行的代码要求线程处于一种特定的状态,而这种状态对于线程池线程来说是非同寻常的,就可以考虑创建专用线程。

例如:

  • 线程需要以非普通线程优先级运行。而所有线程池都以普通优先级运行;虽然可以更改这一优先级,但不建议这么做。并且在不同的线程池操作之间,优先级的更改是无法持续的
  • 需要线程表现为一个前台线程,防止应用程序在线程任务结束前终止。线程池现场呢个始终为后台线程。如果CLR想终止进程,那么它们就完成不了任务。
  • 计算限制的任务需要长时间运行。线程池为了判断是否需要创建一个额外的线程,所采用的逻辑是比较复杂的。直接为长时间运行的任务创建专用线程,就可以避免这一问题。
  • 要启动线程,并可能调用Thread的Abort方法来提前终止它。

一个简单的使用专用线程执行异步操作的例子:

     static void Main(string[] args)
{
Thread 某线程 = new Thread(线程回调函数);
某线程.Start("hello");
Console.WriteLine("某线程运行开始");
某线程.Join();//join方法造成调用线程阻塞当前执行的任何代码,直到“某线程”销毁或者终止
Console.WriteLine("继续运行");
Console.Read();
}
private static void 线程回调函数(Object 状态参数) {
Thread.Sleep();
if (状态参数.GetType() == typeof(string))
{
Console.WriteLine("这是一个字符串");
}
else {
Console.WriteLine("未识别");
}
}

使用线程的理由

  • 可响应性

    • 在客户端GUI应用程序中,可以将一些工作交给线程进行,使GUI线程能灵敏响应用户输入。
  • 性能
    • 在多个CPU或多核CPU上使用多线程会提升性能,因为它可以真正意义上同时执行多件事情

线程调度和优先级

抢占式操作系统必须使用算法去判断什么时候调度哪些线程多长时间。

前面提到过,每个线程都包含一个线程内核对象,而内核对象中包含一个上下文结构,此结构中存储了线程上一次执行完毕后CPU寄存器的状态。

在一个时间片完后,windows会检查现存的所有线程内核对象,在这些对象中,只有那些没有正在等待什么的线程才适合调度。

Windows选择一个可调度的线程内核对象,并上下文切换到它。

然后线程开始执行代码,并在其进程的地址空间处理数据。然后过了一个时间片完后又循环执行以上操作。

Windows从系统启动开始便一直执行上下文切换,直到系统关闭为止。

之所以被称为抢占式操作系统,是因为线程可以在任何时间停止(被抢占)并调度另一个线程。

每个线程都分配了从0(最低)到31(最高)的优先级。系统决定为CPU分配哪个线程时,首先检查优先级为31的线程,并以一种轮流的方式调度它们。

只要还存在优先级高的可调度线程,那么就不会将优先级低的线程分配给CPU。这种情况称为饥饿。

系统启动时会创建一个特殊的零页线程,其优先级为0,而且是整个系统中唯一优先级为0的线程。在没有其它线程需要工作的时候,零页线程会将系统RAM的所有空闲页清零。

将优先级设为数字,实际操作中很难分配合理,于是微软给了一个更简单的方法。

在设计应用程序时,可以选择一个进程优先级类(可以选择Idle,Below Normal,Normal,Above Normal,High和Realtime),默认的Normal为最常见的优先级类。

事实上进程优先级类只是一个抽象的概念,Windows永远不会调度进程,只会调度线程。

在系统中什么都不做的时候运行的应用程序如屏保程序适合分配Idle优先级类。

而RealTime优先级优先级太高,甚至可能影响到操作系统任务,可能造成不能及时地处理键盘和鼠标输入。

而Windows还支持7个相对线程优先级(Idle,Lowest,Below Normal,Normal,Above Normal,Highest和Time-Critical),它们和进程优先级类一起确定最后的线程优先级。

最好是降低一个应用程序的优先级而不是提高另一个线程的优先级。高优先级的线程大多数时候应该使其保持为等待状态。

而我们可以通过设置Thread的Priority,向其传送ThreadPriority枚举类型定义的5个值之一:Lowest,Below Normal,Normal,Above Normal,Highest。

没有Idle和Time-Critical是因为CLR保留了。之前在垃圾回收那里提到的CLR的终结器线程以Time-Critical优先级运行。

如果应用程序以特殊的安全权限运行,可以使用System.Diagnostics命名空间中的Process和ProcessThread类,这两个类分别提供了进程和线程的Windows视图。

应用程序也可以使用AppDomain和Thread类,它们公开了AppDomain和线程的CLR视图。(这两个类不需要特殊的安全权限,只有部分操作需要)

前台线程和后台线程

CLR将线程分为前台线程和后台线程。

一个进程的所有前台线程停止运行时,CLR强制终止仍在运行的任何后台线程,并且不抛出异常。

static void Main(string[] args)
{
Thread 某线程 = new Thread(线程回调函数);
某线程.IsBackground = true;
某线程.Start("hello");
Console.WriteLine("某线程运行开始");
Console.WriteLine("继续运行");
}
private static void 线程回调函数(Object 状态参数) {
Thread.Sleep();
if (状态参数.GetType() == typeof(string))
{
Console.WriteLine("这是一个字符串");
}
else {
Console.WriteLine("未识别");
}
}

如果某线程为前台线程,那么应用程序在10秒后才终止,如果是后台线程,那么应用程序立即终止。

原因是如果是前台线程,那么在运行完Main函数后,还需要这个前台线程结束才终止应用程序,而如果转为后台线程,那么就会在所有前台线程终止后立马终止。

通过Thread对象来显示创建线程的都是前台线程,但是可以通过IsBackgroun属性来切换,而线程池都是后台线程。

【C#进阶系列】25 线程基础的更多相关文章

  1. [CLR via C#]25. 线程基础

    一.Windows为什么要支持线程 Microsoft设计OS内核时,他们决定在一个进程(process)中运行应用程序的每个实例.进程不过是应用程序的一个实例要使用的资源的一个集合.每个进程都赋予了 ...

  2. 25线程基础-CLR

    由CLR via C#(第三版) ,摘抄记录... 1.线程是CPU的虚拟化,windows为每个进程提供专用线程(CPU)2.线程开销:内存和时间. 线程内核对象—OS为系统中创建的每个线程都分配并 ...

  3. Python系列之 - 线程基础

    一.什么是线程 线程:顾名思义,就是一条流水线工作的过程,一条流水线必须属于一个车间,一个车间的工作过程是一个进程 所以,进程只是用来把资源集中到一起(进程只是一个资源单位,或者说资源集合),而线程才 ...

  4. C#进阶系列 ---- 《CLR via C#》

      [C#进阶系列]30 学习总结 [C#进阶系列]29 混合线程同步构造 [C#进阶系列]28 基元线程同步构造 [C#进阶系列]27 I/O限制的异步操作 [C#进阶系列]26 计算限制的异步操作 ...

  5. C#多线程编程系列(二)- 线程基础

    目录 C#多线程编程系列(二)- 线程基础 1.1 简介 1.2 创建线程 1.3 暂停线程 1.4 线程等待 1.5 终止线程 1.6 检测线程状态 1.7 线程优先级 1.8 前台线程和后台线程 ...

  6. C#进阶系列——WebApi 身份认证解决方案:Basic基础认证

    前言:最近,讨论到数据库安全的问题,于是就引出了WebApi服务没有加任何验证的问题.也就是说,任何人只要知道了接口的url,都能够模拟http请求去访问我们的服务接口,从而去增删改查数据库,这后果想 ...

  7. #进阶系列——WebApi 身份认证解决方案:Basic基础认证

    阅读目录 一.为什么需要身份认证 二.Basic基础认证的原理解析 1.常见的认证方式 2.Basic基础认证原理 三.Basic基础认证的代码示例 1.登录过程 2./Home/Index主界面 3 ...

  8. linux高级编程基础系列:线程间通信

    linux高级编程基础系列:线程间通信 转载:原文地址http://blog.163.com/jimking_2010/blog/static/1716015352013102510748824/ 线 ...

  9. Python进阶----线程基础,开启线程的方式(类和函数),线程VS进程,线程的方法,守护线程,详解互斥锁,递归锁,信号量

    Python进阶----线程基础,开启线程的方式(类和函数),线程VS进程,线程的方法,守护线程,详解互斥锁,递归锁,信号量 一丶线程的理论知识 什么是线程:    1.线程是一堆指令,是操作系统调度 ...

随机推荐

  1. 剑指Offer面试题:32.数字在排序数组中出现的次数

    一.题目:数字在排序数组中出现的次数 题目:统计一个数字在排序数组中出现的次数.例如输入排序数组{1,2,3,3,3,3,4,5}和数字3,由于3在这个数组中出现了4次,因此输出4. 二.解题思路 2 ...

  2. JavaScript 事件管理

    在设计JavaScript xxsdk的时候考虑到能让调用者参与到工作流程中来,开始用了回调函数.如下: this.foo = function(args,callbackFn) { //do som ...

  3. 扩展Bootstrap Tooltip插件使其可交互

    最近在公司某项目开发中遇见一特殊需求,请笔者帮助,因此有了本文的插件.在前端开发中tooltip是一个极其常用的插件,它能更好向使用者展示更多的文档等帮助信息.它们通常都是一些静态文本信息.但同事他们 ...

  4. [nRF51822] 11、基础实验代码解析大全 · 实验16 - 内部FLASH读写

     一.实验内容: 通过串口发送单个字符到NRF51822,NRF51822 接收到字符后将其写入到FLASH 的最后一页,之后将其读出并通过串口打印出数据. 二.nRF51822芯片内部flash知识 ...

  5. Android学习第二天-android常用命令

    上一篇文章中,我们重点讲解了adb的常用命令,下面我们一起来看看其它常用的命令 2 android 2.1 查看机器上所有已经安装的Android版本和AVD设备 2.1.1查看机器上已经安装的AVD ...

  6. 小型文件数据库 (a file database for small apps) SharpFileDB

    小型文件数据库 (a file database for small apps) SharpFileDB For english version of this article, please cli ...

  7. java中集合类中Collection接口中的Map接口的常用方法熟悉

    1:Map接口提供了将键映射到值的对象.一个映射不能包含重复的键:每个键最多只能映射到一个值.Map接口中同样提供了集合的常用方法. 2:由于Map集合中的元素是通过key,value,进行存储的,要 ...

  8. Android开发-之数据的存储方式一

    在Android中,数据的存储分为两种方式: 1.直接以文件的形式存储在目录中 2.以json格式存储在数据库中 将数据以文件的存储又分为两种方式: 1.生成.txt文件 2.生成xml文件 那么今天 ...

  9. WPF开发查询加班小工具

    先说一下,我们公司是六点下班,超过7点开始算加班,但是加班的时间是从六点开始计算,以0.5个小时为计数,就是你到了六点半,不算加班半小时,但是加班到七点半,就是加班了一个半小时. 一.打卡记录 首先, ...

  10. L2 Population 原理 - 每天5分钟玩转 OpenStack(113)

    前面我们学习了 VXLAN,今天讨论跟 VXLAN 紧密相关的 L2 Population. L2 Population 是用来提高 VXLAN 网络 Scalability 的. 通常我们说某个系统 ...