查看: 318|回复: 0

[.NET源码] CLR via C# 读书笔记-26.线程基础

发表于 2017-12-6 08:00:04

前言

这俩个月没怎么写文章做记录分享,一直在忙项目上线的事情,但是学习这件事情,停下来就感觉难受,clr线程这章也是反复看了好多遍,书读百遍其义自见,今天我们来聊下线程基础

1.进程是什么,以及线程起源

2.线程开销,以及上线文切换

3.使用线程的理由

4.线程调度和优先级

5.前台线程和后台线程

一、进程是什么,以及线程起源

在计算机的早期岁月,os没有线程的概念,整个系统只运行者一个执行线程,其中包含操作系统和应用程序的代码。这意味着长时间运行的任务会阻止其他任务的运行。在16位windows的那些日子,打印文档的应用程序很容易“冻结”整个机器,造成os和其他应用程序停止响应。遇到这个问题,用户只能按Reset建或重启计算机,所有正在运行的应用程序都会终止,造成应用程序正在处理的数据都会无端的丢失,用户对此深恶痛绝。

Microsoft由此设计出新的OS内核,决定在一个进程中运行应用程序的每个实例,进程实际是 应用程序的实例要使用的资源的集合。每个进程都赋予了一个虚拟的地址空间,确保在一个进程中使用的代码和数据无法由另一个进程访问,进程也访问不了OS的内核代码和数据,所以,应用程序破坏不了其他应用程序或者OS自身,用户体验变得更好了。

听起来不错,但CPU本身呢?应用程序发生死循环会发生什么?如果机器只有一个CPU,它会执行死循环,虽然数据无法被破坏,但系统仍然可能停止响应。Microsoft拿出的解决方案就是线程。作为一个Windows概念,线程的职责是对CPU进行虚拟化。Windows为每个进程都提供了该进程专用的线程(功能相当于一个CPU)。应用程序的代码进入死循环,与那个代码关联的进程会“冻结”,但其他进程(他们有自己的线程)不会冻结,他们会继续执行!

二、线程开销

1.线程内核对象(thread kernel object)

OS为系统中创建的每个线程都分配并初始化这种数据结构之一。数据结构包含一组对线程进行描述的属性。线程结构还包含所谓的线程上下文(thread context)。上下文是包含CPU寄存器集合的内存块。对于x86,x64和ARM CPU架构,线程上下文分别使用约700,1240和350字节的内存

2.线程环境快(thread environment block,TEB)

TEB是在用户模式(应用程序代码能快速访问的地址空间)中分配和初始化的内存块。TEB耗用一个内存页(x86,x64,和ARM CPU中是4KB)。TEB包含线程的异常处理链首(head)。线程进入的每个try块都在链首插入一个节点(node);线程退出try块时从链中删除该节点。此外,TEB还包含线程的“线程本地存储”数据,以及GDI(Graphics Device Interface,图形设备接口)和OpenGL图形使用的一些数据结构

3.用户模式栈(user-mode stack)

用户模式栈存储传给方法的局部变量和实参。它还包含一个地址;指出当前方法返回时,线程应该从什么地方接着执行。Windows默认为每个线程的用户模式栈分配1MB内存。

4.内核模式栈(kernel-mode-stack)

应用程序代码向操作系统中的内核模式函数传递实参时,还会使用内核模式栈。由于对安全的考虑,针对从用户模式的代码传给内核的任何实参,Windows都会把它们从线程的用户模式栈复制到线程的内核模式栈。一经复制,内核就可验证实参的值。由于应用程序代码不能访问内核模式栈,所以应用程序无法更改验证后的实参值。OS内核代码开始处理复制的值。除此之外,内核会调用它自己内部的方法,并利用内核模式栈传递它自己的实参、存储函数的局部变量以及存储返回地址。

4.DLL线程连接(attach)和线程分离(detach)通知

Windows的一个策略是,任何时候在进程中创建线程,都会调用进程中加载的所有非托管DLL的DllMain方法,并向该方法传递DLL_THREAD_ATTACH标志。类似地,任何时候线程终止,都会调用进程中的所有非托管DLL的DllMain方法,并向方法传递DLL_THREAD_DETACH标志。有的DLL需要获取这些通知,才能为进程中创建/销毁的每个线程执行特殊的初始化或(资源)清理操作。

也就是说线程创建销毁时会调用所有DLL中的这个函数,严重影响了进程中创建和销毁线程的性能

上下文切换

单CPU计算机一次只能做一件事情。Windows必须在系统中的所有线程(逻辑CPU)之间共享物理CPU。

Windows任何时刻只将一个线程分配给一个CPU 。那个线程能运行一个“时间片”(有时也成为“量”或者“量程”,即quantum)的长度。时间片到期,Windows就上下文切换到另一个线程。每次上下文切换都要求windows执行以下操作。

1.将CPU寄存器的值保存到当前正在运行的线程的内核对象内部的一个上下文结构中。

2.从现有线程集合中选出一个线程供调度。

3.将所选上下文结构中的值加载到CPU的寄存器中。

上下文切换完成后,CPU执行所选的线程,直到它的时间片到期。然后发生下次上下文切换。Windows大约每30毫秒执行一次上下文切换。

要构建高性能应用程序和组件,就应该尽量避免上下文切换。

减少线程的数量也会显著提升垃圾回收的性能。

三、使用线程的理由

1.可响应性(通常是对于客户端GUI应用程序)

例如windows中每个进程提供它自己的线程,确保发生死循环的应用程序不会妨碍其它应用程序。类似地,在客户端GUI应用程序中,可以将一些工作交给一些线程进行,使GUI线程能灵敏的响应用户输入。

2.性能(对于客户端和服务端应用程序)

由于windows每个CPU调度一个线程,而且多个CPU能并发执行这些线程,所以同时执行多个操作能提升性能。

满足以下任何条件,就可考虑显式创建自己的线程

1.线程需要以非普通线程优先级运行

2.需要线程表现为一个前台线程,防止应用程序在线程结束任务前终止

3.计算限制的任务需要长时间运行

4.要启动的线程,并可能调用Thread的abort方法来提前终止它

四、线程调度和优先级

每个线程都分配了从0(最低)到31(最高)的优先级。

只要存在可调度的优先级31的线程,系统就永远不会将优先级0~30的任何线程分配给CPU。这种情况成为饥饿。

系统启动时会创建一个特殊的零页线程(zero page thread)。该线程的优先级为0,而且是整个系统唯一优先级为0的线程。在没有其他线程需要“干活儿”的时候,零页线程将系统RAM的所有空闲页清零。

五、前台线程和后台线程

  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using System.Text;
  5. using System.Threading;
  6. namespace ConsoleApplication2
  7. {
  8. class Program
  9. {
  10. static void Main(string[] args)
  11. {
  12. //创建新线程(默认为前台线程)
  13. Thread t = new Thread(Worker);
  14. //使线程成为后台线程
  15. t.IsBackground = true;
  16. //启动线程
  17. t.Start();
  18. //如果t是前台线程,则应用程序大概10秒后才终止
  19. //如果t是后台线程,则应用程序立即终止
  20. Console.WriteLine("Returing from Main");
  21. }
  22. private static void Worker()
  23. {
  24. //模拟做10秒钟的工作
  25. Thread.Sleep(10000);
  26. //下面这行代码只有由一个前台线程执行时才会显示
  27. Console.WriteLine("Returning from Worker");
  28. }
  29. }
  30. }
复制代码
View Code

应用程序的主线程以及通过构造一个Thread对象来显式创建的任何线程都默认为前台线程。相反,线程池线程默认为后台线程。

另外,由进入托管执行环境的本机(native)代码创建的任何线程都被标记为后台线程。

天道酬勤,大道至简,坚持。



回复

使用道具 举报

关闭

站长推荐上一条 /1 下一条