一、基础概念
1.1线程与进程
线程——是一个可执行路径,它可以独立于其他线程执行。
进程——每个线程都在操作系统的进程(Process)内执行,而操作系统则提供了程序运行的独立环境,它提供了一个应用程序的一个实例所需要使用资源的集合(执行一个.exe文件其实就是启动一个进程,也是启动一个应用程序实例),每个进程都至少有一个线程
比如进程启动时就会开辟一片独立的内存空间,进程内的所有线程共同享有这片内存,进程与进程之间的空间是独立的,彼此无法访问,这确保了进程与进程无法互相破坏。
1.2线程是怎样执行的
什么是线程上下文的切换?_房东的猫儿呀的博客-CSDN博客_什么是线程上下文
现有的计算机系统对线程(thread)的职责定义是对cpu虚拟化,简单理解就是每个cpu都是一个打工人。现在有很多份活等着他处理,比如xx音乐(音乐播放,音量调整,界面调整)、xx下载(下载、边看边播、界面操作)都等着他来做,现在的做法是将这些活儿细分到最小(细分成单独的线程),在某一时刻打工人只能做其中一件事。windows给每个线程分配一个时间片(执行时间),大约为30毫秒,cpu就在这个时间片时间内执行这个线程,时间片到期后切换到另外一个线程去执行。因为cpu执行速度非常快,30毫秒时间非常短,远低于人的感知,所以即使是单核计算机(单个打工人),在播放音乐的同时调整音量,在这两个线程内来回切换,人也无法感知。对于人来说是”同时“进行的。
每个线程的几个要素:
线程内核对象(thread kernel object)——OS为系统中创建的每个线程都分配并初始化这种数据结构之一。在该数据结构中,包含一组对线程进行描述的属性(本章后面讨论)。
数据结构中还包含所谓的线程上下文(thread context)。上下文是一个内存块,其中包含了CPU的寄存器集合。
Windows在一台使用x86CPU的计算机上运行时,线程上下文使用约700字节的内存。对于x64和1A64CPU,上下文分别使用约1240字节和2500字节的内存。
用户模式栈(uscr-mode stack)用户模式栈用于存储传给方法的局部变量和实参。
它还包含一个地址:指出当前方法返回时,线程接着应该从什么地方开始执行。献认情况
下,Windows为每个线程的用户模式栈分配1MB内存。”
线程上下文(thread context)——可以这么理解,cpu在线程之间来回切换执行时,需要做两件事
1、保存当前线程的执行进度
2、加载下一个要执行线程的原有进度
比如我在播放音乐的时候去调整音量,
1、我需要保存当前音乐的播放进度
2、我需要加载原来调整的音量刻度位置(不是每次都要从0加载)
这个进度保存是通过程序计数器实现的,每个线程都有独立的程序计数器,各条线程之间的计数器互补影响,存放在”线程私有“的内存里
注意:一个时间片结束,系统有可能会决定再次调用同一个线程,就不会执行上下文切换的动作,我们写代码的时候也要尽量避免上下文的切换,因为切换过程本身也是一种资源与时间消耗,即使非常小,在人的层面无法感知
问题一:线程锁的由来
对于进程内的共有资源,有多个线程竞争进程公共内存的公共资源时会需要加线程锁,防止多个线程同时操作同一个资源,造成类似数据库脏读的现象
问题二:windows系统为什么要采用线程来回切换执行的机制
试想一下如果我们采用一个线程执行完再执行下一个线程的机制(参照我们在主线程的行为,就是按顺序执行的),如果遇到大文件下载,如果我们是单核计算器,就不得不长时间等待十几分钟甚至几个小时等文件下载完才能做其他事,体验和效率都太不友好了
关于C#中Thread.Join()的一点理解 - slikyn - 博客园 (cnblogs.com)
前后台线程
C#夯实基础之多线程二:主线程、前台线程与后台线程 - gudi - 博客园 (cnblogs.com)
主线程——当一个程序(进程)启动时,就有一个进程被操作系统(OS)创建,与此同时一个线程也立刻运行,该线程通常叫做程序的主线程(Main Thread),主线程也是前台线程。
默认情况下,在代码中手动创建的线程是前台线程
只要有前台线程在运行,那么应用程序(进程)就会一直处于活动状态
例子:xx下载,比如我们打开xx下载,同时创建10个下载任务,我们删除其中一个程序还是照样进行,但是我们点击退出,就直接结束了应用。这个界面操作的线程就是前台线程(也是主线程),在它结束后,所有的后台线程(10下载任务)都会马上结束
线程优先级
每个线程都分配了0-31的优先级,cpu在分配时间片的时候,总是先有限分配可执行的优先级高的线程去执行。比如一个优先级31的线程先被cpu执行,在执行时间片结束时,cpu会查找当前是否有其他优先级为31的可执行的线程,
如果有就分配给这个线程时间片执行,如果没有可执行的(比如有的线程优先级高,但它是刚好执行到了等待I/O返回的状态,显然这个是不属于可执行的状态)的31级的线程,cpu会继续查找30级的可执行的线程,以此类推
所以如果出现几个高优先级的,执行时间长的计算密集型的线程存在,cpu就会在在这几个高线程之间轮流分配时间片,其他低优先级的线程就会拿不到执行时间片,一直阻塞
高优先级——适应低时延(比如硬件的操作响应)、执行时间短后进入等待的线程,例如任务管理器,就是要马上响应,然后快速计算展现正在运行的线程后进入等待用户的下一步操作
低优先级——锁屏的线程就是个中代表,就是等待大多数正常优先级的线程执行完成后它才执行。
高优先级要慎用,最好降低一个线程的优先级,不要提高另外一个线程的优先级
线程池化
背景:当我们在进行多线程编程时,就需要创建线程,如果说程序并发很高的话,我们会创建大量的线程,而每个线程执行一个时间很短的任务就结束了,这样频繁创建线程,会极大的降低系统性能,增加服务器开销,因为创建线程和销毁线程都需要额外的消耗。线程池技术就是针对优化这一缺陷
需要线程池的原因:
1、创建和销毁线程是一个昂贵的操作,要耗费大量时间。
2、太多的线程会浪费内存资源。
3、由于操作系统必须调度可运行的线程并执行上下文切换,所以太多的线程还有损于性能。
为了改善这个情况,CLR包含了代码来管理它自己的线程池。可将线程池想象成可由你的应用程序使用的一个线程集合(一个进程对应一个线程池,对进程内的所有线程共享)。
池化技术介绍: 什么时池化技术呢?池化技术是一种编程技巧,当程序出现高并发时,能够明显的优化程序,降低系统频繁创建销毁连接等额外开销。我们经常接触到的池化技术有数据库连接池、线程池、对象池(依赖注入思想)等等。池化技术的特点是将一些高成本的资源维护在一个特定的池子(内存)中,规定其最小连接数、最大连接数、阻塞队列,溢出规则等配置,方便统一管理。一般情况下也会附带一些监控,强制回收等配套功能。
适用场景:
1、获取资源的频率很高,且使用资源总数有限(如果不固定好最大线程数量,一直不断开启新的线程、销毁,会耗费大量的cpu资源)
2、对处理时间的延迟比较高,响应速度比较快(线程的创建和销毁都需要时间,会拖慢响应的速度)