线程

一、为什么要引入线程

引入线程的好处

(1)易于调度。

(2)提高并发性。通过线程可方便有效地实现并发性。进程可创建多个线程来执行同一程序的不同部分。

(3)开销少。创建线程比创建进程要快,所需开销很少。。

(4)利于充分发挥多处理器的功能。通过创建多线程进程(即一个进程可具有两个或更多个线程),每个线程在一个处理器上运行,从而实现应用程序的并发性,使每个处理器都得到充分运行。

由于进程是资源的拥有者,所以在创建、撤销、切换操作中需要较大的时空开销,限制了并发程度的进一步提高。为减少进程切换的开销,把进程作为资源分配单位和调度单位这两个属性分开处理,即进程作为资源分配的基本单位,但是不作为调度的基本单位(很少调度或切换),把调度执行与切换的责任交给“线程”。这样做的好处不但可以提高系统的并发度,还能适应新的对称多处理机(SMP)环境的运行,充分发挥其性能。

为了提高CPU利用率。线程给操作系统带来的创建维护和管理负担要轻。因为与线程相关的信息比较少,切换的负担意味着线程的代价或开销比较少。当处理器除一个进程并激活另一个进程时,就要发生上下文切换。为了发生上下文切换,操作系统必须启动和重新启动每个进程所需的信息。这就意味着必须保存描述进程存在状态的有关信息,在进程再次激活时,就可以从离开的地点继续执行。那么系统在进程切换时需要保存哪些信息呢

1、所需信息包括可执行程序、堆栈、以及静态与动态分配变量内存的指针。

2、寄存器组中包含执行下一条指令的指针这样的信息 。

3、重新任命进程时需要进程的状态(进程是被挂起还是被阻塞)和优先权。同时程序的I/O状态也被保存。

4、保存进程的规划信息,内存管理信息以及计数信息。

5、进程需要文件描述器和读写指针来继续使用资源。

而线程同样需要上下文。当线程被抢先时同样也会发生上下文切换。

与进程相比线程的优势如下:

1、线程不需要地址空间。线程包含在进程的地址空间中。(所以在重新任命进程时所需的线程都不需要

2、线程的上下文只包含一个堆栈、一个寄存器组和一个优先权。

3、寄存器组包含程序或指令指针以及堆栈指针。

4、线程的文本包含在他的进程的文本中。

5、进程拥有的所有资源都属于线程。所以与资源相关的所有信息不是线程上下文的部分。

6、其他信息如规划、计数等都是由进程所定义。无需包含在线程的上下文中。

线程与进程的相同之处是:都有ID,寄存器组、状态、以及优先权。线程与子进程共享父进程的资源。

他们的不同之处是:

1、线程没有自己的地址空间,如果进程创建了多个所有的线程都将包含在他的地址空间中。

2、父进程和子进程之间必须通过进程间通信机制来进行通信。而进程中的多个线程之间是通过读取和写入数据到进程变量来通信。

3、子进程对其他子进程不施加控制,而进程的线程被看做同位体(peer)并对进程的其他的线程施加控制。

二、线程中的内容

程序计数器:记录接着要执行哪一条指令

寄存器:保存线程当前的工作变量

堆栈:记录执行历史

状态

进程的内容,即在一个进程中线程共享的内容:

地址空间、全局变量、打开文件、子进程、即将发生的定时器、信号与信号处理程序、账户信息

三、线程的状态

线程从创建、运行到结束总是处于下面五个状态之一:新建状态、就绪状态、运行状态、阻塞状态及死亡状态。

1.新建状态

当用new操作符创建一个线程时。此时程序还没有开始运行线程中的代码。

2.就绪状态

一个新创建的线程并不自动开始运行,要执行线程,必须调用线程的start()方法。当线程对象调用start()方法即启动了线程,start()方法创建线程运行的系统资源,并调度线程运行run()方法。当start()方法返回后,线程就处于就绪状态。

处于就绪状态的线程并不一定立即运行run()方法,线程还必须同其他线程竞争CPU时间,只有获得CPU时间才可以运行线程。因为在单CPU的计算机系统中,不可能同时运行多个线程,一个时刻仅有一个线程处于运行状态。因此此时可能有多个线程处于就绪状态。对多个处于就绪状态的线程是由Java运行时系统的线程调度程序来调度的。

3.运行状态(running)

当线程获得CPU时间后,它才进入运行状态,真正开始执行run()方法。

4.阻塞状态(blocked)

线程运行过程中,可能由于各种原因进入阻塞状态:

①线程通过调用sleep方法进入睡眠状态;

②线程调用一个在I/O上被阻塞的操作,即该操作在输入输出操作完成之前不会返回到它的调用者;

③线程试图得到一个锁,而该锁正被其他线程持有;

④线程在等待某个触发条件;

所谓阻塞状态是正在运行的线程没有运行结束,暂时让出CPU,这时其他处于就绪状态的线程就可以获得CPU时间,进入运行状态。

阻塞的情况分三种:

(1)、等待阻塞:运行的线程执行wait()方法,该线程会释放占用的所有资源,JVM会把该线程放入“等待池”中。进入这个状态后,是不能自动唤醒的,必须依靠其他线程调用notify()或notifyAll()方法才能被唤醒,

(2)、同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入“锁池”中。

(3)、其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。

5.死亡状态(dead)

有两个原因会导致线程死亡:

①run方法正常退出而自然死亡;

②一个未捕获的异常终止了run方法而使线程猝死;

为了确定线程在当前是否存活着(就是要么是可运行的,要么是被阻塞了),需要使用isAlive方法,如果是可运行或被阻塞,这个方法返回true;如果线程仍旧是new状态且不是可运行的,或者线程死亡了,则返回false。

四、线程的堆栈

每个线程的堆栈有一帧,供各个被调用但是还没返回的过程使用。在该帧中存放相应过程的局部变量以及过程调用完成之后使用的返回地址。例:过程X调用Y,而Y又调用Z,则当Z执行时,供X,Y,Z使用的栈帧会全部存放在堆栈中。通常每个线程会调用不同的过程,从而有一个各自不同的执行历史。故,每个线程有自己独立的堆栈很重要。

五、各个线程调用:

pthread_create:创建一个新线程。新创建的线程的线程标识符作为函数值返回

pthread_exit:结束调用的线程

pthread_join:等待一个特定线程退出。要等待线程的线程标识符作为参数给出

pthread_yield:释放CPU运行另外一个线程

pthread_attr_init:创建并初始化一个线程的属性结构

pthread_attr_destroy:删除一个线程的属性结构,释放它占用的内存

六、线程包的实现

1. 在用户空间实现

把整个线程包放在用户空间。内核对线程包一无所知,从内核角度考虑,就是按正常单线程进程管理。每个进程需要有其专用的线程表,用来跟踪该进程中的线程。这些表和内核中的进程表类似,不过仅仅记录各个线程的属性,如每个线程的程序计数器,堆栈指针、寄存器和状态等,并由运行时系统管理。

优点:

  1. 用户级线程包可以在不支持线程的操作系统上实现。线程切换比陷入内核要快一个数量级。因为如果某个线程阻塞,线程表保存该线程的寄存器,然后查看表中可运行的线程,并且把新线程的保存值重新装入机器的寄存器中。只要堆栈指针和程序计数器被切换,新的线程即可立即投入运行。如果机器有一条保存所有寄存器的指令和另一条装入全部寄存器的指令,那么切换可以在几条指令内完成。另外,保存线程状态的过程和调度程序是本地过程,不需要内核调用。不需要陷阱,上下文切换,也不需要对Cache进行刷新。

  2. 允许每个进程有自己定制的调度算法。

  3. 具有较好的可扩展性:内核空间中内核线程需要一些固定表格空间和堆栈空间,如果数量过大,就会出现问题。

缺点:

  1. 阻塞系统调用:假设在没有任何击键之前,一个线程读取键盘,让该线程实际进行该系统调用是不可接受的。因为这会停止所有线程。使用线程的一个目标是,首先要允许每个线程使用阻塞调用,但是还要避免被阻塞的线程影响其他线程。有了阻塞系统调用,这个目标很难实现。

    改进:如果某个调用会阻塞,提前通知。例如在Unix中使用select允许调用者通知预期read是否会阻塞。如果有这个调用,那么read就可以被新的操作替代。首先进行select调用,然后只有在不会阻塞的情况下才进行read调用。如果read调用会被阻塞,有关的调用就不进行,代之以运行另一个线程。

  2. 如果一个线程引起页面故障(跳转到不在内存上的指令),内核由于不知道有线程存在,通常会把整个进程阻塞,直到磁盘I/O完成为止,尽管其他线程是可以运行的。

  3. 如果一个线程开始运行,那么在该进程中其他的线程就不能运行,除非第一个线程自动放弃CPU。在一个单独的进程内部,没有时钟中断,所以不可能用轮转调度。除非某个线程能够按照自己的意志进入运行时系统,否则调度程序没有任何机会。 可能的方法是让运行时系统请求每秒一次的时钟信号(中断),但是这样对程序也是生硬和无序的。不可能总是高频率地发生周期性的时钟中断。有可能扰乱系统使用的时钟,因为线程可能也需要时钟中断。

2.内核中实现

不需要运行环境,每个进程没有自己的线程表。相反,在内核中有用来记录系统中所有线程的线程表。当某个线程希望创建一个新线程或撤销一个已有线程时,它进行一个系统调用,这个系统调用通过对线程表的更新完成线程创建或撤销工作。

优点:

  1. 所有能够阻塞线程的调用都以系统调用的形式实现,当一个线程阻塞时,内核根据其选择, 可以运行同一个进程中的另一个线程(若有就绪线程)或者运行另一个进程中的线程。而在用户级线程中,运行环境始终运行自己进程中的线程,直到内核剥夺它的CPU为止。

    因为在内核中创建或撤销线程代价较大,某些系统采取的处理方式为,回收线程,当某个线程被撤销时,把它标志为不可运行的,但是内核数据结构没有受到影响,在稍后创建新线程时,就重新启动某个旧线程,节省开销。

缺点:

  1. 当一个多线程进程创建新的进程时,新进程是拥有与原进程相同数量的线程还是应该只有一个线程。取决于进程下一步要做什么。如果要启动一个新程序,一个线程或许就可以。如果要继续运行,则应该复制所有线程。实现复杂。

  2. 信号:因为信号是发给进程而不是线程的,当一个信号到达时,应该如何决定由哪一个线程处理它。

3. 混合实现

使用内核级线程,一个内核级线程可能对应多个用户级线程

4. 调度程序激活机制

模拟内核线程的功能,但是为线程包提供通常在用户空间中才能实现的更好的性能和更大的灵活性。例如:如果用户线程从事某种系统调用时是安全的,那就不应该进行专门的非阻塞调用或进行提前检查。如果线程阻塞在某个系统调用或页面故障上,只要在同一个进程中有任何就绪的线程,就应该有可能运行其他的线程。

避免了用户空间和内核空间之间的不必要转换。 例如:如果某个线程由于等待另一个线程的工作而阻塞,此时没有理由请求内核,这样就减少了内核-用户转换的开销。用户空间的运行环境可以阻塞同步的线程而另外调度一个新线程。

内核可以给每个进程安排一定数量的虚拟处理器,并且让用户空间运行环境将线程分配到处理器上。当这一机制用在多处理器中时,虚拟处理器可能成为真实CPU。分配给每个进程的虚拟处理器的初始数量是一个,但是该进程可以申请更多的处理器并且在不用的时候退回。内核也可以取回已经分配的虚拟处理器,以便把它们分给需要多处理器的进程。

思路:当内核了解到一个线程被阻塞之后,内核通知该进程的运行时环境,并且在堆栈中以参数形式传递。有问题的线程编号和所发生事件的一个描述。内核通过在一个已知的起始地址启动运行时系统,从而发出了通知,称为上行调用(upcall)。把当前线程标记为阻塞并从就绪表中取出另一个线程,设置其寄存器,然后再启动之。稍后,当内核知道原来的线程又可运行时,内核就又一次上行调用运行时系统,通知它这一事件。此时运行时系统可以立即重启原来被阻塞的线程,或者放入就绪表中稍后运行。

在某个用户线程运行的同时发生一个硬件中断时,被中断的CPU切换进核心态。如果被中断的进程对引起该中断的时间不感兴趣,比如是另一个进程的I/O完成了,那么在中断处理程序结束之后,就把被中断的线程恢复到中断之前的状态。相反,若是该进程对中断感兴趣(如该进程中某个线程所需资源到达),那么被中断的线程就不再启动,代之为挂起被中断的线程。而运行时系统则启动对应的虚拟CPU,此时被中断线程的状态保存在堆栈中。随后,运行时系统决定在该CPU上调度哪个线程:被中断线程,新就绪线程,或第三种选择。

在层次系统结构中,通常n层提供n+1层可调用的特定服务,但是n层不能调用n+1层中的过程,但是上行调用不遵守这个原理。

5. 弹出式线程

一个消息的到达导致系统创建一个处理该消息的线程(传统方法为将进程或线程阻塞在receive系统调用上,等待消息到来。当消息到达是,该系统调用接收消息,并打开消息检查其内容,然后进行处理)

优点:

        1、因为是新线程,没有必须存储的寄存器、堆栈等内容。每个线程从全新开始,可以快速创建。

缺点:

        1、需要提前计划,哪个进程中的线程线运行。如果系统支持在内核上下文种运行线程,线程就有可能在那里运行。

6.单线程程序多线程化

1、对于全局变量:

            1、为每个线程赋予其私有的全局变量(全局变量的私有副本)。避免冲突。但是多数程序设计语言只具有表示局部变量和全局变量的方式,而没有中                间形式。所以想使用这种方法,有可能为全局变量分配一块内存,并将它传送给线程中的每个过程作为额外的参数。

             2、引入库过程,将特殊存储区上替全局变量分配存储空间。无论该存储空间分配在何处,只有调用线程才可访问其全局变量。如果另一个线程创建了同名的全局变量,由于在不同的存储单元上,所以不会与已有变量冲突。但是有许多库过程不是可重入的。(可重入的意思为程序在任意时刻被中断然后操作系统调度执行另外一段代码,这段代码又调用了该程序不会出错,即子程序正在运行时,执行线程可以再次进入并执行它,仍然获得符合设计时预期的结。不可重入即意味着,不同任务调用这个过程时可能修改其他任务调用这个过程的数据,从而导致不可预料的后果)

            3、给每个过程提供一个包装器,该包装器设置一个二进制位从而标志某个库处于使用中。在先前调用还没有完成之前,任何试图使用该库的其他进程都会被阻塞。

2、信号:

            1、一些信号是线程专用的,但是另外一些并不是,当线程完全在用户空间实现时,内核根本不知道有线程存在,所以很难将信号发送给正确线程。

            2、一些信号不是线程专用,那么哪个线程该捕捉它。指定线程?所有线程?弹出式线程?如果某个线程修改了信号处理程序,是否需要通知其他线程。如果一个线程想捕捉一个特定的信号,而另一个线程却想用这个信号终止进程,会发生什么。

3、堆栈的管理:当一个进程堆栈溢出时,内核只是自动为该进程提供更多堆栈。当一个进程有多个线程时,就必须有多个堆栈。内核不了解所有堆栈,无法为他们提供增长,直到堆栈出错。内核可能没有意识到内存错误是和某个线程栈的增长有关系的。

八、进程和线程的联系和区别

进程和线程的关系

(1)一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程。线程是操作系统可识别的最小执行和调度单位。

(2)资源分配给进程,同一进程的所有线程共享该进程的所有资源。 同一进程中的多个线程共享代码段(代码和常量),数据段(全局变量和静态变        量),扩展段(堆存储)。但是每个线程拥有自己的栈段,栈段又叫运行时段,用来存放所有局部变量和临时变量。

(3)处理机分给线程,即真正在处理机上运行的是线程。

(4)线程在执行过程中,需要协作同步。不同进程的线程间要利用消息通信的办法实现同步。

从调度、并发性、 系统开销、拥有资源等方面,来比较线程与进程

1.调度

在传统的操作系统中,拥有资源的基本单位和独立调度、分派的基本单位都是进程。而在引入线程的操作系统中,则把线程作为调度和分派的基本单位。而把进程作 为资源拥有的基本单位,使传统进程的两个属性分开,线程便能轻装运行,从而可显著地提高系统的并发程度。在同一进程中,线程的切换不会引起进程的切换,在由一个进程中的线程切换到另一个进程中的线程时,将会引起进程的切换。

2.并发性

在引入线程的操作系统中,不仅进程之间可以并发执行,而且在一个进程中的多个线程之间,亦可并发执行,因而使操作系统具有更好的并发性,从而能更有效地使用系统资源和提高系统吞吐量。例如,在一个未引入线程的单CPU操作系统中,若仅设置一个文件服务进程,当它由于某种原因而被阻塞时,便没有其它的文件服务进程来提供服务。在引入了线程的操作系统中,可以在一个文件服务进程中,设置多个服务线程,当第一个线程等待时,文件服务进程中的第二个线程可以继续运 行;当第二个线程阻塞时,第三个线程可以继续执行,从而显著地提高了文件服务的质量以及系统吞吐量。

3.拥有资源

不论是传统的操作系统,还是设有线程的操作系统,进程都是拥有资源的一个独立单位,它可以拥有自己的资源。一般地说,线程自己不拥有系统资源(也有一点必 不可少的资源),但它可以访问其隶属进程的资源。亦即,一个进程的代码段、数据段以及系统资源,如已打开的文件、I/O设备等,可供问一进程的其它所有线程共享。

4.系统开销

由于在创建或撤消进程时,系统都要为之分配或回收资源,如内存空间、I/o设备等。因此,操作系统所付出的开销将显著地大于在创建或撤消线程时的开销。类似地,在进行进程切换时,涉及到整个当前进程CPU环境的保存以及新被调度运行的进程的CPU环境的设置。而线程切换只须保存和设置少量寄存器的内容,并不涉及存储器管理方面的操作。可见,进程切换的开销也远大于线程切换的开销。此外,由于同一进程中的多个线程具有相同的地址空间,致使它们之间的同步和通信的实现,也变得比较容易。在有的系统中,线程的切换、同步和通信都无须操作系统内核的干预。

5.独立性:
在同一进程中的不同线程之间的独立性要比不同进程之间的独立性低得多。这是因为
为防止进程之间彼此干扰和破坏,每个进程都拥有一个独立的地址空间和其它资源,除了共享全局变量外,不允许其它进程的访问。但是同一进程中的不同线程往往是为了提高并发性以及进行相互之间的合作而创建的,它们共享进程的内存地址空间和资源,如每个线程都可以访问它们所属进程地址空间中的所有地址,如一个线程的堆栈可以被其它线程读、写,甚至完全清除。
6、支持多处理机系统:
在多处理机系统中,对于传统的进程,即单线程进程,不管有多少处理机,该进程只能运行在一个处理机上。但对于多线程进程,就可以将一个进程中的多个线程分配到多个处理机上,使它们并行执行,这无疑将加速进程的完成。因此,现代处理机OS都无一例外地引入了多线程。




评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值