不论在多处理器还是单处理器上,线程都是“并发”的。
线程数量小于处理器数量时,是真正并发的。
单处理器下,并发是模拟的,操作系统会让这些多线程程序轮流执行,每次都只执行一小段时间,这就称为线程调度。
线程调度中,线程拥有三种状态:
处于运行中的线程拥有一段可以执行的时间,这称为时间片,当时间片用尽的时候,进程进入就绪状态,如果在用尽之前开始等待某事件,那么它就进入等待状态。每当一个线程离开运行状态的时候,调度系统就会选择一个其他的就绪线程继续执行。
现在的主流调度方法尽管都不一样,但基本都带有优先级调度和轮转法。
轮转法:各个线程轮流执行一段时间。
优先级调度:按线程的优先级来轮流执行,每个线程都拥有各自的线程优先级。
在win和lin里面,线程优先级不仅可以由用户手动设置,系统还会根据不同线程表现自动调整优先级。
一般频繁等待的线程称之为IO密集型线程,而把很少等待的线程称为CPU密集型线程。
优先级调度下,存在一种饿死现象。
饿死:线程优先级较低,在它执行之前,总是有较高级的线程要执行,所以,低优先级线程总是无法执行的。
当一个CPU密集型线程获得较高优先级时,许多低优先级线程就可能被饿死。
为了避免饿死,操作系统常常会逐步提升那些等待时间过长的线程。
线程优先级改变一般有三种方式:
抢占:在线程用尽时间片之后被强制剥夺继续执行的权利,而进入就绪状态。
在早期的系统中,线程是不可抢占的,线程必须主动进入就绪状态。
在不可抢占线程中,线程主动放弃主要是2种:
不可抢占线程有一个好处,就是线程调度只会发生在线程主动放弃执行或线程等待某个事件的时候,这样就可以避免一些抢占式线程时间不确定而产生的问题。
Linux的多线程Linux内核中并不存在真正意义上的线程概念。Linux所有执行实体(线程和进程)都称为任务,每一个任务概念上都类似一个单线程的进程,具有内存空间,执行实体,文件资源等。Linux不同任务之间可以选择共享内存空间,相当于同一个内存空间的多个任务构成一个进程,这些任务就是进程中的线程。
系统调用作用
fork 复制当前线程
exec 使用新的可执行映像覆盖当前可执行映像
clone 创建子进程并从指定位置开始执行
fork产生新任务速度非常快,因为fork不复制原任务的内存空间,而是和原任务一起共享一个写时复制的内存空间。
写时复制:两个任务可以同时自由读取内存,当任意一个任务试图对内存进行修改时,内存就会复制一份单独提供给修改方使用。
fork只能够产生本任务的镜像,因此需要和exec配合才能启动别的新任务。
而如果要产生新线程,则使用clone。
clone可以产生一个新的任务,从指定位置开始执行,并且共享当前进程的内存空间和文件等,实际效果就是产生一个线程。
多线程程序处于一个多变的环境中,可访问的全局变量和堆数据随时都可能被其他的线程改变。因此多线程程序在并发时数据的一致性变得非常重要。
竞争与原子操作++i的实现方法:
单条指令的操作称为原子的,单挑指令的执行不会被打断。在windows里,有一套API专门进行一些原子操作,这些API称为Interlocked API。
为了防止多个线程读取同一个数据产生不可预料结果,我们将各个线程对一个数据的访问同步。
同步:在一个线程对一个数据访问结束的时候,其他线程不能对同一个数据进行访问。对数据的访问被原子化。
锁:锁是一种非强制机制,每一个线程在访问数据或者资源之前会先获取锁,在访问结束后会释放锁。在锁被占用时候试图获取锁时,线程会等待,知道锁可以重新使用。
二元信号量:最简单的锁,它适合只能被唯一一个线程独占访问的资源,它的两种状态:
信号量:允许多个线程并发访问的资源。一个初始值为N的信号量允许N个线程并发访问。
操作如下:
访问完资源后,线程释放信号量: