操作系统为了程序的并发执行引入了进程的概念,提高了资源的利用率以及吞吐量。
在20世纪 60年代人们提出了进程的概念后,在OS中一直都是以进程作为能拥有资源和独立运行的基本单位的。
直到 20 世纪 80 年代中期,人们又提出了比进程更小的能独立运行的基本单位——线程(Threads)
试图用它来提高系统内程序并发执行的程度,从而可进一步提高系统的吞吐量。
简言之,进程的概念,使之能够并发执行多道程序,线程的概念让你更好地并发执行程序,一个是能不能的问题,一个是更好的问题。
线程与进程对比
线程概念的发展
进程概念提出的目的就是为了多道程序并发执行,并发过程中必然意味着不断地进程调度任务切换,但是他又是资源分配的独立单位,也就是说他要背着资源来回跑。
举个例子:
办公室内,每个人都有一台电脑,电脑就是资源
然后大家经常需要不断地变换座位位置(比如大家都是哪里需要去哪里,客服缺人了,销售就顶一个过去)
每个人都抱着自己的电脑来回的换位置方便?还是大家只是人员走动,电脑就使用那个位置的电脑方便?
进程也是有些类似的道理,你带着这么多资源来回切换调度,必然会带来更多的时&&空开销。
所以创建了线程的概念,程序运行时所需的资源和程序的调度进行解耦
进程仍旧负责资源的独立分配,但是线程作为调度运行的独立单位,仅仅携带自身运行的必备的一丁点资源。
对比
线程具有许多传统进程所具有的特征,所以又称为轻型进程(Light-Weight Process)或进程元
相应地把传统进程称为重型进程(Heavy-Weight Process),传统进程相当于只有一个线程的任务。
在引入了线程的操作系统中,通常一个进程都拥有若干个线程,至少也有一个线程。
并发性
传统的OS系统,进程之间可以并发执行,引入线程概念的OS,不仅仅进程间可以并发执行,一个进程中的线程也可以并发执行,不同进程中的线程也可以并发执行
独立性
同一进程中的多个线程独立性比不同进程间的独立性差很多。
每个进程都是独立的地址空间和资源,同一进程下多线程他们共享进程下的资源,而且通常他们往往是用来相互合作的,每个线程都可以访问所在进程的所有地址空间,比如一个线程打开的文件,可以被其他线程读写。
调度性
传统OS,进程作为资源分配和调度分派的基本单位,进程是可以独立运行的基本单位,不过进程调度切换时空开销大
引入线程的OS,线程是运行调度和分派的基本单位,线程才是独立运行的基本单位,线程切换时,仅仅需要保存和设置少量寄存器内容,代价远远小于进程切换,不过需要注意是同一个进程内线程切换不会进程切换,但是不同进程中的线程进程切换,仍旧会导致进程切换。
拥有资源
进程拥有资源,并且作为系统中拥有资源的独立基本单位。
线程自身不拥有系统资源,仅仅拥有一点必不可少的,独立运行需要的资源,比如线程中的TCB。
除了自身的丁点儿资源外,共享所属进程的资源,同一个进程下所有线程,拥有相同的地址空间。
多处理器支持
传统进程(或者说单线程进程)只能运行于一个处理机上,不管有多少个处理机;
但是对于多线程进程,就可以将一个进程中的多个线程分配到多个处理机上,并行运行
简言之,多线程可以让多核CPU充分发挥性能并行运行。
系统开销
进程和线程的创建撤销,系统都要为止分配和回收资源,比如内存空间、IO设备等,进程和线程的上下文切换,系统也都需要付出一定的时空开销。
但是,线程相关的开销明显小于进程。
线程简介
各线程之间也是存在资源共享和相互合作的,线程在运行时也是间断的,轮转切换的。
线程也是有运行状态的,这一点与进程并没有本质区别,最主要的状态也是就绪、执行、阻塞
进程的控制核心信息保存在PCB中,线程也有对应的组成---TCB,所有用于控制和管理线程的信息都保存在TCB中
线程尽管是另外一种完全不同的事物,但是毕竟是从进程的概念演化而来,也是操作系统对程序运行抽象的一部分,所以,线程必然与进程有着很多的相似点
线程实现
线程的实现主要有三种形式
从上面的分析中可以看得出来,内核支持和用户级都有各自明显的缺点和优点。
有些操作系统把用户级线程和内核支持线程两种方式进行组合,提供了组合方式ULT/KST 线程。
在组合方式线程系统中, 内核支持多KST线程的建立、调度和管理
同时,也允许用户应用程序建立、调度和管理用户级线程。
一些内核支持线程对应多个用户级线程,程序员可按应用需要和机器配置对内核支持线程数目进行调整,以达到较好的效果。
组合方式线程中,同一个进程内的多个线程可以同时在多处理器上并行执行,而且在阻塞一个线程时,并不需要将整个进程阻塞。
所以,组合方式多线程机制能够结合 KST和 ULT两者的优点,并克服了其各自的不足。
本文作者:程序员潇然 疯狂的字节X https://crazybytex.com/
线程的同步与通信
关于进程的同步与通信的相关逻辑原理,对于进程的同步与通信绝大多数都是适用的。
针对于这些原理,多线程OS也提供了多种同步机制,如互斥锁、条件变量、计数信号量以及多读、单写锁等。
信号量机制
进程中的信号量机制完全适合多线程同步
根据用法分为两种
- 私用信号量(private samephore)
- 公用信号量(public semaphort)
系统运行中,有多个进程,进程中又有多个线程。
如果是为了同一进程中多个线程同步设置的信号量,量属于特定的进程所有,这就叫做私用,OS并不知道私用信号量的存在。
如果是为了不同进程或者不同进程中的线程之间而设置的,就叫做公用。其数据结构是存放在受保护的系统存储区中,由OS为它分配空间并进行管理,故也称为系统信号量。
互斥锁(mutex)
互斥锁是一种比较简单的、用于实现线程间对资源互斥访问的机制
互斥锁可以有两种状态
当一个线程需要读/写一个共享数据段时,需要对mutex进行上锁,离开时需要解锁。
上锁时,首先校验 mutex 的状态,如果它已处于关锁状态,则试图访问该数据段的线程将被阻塞;如果 mutex处于开锁状态,则将 mutex 上锁后便去读/写该数据段。
线程完成操作后,必须将 mutex 解锁,同时还需要将阻塞在该互斥锁上的一个线程唤醒,其它的线程仍被阻塞在等待mutex打开的队列上。
另外,为了减少线程被阻塞的机会,在有的系统中还提供了一种用于 mutex 上的操作命令 Trylock。
顾名思义,并不会因为无法进入而阻塞,若 mutex 处于上锁状态,则 Trylock 并不会阻塞该线程,而只是返回一个指示操作失败的状态码。
条件变量
在许多情况下,只利用 mutex 来实现互斥访问可能会引起死锁
比如A线程请求资源顺序为R1,R2,B线程请求资源顺序为R2,R1
如果A对mutex 1上锁成功进入临界区后,需要获取R2的锁mutex 2,可是此时B获得了资源R2,对mutex 2已经上锁,此时,A等待mutex 2 B等待mutex 1,形成了死锁
所以说,锁,应该是仅仅用于在条件成立时进行操作时的一个同步保障,而不能在整个过程中都依靠锁
可以借助于条件变量,就是条件
每一个条件变量通常都与一个互斥锁一起使用,单纯的互斥锁用于短期锁定,主要是用来保证对临界区的互斥进入。
而条件变量则用于线程的长期等待,直至所等待的资源成为可用的资源。
申请
Lock mutex
while (条件状态不满足) {
wait(condition variable);//释放锁,线程挂起等待,直到条件满足通知;
}
临界区其他操作
unlock mutex;
释放
Lock mutex
一些操作
unlock mutex;
wakeup(condition variable);
简言之,借助于条件变量用于控制长时间的等待,锁用于控制对资源的同步。
总结
本文对线程进行了非常简单的介绍,线程之于进程在很多的方面有着极其类似的逻辑,尤其是从调度的视角看。
毕竟线程就是对进程中关于调度部分的独立抽象。
只要能够理解进程和线程的目的就能够很好地理解他们相似的原因,因为都是操作系统对于程序运行的抽象描述,线程是进程的更加细粒度的掌控。
在换句话说就是操作系统的角度对程序的执行抽象为:“资源的分配”“调度
”
最初这两个概念都是加诸于进程这个概念上,后续为了更加高效将两个概念进行了拆分,就是这样
所以说,对于原先介绍的进程的相关概念中关于调度部分的绝大多数理论,都是适用于线程概念的
转载务必注明出处:程序员潇然,疯狂的字节X,https://crazybytex.com/thread-60-1-1.html