我们运行一个exe,就是一个进程实例,系统中有很多个进程。每一个进程都有自己的内存地址空间,每个地址相当于一个独立的边界,有自己独占的资源,进程之间不能共享代码和数据空间。(可用SendMessage实现进程之间消息传递)每一个进程有一个或多个线程,进程内多个线程可以共享所属进程的资源和数据,线程是操作系统调度的基本单元。线程是操作系统来调度和执行的。
当我们创建了一个线程后,线程里面到底有些什么东西呢?主要包括线程内核对象、线程环境块、1M大小的用户模式栈、内核模式栈。其中用户模式栈对于普通的系统线程那1M是预留的,在需要的时候才会分配,但是对于CLR线程,那1M一开始就分配了内存空间的。CLR线程是直接对应于一个Windows线程的。
还记得以前学校里学习的计算机课程里讲到,计算机的核心计算资源就是CPU核心和CPU寄存器,这也就是线程运行的主要战场。操作系统中那么多线程(一般都有上千个线程,大部分都处于休眠状态),对于单核CPU,一次只能有一个线程被调度执行,那么多线程怎么分配的呢?Winsows系统采用时间轮训机制,CPU计算资源以时间片(大约30ms)的实行分配给执行线程。
计算机资源(CPU核心和CPU寄存器)一次只能调度一个线程,具体的调度流程:
1.把CPU寄存器内的数据保存到当前线程内部(线程上下文等地方),给下一个线程腾地方;
2.线程调度:再线程集合里取出一个需要执行的线程;
3.加载新线程的上下文数据到CPU寄存器;
4.新线程执行,享受她自己的CPU时间片(大约30ms),完了之后继续回到第一步,继续轮回;
上面线程调度的过程,就是一次线程切换,一次切换就涉及到线程上下文灯数据的搬入搬出,性能开销很大的。因此线程不可滥用,线程的创建和消费也是和昂贵的,这也是为什么建议尽量使用线程池的一个主要原因。
对于Thread的使用太简单了,不重复了,总结一下线程的主要几点性能影响:
1、线程的创建、销毁时很昂贵的;
2.线程上下文切换有极大的性能开销,当然假如需要调度的新线程与当前是同一线程的话,就不需要线程上下文切换了,效率也要快很多。
3、这一点需要注意,GC执行回收时,首先(安全的)要挂起所有线程,遍历所以线程栈(根),GC回收后更新所有线程的根地址,再恢复线程调用,线程越多,GC要干的活就越多;
当然现在硬件的发展,CPU的核心越来越多,多线程技术可以极大提高应用程序的效率。但这也是必须在合理利用多线程技术的前提下,了解线程的基本原理,然后根据实际需求,好药注意相关资源环境,如磁盘IO、网络等情况综合考虑。
这是很多开发C/S客户端应用程序会遇到的问题,GUI程序的界面控件不允许跨县城访问,如果在其他线程中访问了界面控件,运行时就会抛出一个异常,就像下面的图示,是不是很熟悉,这其中的罪归祸首就是,就是“GUI的线程处理模型“,GUI应用程序(主要是Winform、wpf)引入了一个特殊线程处理模型,UI控件元素只能由创建它的线程访问和修改,微软这样处理是为了保证UI控件的线程安全。
为什么在UI线程执行一个耗时的计算操作,会导致UI假死呢?这个问题要追溯到Windows的消息机制了。
因为Windows是基于消息机制的,我们在UI上所有的键盘、鼠标操作都是以消息的形式发送给各个应用程序的。
GUI线程内部就有一个消息队列,GUI线程不断的循环处理这些消息,并根据消息更新UI的呈现。如果这个时候,
你让GUI线程去处理一个耗时的操作(比如花10秒去下载一个文件),那GUI线程就没办法处理消息独立饿了,
UI界面就处于假死状态。
那我们该怎么办呢?不难想到使用线程,拿在线程里处理事件完成后,需要更新UI控件的状态,又该怎么办呢?
常用几种方式:
1.使用GUI控件提供的方法,Winform是控件的Invoke方法,wpf中控件的Dispatcher.Invoke方法
2.使用.net中提供的backgroundWorker执行耗时计算操作,在其任务完成事件RunWorkCompleted中更新UI控件
3.看上去很高大上的方法:使用GUI线程处理模型的同步上下文来送封UI控件修改操作,这样可以不需要调用UI控件元素
应用程序必须运行完所有的前台线程才可以退出,或者主动结束前台线程,不管后台线程是否还在运行,应用程序都会结束;对于后台线程,应用程序则不考虑是否已经运行完毕而直接退出,所以的后台线程在应用程序退出时自动结束。
通过将Thread.isBackground设置为True,就可以将线程指定为后台线程,主线程就是一个前台线程。
常用的如如SemaphoreSlim、ManualResetEventSlim、Monitor、ReadWriteLockSlim,lock是一个混合锁,其实质是Monitor
- lock为什么要锁定一个参数(可否为值类型?)参数有什么要求?
lock的锁对象要求为一个引用类型。她可以锁定值类型,但值类型会被装箱,每次装箱后的对象都不一样,会导致锁定无效。
对于lock锁,锁定的这个对象参数才是关键,这个参数的同步索引块指针会指向一个真正的锁(同步块),这个锁(同步块)会被复用。
线程池的使用是非常简单的,如下面的代码,把需要执行的代码提交到线程池,线程池内部会安排一个空闲的线程来执行你的代码,完全不用管理内部是如何进行线程调度的。
ThreadPool.QueqeUserWorkItem(t=>Console.WriteLine("Hello thread pool"));
每个CLR都有一个线程池,线程池在CLR内可以多个AppDomain共享,线程池是CLR内部管理的一个线程集合,初始是没有线程的,在需要的时候才会创建。线程池的主要结构图如下,基本流程如下:
1.线程池内部维护一个请求列队,用于缓存用户请求需要执行的代码任务,就是Thread.QueqeUserWorkItem提交的请求;
2.有新任务后,线程池使用空闲线程或者新线程来执行队列请求;
3.任务执行完后线程不会销毁,留着重复使用;
4.线程池自己负责维护线程的创建和销毁,但线程池有大量闲置的线程是,线程池会自动结束一部分多余的线程来释放资源;
线程池是有一个容量的,因为他是一个池子吗,可以设置线程池的最大活跃线程数,调用方法Threadpool.SetMaxThreads可是设置相关参数,但很多线程实践里都不建议程序猿们自己去设置这些参数,其实微软为了提高线程池性能,做了大量的优化,线程池可以很智能的确定是否要创建或者消费线程,大多数情况都可以满足需求了。线程池使得线程可以充分有效的被利用,减少了任务启动的延迟,也不用大量的去创建线程,避免了大量线程的创建和销毁对性能造成极大影响。
上面了解了线程的基本原理和诸多优点后,如果你是一个爱思考的猿类,应该会很容易的发现很多疑问,比如把任务添加到线程池队列后,怎么取消和挂起呢?如何知道他执行完了呢,下面来总结一下线程池的不足:
1.线程池内的线程不支持线程的挂起、取消等操作,如想要取消线程池的任务,.net支持一种协作方式取消,使用起来也不少很方便,而且有些场景并不是很满足需求;
2.线程内的任务没有返回值,也不知道何时执行完成;
3.不支持设置线程的优先级,还包括其他类似需要对线程有更多的控制的需求也不支持;
- Mutex和lock有什么不同?一般用哪一种比较好?
Mutex是一个基于内核模式的互斥锁,支持锁的递归调用,而Lock是一个混合锁,一般建议使用Lock更好,因为lock的性能更好。