代码改变世界

C#中的线程<一>

2013-04-02 21:17  Chan08  阅读(419)  评论(0编辑  收藏  举报

平时我发现周围有些人谈及到C#中的线程,说这些线程不可控,或则很难控制,问其为什么?曰:因为是托管线程。扯淡!!!

因此不得不让我狠下心做一个C#线程相关的连载。

小生愚钝,连载中的内存有些是读书笔记有些是自己总结。如果哪里让你疑惑,可以留言我们讨论,技术贵在分享。希望看了这个系列的朋友不要再说线程不可控制,就算我没白折腾。

开始吧….

 

--------------------------------------------------------------------------------------------------------------------------------------------

这一节我们需要了解线程的基础部分,便于以后更深入的讨论。

 

1:在早期的计算机时期,操作系统没有提供线程的概念。事实上在16 位 windows ,整个系统只运行着一个执行线程,很容易阻止其他任务执行,如果有程序含有bug,造成无线循环,这会造成整个机器停止工作。

2:解决这个问题:他们解决在一个进程中运行应用程序的每个实例。进程不过是应用程序的一个实例要使用的资源的一个集合。每个进程都被赋予了一个虚拟地址空间,确保一个进程使用的代码和数据无法由另一个进程访问。,这就确保了应用程序的健壮性。但是CPU只有一个,如果一个程序进入死循环,其他任务就不能被执行,所以系统仍然可能停止响应。

为了解决这个问题,他们用线程解决了这个问题。作为一个windows概念(这里强调Windows概念的原因是这个讨论的线程概念是Windows特有得,linux的线程概念和windows就有很大的不同),线程的职责是对CPU进行虚拟化。Windows为每个进程都提供了该进程专用的线程(功能相当于一个CPU,可将线程理解成一个逻辑cpu).如果应用程序的代码进入无限循环,只会冻结与那个代码关联的进程,但其他进程(因为他们有自己的线程,也就是有自己的逻辑cpu)不会被冻结:他们会继续执行!

线程会产生空间(内存耗用)和时间(运行时的执行性能)上的开销:

1:在每个线程中,都有以下的要素:

a:线程内核对象:【包含对线程进行描述的属性,以及一个线程上下文--一个内存块】,x86CPU的计算机上运行时,线程上下文使用约700字节。对于x64和IA64CPU,上下文分别使用大约1240字节和2500字节的内存。---------存在的意义:因为在单个CPU计算机中,一次只能做一件事情,所以Windows中的所有线程必须共享物理CPU,Windows只把一个线程分配给CPU,运行一个“时间片”,一旦时间片到期,Windows就上下文切换到另一个线程,在切换过程中Windows必须将CPU寄存器中的值保留到当前正在运行的线程的内核对象内部的一个上下文结构中,还得把下一个选中的线程的上下文结构中的值加载到CPU的寄存器中,Windows大概30毫秒执行一次切换,上下文切换为净开销,也就是说,上下文切换不会换来任何内存和性能上的收益,但是通过切换实现了多线程任务并行执行和好的用户体验-------要构建高性能的应用程序和组建,就应该尽可能地避免上下文切换,这也是多线程程序编码设计的一个重要因素

b:线程环境块:【线程异常处理节点,线程本地储存数据,和一些图形使用数据】x86和x64cpu中是4kb,IA64CPU中是8kb

c:用户模式栈:【储存传给方法的局部变量和实参,以及指出线程在方法返回的时候该从什么地方开始执行的连接】Windows 为每个线程分配1MB内存---这1MB内存在本地应用程序是虚拟地址控件,并没有物理储存,但是托管代码会强制会强制让Windows立即划分出栈的物理储存,CLR团队这样设计是为了保证当系统中的可用很少时候,托管代码具有可靠性

d:内核模式栈:【出于安全考虑,应用程序不能访问内核模式栈,但是应用程序访问内核模式的函数传递参数的时候会使用内核模式栈,此时windows会从用户模式栈复制到内核模式栈,】32位大小为12KB,64位大小为:24KB

e:DLL线程连接和线程分离通知:

线程创建和线程终止都会调用那个进程中加载的所有DLL的DLLMain方法,并向该方法传递一个标志,为进程中创建和销毁的每个线程执行一些特殊的初始化或清理操作

以上就是创建线程,让它进驻系统以及最后销毁它所需要全部空间和时间开销

*C#和其他大多数托管编程语言生成的DLL没有DLLMain函数,所以托管代码DLL不会收到DLL_THREAD_ATTACH和DLL_THREAD_DETACH通知,这提升了性能。除此之外,非托管DLL可调用Win32 DisableThreadLibrearyCalls函数来决定不理会这些通知,遗憾的是许多非托管开发人员不知道这个函数,所以没有调用它

如果线程在时间片规定时间内完成了任务,它可以自主终止其时间片

在进行垃圾回收的时候,CLR必须挂起(暂停)所有线程,遍历它们栈来查找根以便对堆中的对象进行标记,再次遍历它们的栈,再恢复所有线程。所以减少线程数量对于垃圾会回收性能有帮助,每次使用一个调试器并遇到一个断点,Windows都会挂起正在调试的应用程序中的所有线程,并在单步执行或者运行应用程序时恢复所有线程。因此,你使用的线程越多,调试体验也就越差

在安装了多个CPU或者多核CPU计算机上,Windows确保单个线程不会同时在多个内核上调度,那样会引起巨大混乱

虽然线程会消耗大量内存和时间,性能上牺牲了不少,但是至今用线程来增强应用程序的可伸缩性,并且在现在多CPU或多核计算机的普及和硬件的大力发展,多线程的路会越走越远

如果只关心原始性能,那么任何计算机最优的线程数量就是那台机器的CPU数量。超过CPU了那就会发生上下文切换,有性能损失,而Windwos设计时,决定侧重可靠性和响应能力,非原始性能和速度,这样就不会造成OS“冻死”  要不没人会用Windows   我也不会  呵呵

在Windows中线程比进程廉价得多,所以开发人员不怎么创建进程,而大量创建线程,打开你的任务管理器,看看占着内存但并没让CPU干活的线程一大堆,线程虽然没进程昂贵,但是相对其他系统资源还是昂贵得多,所以能省则省了

Jeffrey  Richter 在他的《CLR via C#》一书中,用很严肃的语言批评了,以为线程廉价 而胡乱大量使用的行为...并多出用了感叹号。作为一个开发人员对线程的理性应用是对程序和用户负责

多核CPU看起十分强大, 但是也带了新的问题,比如多个内核需要并发访问其他系统资源,这些资源就成了系统总体性能的瓶颈,比如两个或多个内核要同时访问RAM,这个时候内存带宽就限制了总体性能。

为了解决了这个问题,所以现在计算机都采用了所谓的MUMA架构如图:

Image

32位Windows 支持安装32个CPU,64位支持安装64个CPU    从Windows Server R2 开始,Windows开始支持在一台机器中使用256个逻辑处理器。

如图:

Image(1)

今天CLR还不能利用处理器组,所以它创建额所有线程都在处理器组0(默认组)中运行。所以目前情况下托管程序在64位Windows上只能使用64核,在32位系统上只能使用32核

虽然今天的一个CLR线程是直接对应一个Windows线程,但是CLR团队保留了将来把它从Windows线程分离的权利,使用一个CLR逻辑线程并非一定要映射到一个物理Windows线程,据说,逻辑线程将使用比物理线程少得多的资源,所以可以在很少的物理线程上运行大量的逻辑线程,比如,CLR可以判断你的一个线程处于等待状态,重新分配那个线程去做一个不同的任务。遗憾的是要想实现这个方案,CLR团队还有很多大量工作要做,所以近期不太可能看到CLR推出这个功能

针对以上所说我们应该尽量避免P/Invoke本地Windows函数,因为这些函数对CLR线程一无所知,因为分离了,坚持使用FCL(Framework类库)中的类型,将来在性能提升后,你的代码马上就能享受这种提升

PS:如果想P/Invoke本地代码,而且代码必须使用当前物理操作系统的线程来执行,那么应该调用System.Threading.Thread的静态BeginThreadAffinity方法。线程不在需要使用物理操作系统线程运行时,可调用Thread的EndThreadAffinity方法通知CLR

一般强烈建议使用CLR线程池来调度线程,但是我们有时候必须要用到专用线程来实现我的操作,比如:

a:线程需要以非普通线程优先级运行。所有线程池线程都以普通优先级运行。虽然可以更改这个优先级,但是不建议那样做,而且在不同线程池操作之间,对优先级的更改是无法持续的。

b:需要线程表现为一个前台线程,防止应用程序在线程结束它任务之前被终止。线程池的线程始终是后台线程,如果CLR想终止进程,他们就可能被迫无法完成任务。

c:一个计算限制的任务需要长时间运行,线程池为了判断是否需要创建一个额外的线程,所采用的逻辑是比较复杂的,直接为长时间运行的任务创建一个专用线程,就可以避免这个问题

d:要启动一个线程,并可能调用Thread的Abort方法来提前终止它。

以下是Thread 的构造器原型:

View Code

Start参数表示专用线程要执行的方法,这个方法必须和ParameterizedThreadStart委托的签名匹配:

delegate void ParameterizedThreadStart(Object obj);

创建专用线程如下:

View Code

 

PS:Thread 还提供了一个获取ThreadStart委托构造器,但是气不接受任务参数,建议不是用这个构造器和委托,因为功能十分有限,不接受参数处理

是用线程的理由:

a:可以使用线程将代码同其他代码隔离

b:可以使用线程来简化编码

c:可以使用线程来实现并发执行

Date:2012.4.9

1:使用microsoft spy++ 可以查看Windows实际记录了每个线程被上下文切换到的次数【右键属性即可看到】

2:Windows 是一种抢占式多线程操作系统,不是实时操作系统,所以不能保证线程在发生某个事件后的一段时间里开始运行

3:每个线程都分配了从0~31的一个优先级。

4:饥饿:只要存在可以调度的优先级31的线程,系统就永远不会将优先级0~30的任何线程分配给cpu。。权限低级的得不到执行,就算低级线程正在执行时间片没有结束的情况系统会立即将其挂起让给高级线程

5:系统在启动时,会创建一个名为零页线程的特殊线程。这个线程的优先级定位0,是在所有线程没有执行的时候,将系统RAM所有的空闲页清零

6:下图总结了进程的优先级类和线程的相对优先级与优先级(0~31)的映射关系:

Image(2)

因为normal 是最常用的 所以系统中大多数线程优先级都是8

7:当一个线程需要长时间执行计算限制任务,一般应该降低改线程的优先级。相反如果运行非常短暂时间,再恢复为等待状态,则应该提高该线程优先级。比如windows微标键就是一个优先级很高的线程

8:线程池和本地现成创建默认为后台线程,但是显示调用Thread创建后默认为前台

9:要尽量避免使用前台线程,不然会发生进程【应用程序】始终终止不了的情况,前台线程应该用于执行确实想完成的任务

如下提现了前台线程和后台线程的区别:

View Code

 

今天就到这吧。下一节我们继续线程池的讲解。