属于自己的《程序员的自我修养》之温故而知新

来源:互联网 发布:斯坦福英语软件下载 编辑:程序博客网 时间:2024/05/19 09:17
    最近看了一本由俞甲子和石凡写的《程序员的自我修养》,这本书讲的通俗易懂,非常适合初学者去学习。而我便把最精彩的部分呈现出来。


第一章:温故而知新


    第一章主要是巩固和总结计算机软硬件体系里面几个重要的概念。


万变不离其宗


       对于程序开发人员来说,有三个部件最为关键,它们分别为:中央处理器CPU内存I/O控制芯片,这三个部件。

      早期的计算机没有很复杂的图形功能,CPU的核心频率也不高,跟内存的频率一样,它们都是直接连接在同一个总线上的。为了协调I/O设备与总线之间的速度,也为了能够让CPU能与I/O设备进行通讯,一般每个设备都会有一个相应的I/O控制器。

       北桥芯片:随着3D游戏和多媒体的发展,使得图形芯片需要跟CPU和内存之间大量交换数据,慢速的I/O总线已经无法满足图形设备的巨大需求。为了协调CPU,内存和高速的图形设备,人们专门设计了了一个高速的北桥芯片,以便塔门之间能够高速的交换数据。

       南桥芯片:由于北桥的运行的速度非常高,所有相对低速的设备如果全部直接连接在北桥上,北桥既需处理高速设备,又需处理低速设备,设计就会相当的复杂。于是人们又专门设计了处理低速设备的南桥芯片。键盘,USB,磁盘,鼠标等设备都连接在南桥上,由南桥将他们汇总后连接到北桥上。

SMP与多核


       在过去的50年里,CPU的频率从几十kHz到现在的4GkHZ,整整提高了数十万倍。但是CPU的频率从2004年开始再也没有发生质的提高,原因是人们制造CPU的工艺方面已经达到了物理极限。

       在频率上短期内已经没有提高的余地了,于是人们开始想办法从另外的角度来提高CPU的速度,就是增加CPU的数量。其中最常见的一种形式就是对称多处理器(SMP,Sysmetrical Multi-Processing)。简单的讲就是每个CPU在系统中所处的地位和所发挥的功能都是一样的,是相互对称的。理论上讲,增加CPU的数量就可以提高运算速度,并且理想情况下,速度的提高与CPU的数量成正比。但是实际上并非如此,因为我们的程序并不是都能分解成若干个完全不相关子问题。就如一个女人可以花10个月生出一个孩子,但是10个女人不能在一个人就生出一个孩子一样。当然很多时候多处理器是非常有用的。

       而在个人电脑中,使用多个处理器则是比较奢侈的行为,于是处理器的厂商开始考虑将多个处理器“合并在一起打包出售”,这些“被打包”的处理器之间共享比较昂贵的缓存部件,只保留多个核心,并且以一个处理器的外包装进行出售,售价只是比单核处理器贵了一点,这就是多核处理器(Multi-core Processor)的基本想法。多核处理器实际上就是SMP的简化版,只是多核和SMP在缓存共享等方面有细微的差别,除非想把CPU的每一滴油水都榨干,否则可以把多核和SMP看成同一个概念。

站的高,望的远

      传统意义上一般将用于管理计算机本身的软件称为系统软件,以区分普通的应用程序。系统软件可以分为两块:一块是平台性的,比如操作系统内核,驱动程序,运行库等等;另外一块是用于程序开发的,比如编译器,汇编器,链接器等开发工具和开发库。

      计算机系软件体系结构采用一种的结果,有人说过一句名言:“计算机科学领域的任何问题都可以通过增加一个间接的中间层来解决”
“Any problem in computer science can be solved by another layer of indirection."

       每个层次之间都需要互相通讯,既然需要通讯就必须有一个通信的协议,我们一般称为接口(Interface)。接口的下面那层是接口的提供者,由它定义接口;接口上面那层是接口的使用者,它使用该接口来实现所需要的功能。

       我们的软件体系中,位于最上层的是应用程序,比如我们平时用到的网络浏览器,Email客户端,多媒体播放器等。从整个层次结果上来看,开发工具应用程序都属于同一个层次的,因为他们都使用同一个接口,那就是操作系统应用程序编程接口(Application Programming Interface)。应用程序的提供者是运行库,什么样的运行库提供什么样的API,比如Linux下的Glibc库提供POSIX的API;Windows的运行库提供Windows API,最常见的32位Windows提供的API又称为Win32。

       运行库使用操作系统提供的系统调用接口(System call Interface),系统调用接口在实现中往往以软件中断(Software Interrupt)的方式提供,比如Linux使用0x80号中断作为系统调用接口,Windows使用0x2E号中断作为系统调用接口。

       操作系统的内核层对于硬件层来说是硬件接口使用者,而硬件是接口的定义者,硬件的接口定义了操作系统内核,具体来讲就是驱动程序如何操作硬件,如何与硬件进行通信。这种接口往往叫做硬件规格(Hardware Specification),硬件的生产厂家负责提供硬件规格,操作系统和驱动程序的开发者通过阅读硬件规格文档所规定的各种硬件编程接口标准来编写操作系统和驱动程序。


操作系统做什么


       操作系统的一个功能是提供抽象的接口,另外一个主要功能是管理硬件资源。

不要让CPU打盹


       在计算机发展早期,CPU资源十分昂贵,如果一个CPU只能运行一个程序,那么当程序读写磁盘(当时可能是磁带)时,CPU就空闲下来了,这在当时就是暴殄天物。于是人们很快编写了一个监控程序,当某个程序不再使用CPU时,监控程序就把另外在等待CPU资源的程序启动,是的CPU充分的利用起来。这种被称为多道程序(Multiprogramming)的方法看似很原始,但是大大提高了CPU的利用率。

       但是这种原始的多道程序设计存在最大的问题是程序之间的调度策略太粗糙。对于多道程序来说,程序之间不分轻重缓急,如果有些程序急需使用CPU来完成一些任务(比如用户交互的任务),那么很有可能很长时间后才有机会分配到CPU。

       经过稍微的改进,程序运行模式改变成一种协作的模式,即每个程序运行一段时间以后都会主动让出CPU给其他的程序,使得一段时间内每个程序都有机会运行一小段时间。这种程序协作模式叫做分时系统(Time-Sharing System),这时候的监控程序已经比多道程序要复杂的多了,完整的操作系统雏形已经逐渐形成。如果一个程序在进行一个很耗时的计算,一直霸占着CPU不放,那么操作系统也没有办法,其他程序都只能等着,整个系统就好像死机了一样。

       更先进的操作系统模式:多任务(Multi-tasking)系统,操作系统接管了所有的硬件资源,并且运行在一个受硬件保护级别。所有的应用程序都已进程(Process)的方式运行在比操作系统权限更低的级别,每个进程有自己独立的地址空间,使得进程之间的地址空间相互隔离。CPU由操作系统统一进行分配,每个进程根据优先级的高低都会有机会得到CPU,但是,如果运行超出了一定的时间,操作系统就会暂停该进程,将CPU的资源分配给其他等待运行的进程。这种CPU的分配方式称为抢占式(Preemptive),操作系统可以强制剥夺CPU资源并且分配给他认为目前最需要的进程。如果操作系统分配给每个进程的时间非常的短,即CPU在多个进程间快速的切换,从而造成了很多进程都在同时运行的假象。目前几乎所有的操作系统都在使用这种方式。

设备驱动


       当成熟的操作系统出现以后,硬件逐渐被抽象成一系列概念,应用程序开发者不要要直接跟硬件打交道。在UNIX中,硬件设备的访问形式跟访问普通的文件形式一样;在Windows系统中,图像硬件被抽象成了GDI,声音和多媒体设备被抽象成了DirectX对象;磁盘被抽象成了普通文件系统,等等。程序员逐渐从硬件细节中解放出来,可以更多的关注应用程序本身的开发。这些繁琐的硬件细节全都交给了操作系统,具体的讲是操作系统中的硬件驱动(Device Drive)程序来完成。驱动程序可以看作是操作系统的一部分,它往往跟操作系统内核一起运行在特权级,但它又与操作系统内核之间有一定的独立性,使得驱动程序有较好的灵活性。操作系统开发者不可能为每个硬件开发一个驱动程序,所有驱动程序开发工作一般由硬件生产厂商完成。操作系统开发者为硬件生产厂商提供了一系列接口和框架,凡是按照这个接口和框架开发的驱动程序都可以在该操作系统上使用。

       穿插一个关于硬盘的结果介绍,硬盘基本存储单位扇区(Sector),每个扇区一般为512字节。一个硬盘往往有多个盘片,每个盘片两面每面按照同心圆划分为若干个磁道每个磁道划分为若干个扇区。比如一个硬盘有2个盘片,每个盘片面分65536磁道,每个磁道分1024个扇区,那么硬盘的容量就是2*2*65536*1024*512=137438953472字节(128GB)。但是我们可以想象,每个盘面上同心圆的周长不一样,如果按照每个磁道都拥有相同数量的扇区,那么靠近盘面外围的磁道密度肯定比内圈更加的稀疏,这样是比较浪费空间。如果不同的磁道扇区数又不一样,计算起来就什么麻烦。为了屏蔽这些复杂的硬件细节,现代的硬盘普遍使用一种叫做LAB(Logic Block Address)的方式,即整个硬盘中所有的扇区从0开始编号,一直到最后一个扇区,这个扇区编号叫做逻辑扇区编号。逻辑扇区号抛弃了所有复杂的磁道,盘面之类的概念。当我们给出一个逻辑的扇区号时,硬盘的电子设备会将其转换成实际的盘面,磁道等这些位置。

       那么当我们在Linux操作系统中,要读取这个文件的前4096个字节时,我们会使用一个read的系统调用来实现。文件系统收到read请求之后,判断文件的前4096个字节位于磁盘的1000号逻辑扇区到1007号逻辑扇区。然后文件系统就向硬盘驱动发送一个读取逻辑扇区为1000号开始的8个扇区的请求磁盘驱动程序收到这个请求以后就向硬盘发出硬盘命令。向硬盘发送I/O命令的方式有很多种,其中最为常见的一种就是通过读写I/O端口寄存器来实现。

内存不够怎么办


       进程的总体目标是希望每个进程从逻辑上来看都可以独占计算机资源。操作系统的多任务功能使得CPU能够在多个进程之间很好的共享,操作系统的I/O抽象模型也很好地实现了I/O设备的共享与抽象,那么接下来就是内存分配的问题了。


      在早期的计算机中,程序是直接运行物理内存上的,也就是说,程序在运行时所访问的地址都是物理地址

      我们要解决的问题是,如何将计算机上有限的物理内存分配给多个程序使用。

      假设我们的计算机有128M内存,程序A运行需要10M,程序B运行需要100M,程序C需要20M,如果我们需要实现同时运行程序A和B,那么比较直接的做法是将内存的前10M分配给程序A,10~110M分配给B。但是这种简单的内存分配策略问题很多。

1.地址空间不隔离:所有程序都直接访问物理地址,程序使用的地址空间不是相互隔离的。恶意程序可以很容易改写其他程序的内存数据;有些非恶意的,但是有臭虫的程序可能不小心修改了其他程序的数据,就会使其他程序也崩溃。

2.内存使用效率低:由于没有有效的内存管理机制,通常要一个程序执行时,监控程序就将整个程序装入内存中然后开始执行。如果我们突然要运行程序C,那么这时内存空间其实已经不够的了,我们可以将其他程序的数据暂时写到磁盘里面,等到要用的时候再读回来。可以看到整个过程中有大量的数据在换入换出,导致效率低下。

3.程序运行的地址不确定:因为程序每次装入运行时,我们都需要给它从内存中分配一块足够大的空闲区域,这个空闲区域是不确定的。这给程序的编写造成了一定的麻烦,因为在程序编写时,它访问数据和指令跳转时的目标地址很多都是固定的,这就涉及程序的重定位问题。

       解决这几个问题的思路就是使用我们前文提到过的法宝:增加中间层,即使用一种间接的地址访问方法。我们把程序给出的地址看作是一种虚拟地址(Virtual Address),然后通过某些映射的方法,将这个虚拟地址转换成实际的物理地址。这样就能够妥善的控制这个虚拟地址到物理地址的映射过程,就可以保证任意一个程序所能够访问的物理内存区域跟另外一个程序互相不重叠,以达到地址空间隔离的效果。

关于隔离


        所谓的地址空间你可以把它想象成一个很大的数组,每个数组的元素是一个字节,而这个数组的大小由地址空间的地址长度决定,比如32位的地址空间的大小为2^32=4294967296字节,即4GB,地址空间有效的地址为0~4294967295,用十六进制表示就是0x00000000~0xFFFFFFFF。地址空间分两种:虚拟地址空间(Virtual Address)和物理地址空间(Physical Address Space)。

       物理地址空间:是真实存在的,存在于计算机中,而且对于一个计算机来说就只有唯一的一个。假如你的计算机使用的是Intel的Pentium4的处理器,那么它是32位的机器,即计算机地址线有32条,那么物理地址空间就有4G.但是你的计算机上只装了512M的内存,那么其物理地址的真正有效部分至于0x00000000~0x1FFFFFFF,其他部分是无效的(实际上还有一些外部I/O设备映射到物理空间的,也是有效的,但是我们暂时忽略)。

       虚拟地址空间:是指虚拟的,人为想象出来的地址空间,其实它并不存在,每个进程都有自己独立的虚拟空间,而且每个进程只能访问自己的地址空间。

分段(Segmentation)


       分段:基本思路是把一段与程序所需要的内存空间大小的虚拟空间映射到某个地址空间。比如程序A需要10MB的内存,那么我们假设有一个地址从0x00000000到0x00A00000的10MB大小的一个假想空间,也就是虚拟空间,然后我们从实际的物理内存中分配一个相同大小的物理地址,假设是物理地址0x00100000开始到0x00B00000结束的一块空间。然后我们把这两块相同大小的地址空间一 一映射,即虚拟空间中的每个字节相对应于物理空间中的每个字节。这个映射过程软件设置,比如操作系统设置这个映射函数实际地址转换硬件来完成。

       分段的方法基本解决地址空间不隔离程序运行的地址不确定的问题,但是内存使用效率低下的问题还是没有解决。分段对内存区域的映射还是按照程序为单位,如果内存不足,被换入换出到磁盘的都是整个程序,这样势必会造成大量的磁盘访问操作,从而严重影响速度,这种方法还是显得比较粗糙,粒度较大。事实上,根据程度的局部性原理,当一个程序在运行时,在某个时间段内,它只是频繁地用到一小部分数据,也就是说,程序的很多数据其实在一个时间段内都不会被用到。人们想到了更小粒度的内存分割和映射的方法,使得程序的局部性原理得到充分的利用。这种方法就是分页

分页(Paging)


       分页的基本方法是把地址空间人为的等分成固定大小,每页的大小由硬件决定,或硬件支持多种大小的页,由操作系统选择决定页的大小。但是在同一个时刻只能选择一种大小,所以对于整个程序来说,页的大小就是固定的。程序运行时,只需要把常用的数据代码页装载到内存中,部分虚拟页面映射物理页面。当要用到的虚拟页不在内存中时,硬件会捕获到这个消息,就是所谓的页错误(Page Fault),然后操作系统接管进程,把需要的虚拟页面从磁盘调入内存中。

       虚拟存储的实现需要依靠硬件的支持,对于不同的CPU来说是不同的,。但是几乎所有的硬件都采用一个叫MMU(Memory Management Unit)的部件来进行页映射。MMU一般都集成在CPU内部。

CPU--------Virtual Address--------->MMU-------Physical Address------>Physical Memory
                                     虚拟地址到物理地址的转换

众人拾柴火焰高


线程基础


什么是线程

       线程(Thread),有时候被称为轻量级进程(Lightweight Process,LWP),是程序执行流的最小单元。一个标准的线程由线程ID,当前指令指针(PC),寄存器集合和堆栈组成。通常意义上一个进程由一到多个线程组成,各个线程之间共享程序的内存空间(包括代码段,数据段,堆等)及一些进程级的资源(如打开文件和信号)。

使用多线程原因有如下几点:
1.某个操作可能会陷入长时间等待,等待的线程会进入休眠状态,无法继续执行。多线程执行可以有效利用等待时间。
2.某个操作(常常是计算)会消耗大量的时间,如果只有一个线程,程序和用户之间的交互会中断。多个线程可以让一个线程负责交互,另一个线程负责计算。
3.程序逻辑本身就要求并发操作。如一个多端下载软件。
4.多CPU或多核计算机,本身具备同时执行多个线程的能力,因此单线程程序无法全面发挥计算机的全部计算能力。
5.相对于多进程应用,多线程在数据共享方面效率要高很多。


线程的访问权限


       线程的访问非常的自由,它可以访问进程内存里的所有数据,甚至包括其他线程的堆栈(如果知道其他线程的堆栈地址,不过这种情况很少见)。但实际中线程也会有自己的私有存储空间,包括一下几个方面:
1.栈(尽管并非完全无法被其他线程访问,但一般情况下仍然可以认为是私有的数据)
2.线程局部存储(Thread Local Storage,TLS)。
3.寄存器(包括PC寄存器),寄存器是执行流的基本数据,因此为线程私有。

线程调度与优先级


       不论是在多处理器的计算机上还是在单处理器的计算机上,线程总是“并发”执行的。当线程的数量小于等于处理器数量是(并且操作系统支持多处理器),线程的并发是真正并发,不同的线程运行在不同的处理器上,彼此之间互不相干。但对于线程数量大于处理器数量的情况下,线程的并发会受到一些阻碍,因为此时至少有一个处理器会运行多个进程。

       在单处理器对应多线程的情况下,并发是一种模拟出来的状态。操作系统会让这些多线程程序轮流执行,每次仅执行一小段时间,这样每个线程就“看起来”在同时执行。这样的一个不断在处理器上切换的线程的行为称之为线程调度(Thread Schedule)。在线程调度中,线程通常拥有至少三种状态,分别是:
1.运行(Running):此时线程正在执行
2.就绪(Ready):此时线程可以立刻运行,但CPU已经被占用。
3.等待(Waiting):此时线程正在等待某一事件(通常是I/O或同步)发生,,无法执行。

       处于运行中线程拥有一段可以执行的时间,这段时间称为时间片(Time Slice),当时间片用尽的时候,该进程就会进入就绪状态。如果时间片用尽之前进程就开始等待某事件,那么它将进入等待状态。每当一个线程离开运行状态时,调度程序就会选择一个其他的就绪线程继续执行。在一个处于等待状态的线程所等待事件发生之后,该线程就会进入就绪状态。

       现在主流的调度方式尽管各不相同,但都带有优先级调度(Priority Scheduel)和轮转法(Round Robin)的痕迹。所谓轮转法,即是让各个线程轮流执行一小段时间的方法。而优先级调度则决定了线程按照什么顺序轮流执行。在具有优先级的线程调度系统中,线程都拥有各自的线程优先级(Thread Priority)。在Windows中,可以通过使用:
BOOL WINAPI SetThreadPriority(HANDLE hThread,int nPrioruty);来设置线程的优先级。

       线程的优先级不仅可以由用户手动设置,系统还会根据不同线程的表现来自动调整优先级。我们一般把频繁等待的线程称之为IO密集型线程(IO Bound Thread),而把很少等待的线程称之为CPU密集型(CPU Bound Thread)。IO密集型线程总是比CPU密集型线程更容易得到优先级的提升

       在优先级调度下,存在一种饿死(Starvation)的现象,一个线程被饿死,是说它的优先级比较低,在它执行之前,总是有较高优先级的线程要执行,因此这个低优先级线程始终无法执行。为了避免饿死现象,调度系统常常会逐步提升那些等待了过长时间的得不到执行的线程的优先级。

总结一下,在优先级调度的环境下,线程的优先级改变一般有三种方式:
1.用户指定优先级
2.根据进入等待状态的频繁程度提升或降低优先级
3.长时间得不到执行而被提升优先级

可抢占线程和不可抢占线程


       我们之前讨论的线程调度有一个特点,那就是线程在用尽时间片之后会被强制剥夺继续执行的权利,而进入就绪状态,这个过程叫做抢占(Preemption)。不可抢占线程:线程必须主动进入就绪状态,而不是靠时间片用尽。在不可抢占线程中,线程主动放弃执行无非两种情况:
1.当线程试图等待某事件时(I/O等)。
2.线程主动放弃时间片。
在不可抢占线程执行的时候有一个显著的特点,那就是线程调度的时机是确定的,这样可以避免一些以为抢占式程序里调度时机不确定而产生的问题,即使如此,非抢占式线程在今日已经非常少见。

       在Windows API中,可以使用明确的API:CreateProcessCreateThread来创建进程线程,并且有一系列的API来操纵它们。但Linux将所有的执行实体(无论是线程还是进程)都称为任务(Task),每个任务概念上都类似于一个单线程的进程,具有内存空间,执行实体,文件资源等。不过Linux下不同的任务之间可以选择共享内存空间,因而实际意义上,共享了同一个内存空间的多个任务构成了一个进程,这些任务也就成了这个进程里的线程。


线程安全


       多线程程序处于一个多变的环境当中,可访问的全局变量和堆数据随时都可能被其他的线程改变。因此多线程程序在并发时数据的一致性变得非常重要。

竞争与原子操作


       我们把单指令的操作称为原子的(Atomic),因为无论如何,单指令的执行是不会被打断的。在Windows理,有一套API专门进行一些原子操作,这些API称为INterlocked API。

       Windows API                                                 作用
InterlockedExchange                                 原子地交换两个值
InterlockedDecrement                               原子地减少一个值
InterlockedIncrement                                原子地增加一个值
InterlockedXor                                          原子地进行异或操作
使用这些函数时,Windows将保证是原子操作的,因此可以不用担心出现问题。遗憾的是,它们仅适用于比较简单特定的场合。在复杂的场合下,,原子操作指令就力不从心了。在这里我们需要更加通用的手段:

同步与锁


       为了避免多个线程同时读写同一个数据而产生不可预料的后果,我们要将各个线程对同一个数据的访问同步(Synchronization),所谓同步,即指在一个线程访问数据未结束的时候,其他线程不得对同一个数据进行访问。如此,对数据的访问被原子化了。

       同步最常见方法(Lock)。锁是一种非强制机制,每一个线程在访问数据或资源之前首先试图获取(Acquire)锁,并在访问结束之后释放(Release)锁。在锁已经被占用的时候试图获取锁时,线程会等待,直达锁重新可用。

二元信号量(Binary Semaphore)是最简单,它只有两种状态:占用与非占用。它适合只能被唯一 一个线程独占访问的资源。

对于允许多个线程并发访问资源,多元信号量简称信号量(Semaphore)是一个很好的选择。一个初始值为N的信号量允许N个线程并发访问。线程访问资源的时候首先获取信号量,进行如下操作:
1.将信号量的值减1。
2.如果信号量的值小于0,则进入等待状态,否则继续执行。
       访问完资源之后,线程释放信号量,进行如下操作:
1.将信号量加1。
2.如果信号量的值小于1,唤醒一个等待中的线程。

       互斥量(Mutex)和二元信号量很类似,资源仅同时允许访问一个线程访问,但和信号量不同的是,信号量在整个系统可以被任意线程获取并释放,也就是说,同一个信号量可以被系统中的一个线程获取之后由另外一个线程释放。而互斥量则要求哪个线程获取了互斥量,哪个线程就要负责释放这个锁。

       临界区(Critical Section)是比互斥量更加严格的同步手段。在术语中,把临界区的锁的获取称为进入临界区,而把锁的释放称为离开临界区。临界区的作用范围仅限于本进程,其他的进程无法获取该锁。

       读写锁(Read-Write Lock)致力于一种更加特定的场合的同步。
     
       条件变量(Condition Variable)作为一种同步手段,作用类似于一个栅栏。使用条件变量可以让许多线程一起等待某个事件的发生,当事件发生时(条件变量被唤醒),所有的线程可以一起恢复执行。

可重入(Reentrant)与线程安全

       一个函数被重入,表示这个函数没有执行完成,由于外部因素或内部调用,又一次进入该函数执行。一个函数被重入,只有两种情况:
(1)多个线程同时执行这个函数
(2)函数自身(可能是经过多层调用之后)调用自身。
       一个函数被称为可重入的,表明该函数被重入之后不会产生任何不良后果。一个函数成为可重入,必须有如下几个特点:
(1)不使用任何(局部)静态或全局的非const变量。
(2)不返回任何(局部)静态或全局的非const变量的指针。
(3)仅依赖于调用方提供的参数。
(4)不依赖任何单个资源的锁(mutex等)。
(5)不调用任何不可重入的函数。
        可重入是并发安全的强力保障,一个可重入的函数可以在多线程环境下放心使用。

过度优化


       线程安全是一个非常烫手的山芋,因为即使合理的使用了锁,也不一定能保证线程安全,这是源于落后的编译器技术已经无法满足日益增长的并发需求。在几十年前,CPU就发展出了动态调度,在执行程序的时候为了提高效率有可能交换指令的顺序。同样,编译器在进行优化的时候,也可能为了效率而交换毫不相干的两条相邻指令

       我们可以使用volatile关键字视图阻止过度优化,volatile基本可以做到两件事:
(1)阻止编译器为了提高速度将一个变量缓存到寄存器内而不写回。
(2)阻止编译器调整操作volatile变量的指令顺序。

       可见volatile可以完美地解决第一个问题,但是还是没有能够解决阻止编译调整顺序,也无法阻止CPU动态调度换序
  
       通常情况下是调用CPU提供的一条指令barrier。一条barrier指令会阻止CPU将该指令之前指令交换到barrier之后,反之亦然。

多线程内部情况

三种线程模型


      用户实际使用的线程并不是内核线程而是存在于用户态的用户线程

1. 一对一模型

       一个用户使用的线程就唯一对应一个内核使用的线程(但反过来不一定,一个内核理的线程在用户态不一定有对应的线程存在)。
      优点:实现真正的并发,某个线程阻塞时,不会影响到其他线程的执行。
      缺点:(1)由于许多操作系统限制了内核线程的数量,因此会让用户的线程数量受到限制
                 (2)许多操作系统内核线程调度时,上下文切换的开销较大,导致用户线程的执行效率下降。

2.多对一模型
       多个用户线程映射到一个内核线程上,线程之间的切换由用户态的代码来进行,因此多对一模型的线程相对一对一来说,线程切换要快速很多。
      缺点:如果其中一个线程阻塞,那么所有的线程将无法执行,因为此时内核里的线程也随之阻塞了。另外在多处理器系统上,处理器的增加对多对一模型的线程性能也不会有明显的帮助
     优点:高效的上下文切换和几乎无限制的线程数量

3.多对多模型
      结合辆一对一和多对一模型的特点,将多个用户线程映射到少数但不止一个内核线程上。在多对多模型中,一个用户线程阻塞不会使得所有的用户线程阻塞,因为此时还有别的线程可以被调度来执行。另外,多对多模型对用户线程的数量也没有什么限制,在多处理器系统上,也能得到一定的性能提升,不过提升幅度不如一对一模型高。



1 0
原创粉丝点击