操作系统 --多线程编程
概述
多线程模型
线程库
线程问题
操作系统实例
操作系统 --多线程编程
介绍线程的概念——构成多线程计算机系统基础的CPU使用的基本单位
讨论Pthreads、Win32和Java线程库的api
研究与多线程编程相关的问题
概述
单线程和多线程
单线程在程序中只有一个registers(寄存器),一个堆栈(stack)来执行一个线程
而多线程有多个registers 和stack 一组来执行多个thread
多线程服务模型
客户端 想服务端发送一个请求
客户端创建一个新线程来处理这个请求
然后将结果返回给客户端
优点
一个交互式应用程序可能允许一个程序继续运行,即使它的一部分被阻塞或正在执行一个长时间的操作,
从而增加对用户的响应。
例如,一个多线程的Web浏览器可以允许用户在一个线程中交互,而在另一个线程中加载图像。
资源共享:进程只能通过共享内存或由程序员安排的消息传递来共享资源。
线程共享它们默认所属进程的内存和资源。
共享代码和数据的好处在于,它允许应用程序在相同的地址空间内拥有多个不同的活动线程。
可伸缩性:在多处理器架构中,多线程的好处可以大大增加,因为线程可以在不同的处理器上并行运行。
多cpu机器上的多线程增加了并行性。
多核编程
多核系统给程序员带来压力,挑战包括
分裂活动
平衡
数据分割
数据依赖
测试和调试
在单核上的并发执行
那么所有的线程只需要排成一列等待执行即可
在多核上的并发执行
会有多个列之间并行执行
多线程模型
对线程的支持可以在用户级提供给用户线程,也可以由内核提供给内核线程。
在内核之上支持用户线程,在没有内核支持的情况下管理用户线程。
内核线程由操作系统直接支持和管理。
几乎所有的当代操作系统,包括Windows XP/2000、Solaris、Linux、Mac OS X和Tru64 UNIX(以前的数字UNIX),都支持内核线程。
用户线程和内核线程之间必须存在关系。
建立这种关系的三种常见方式:
多对一
许多用户级线程映射到单个内核线程。线程管理由用户空间中的线程库完成,效率高。
但是,如果一个线程进行了一个阻塞的系统调用,整个进程将会阻塞。
同时只有一个线程可以访问内核,多个线程无法在多处理器上并行运行。
例子:
Solaris绿色线程
GNU轻便线程
一对一的
每个用户级线程都映射到一个内核线程。
当一个线程进行阻塞的系统调用时,允许另一个线程运行。
也允许多个线程在多处理器上并行运行。
创建用户线程需要创建相应的内核线程限制系统支持的线程数量
例子
Windows NT / 2000 / XP
Linux
Solaris 9及以后版本
多对多
将许多用户级线程多路复用到数量较少或相等的内核线程
允许开发人员按照自己的意愿创建多个用户线程,因为线程在同一时间只能调度一个内核,所以不能获得真正的并发性。
但是内核线程可以在多处理器上并行运行。
也允许另一个线程在一个线程进行阻塞的系统调用时运行。
版本9之前的Solaris
Windows NT/2000与ThreadFiber包
多对多模型(称为两级模型)的一个流行变体是,它还允许将用户线程绑定到内核线程
例子
IRIX
hp - ux
Tru64 UNIX
Solaris 8和更早的版本
线程库
线程库为程序员提供了创建和管理线程的API。
两种主要的实现方式
提供一个完全在用户空间中,没有内核支持的库。库的所有代码和数据结构都存在于用户空间中。调用库中的函数会导致用户空间中的本地函数调用,而不是系统调用。
操作系统直接支持的内核级库。该库的代码和数据结构存在于内核空间中。调用库的API中的函数会导致对内核的系统调用。
现在有三个主线程库在使用
POSIX Pthreads
Win32
Java
Pthreads可以作为用户级库,也可以作为内核级库
Win32线程库是一个内核级库
Java thread API允许在Java程序中直接创建和管理线程。
然而,由于JVM运行在主机操作系统之上,Java线程API通常使用主机系统上可用的线程库来实现。
让我们描述一下使用这三个线程库创建基本线程的情况。
设计一个多线程程序,使用众所周知的求和函数在单独的线程中执行非负整数的求和
N=3, sum = 0+1+2+3 = 6
N = 5, sum = 0+1+2+3+4+5 = 15
地址
可以作为用户级或内核级提供
用于线程创建和同步的POSIX标准(IEEE 1003.1c) API
API指定线程库的行为,实现取决于库的开发
适用于UNIX操作系统(Solaris、Linux、Mac OS X)
使用Pthreads API的多线程C程序
win32 threads
使用Win32线程库创建线程的技术类似于Pthreads技术。
由不同线程共享的数据(sum)是全局声明的。
在单独的线程中执行的sum()函数。
线程是使用CreateThread()函数创建的。一组属性被传递给这个函数
使用WaitForSingleObject()函数,它会导致创建线程阻塞,直到求和线程存在为止。
java 线程
Java线程由JVM管理
通常使用底层操作系统提供的线程模型实现
可以创建Java线程:
创建从Thread类派生的新类并重写其run()方法,或
定义一个实现Runnable接口(更常用)的类。
当一个类实现Runnable时,它必须定义一个run()方法。
实现run()方法的代码作为单独的线程运行。
一个非负整数求和的Java程序
class Sum implements Runnable{
private int upper;
public Sum ( int upper){
this.upper = upper;
}
public void run() {
int sum = 0;
for (int i= 0 , i <= upper; i++){
sum += i;
}
}
}
线程问题
关于多线程程序需要考虑的一些问题。
fork()和exec()系统调用的语义
目标线程的线程取消
异步或递延
信号处理
线程池
表数据
调度程序激活
fork()和exec()的语义
第3章描述了如何使用fork()系统调用来创建一个独立的、重复的进程。
fork()和exec()系统调用的语义在多线程程序中会发生变化
如果程序中的一个线程调用fork(),新进程是否复制所有线程,或者新进程是单线程的?
有些UNIX系统有两个版本的fork(),一个复制所有线程,另一个只复制调用fork()系统调用的线程。
如果一个线程调用exec()系统调用,在exec()参数中指定的程序将替换整个进程——包括所有线程。
fork()的两个版本中使用哪个取决于应用程序。
如果fork之后立即调用exec(),则没有必要复制所有线程,因为在exec()的参数中指定的程序将取代进程。在这种情况下,只复制调用线程是合适的。
但是,如果单独的进程在fork之后没有调用exec(),那么单独的进程应该复制所有线程。
线程取消
在一个线程完成之前终止它
两个的一般方法:
异步取消立即终止目标线程
延迟取消允许目标线程定期检查是否应该取消它
信号处理
在UNIX系统中,信号用于通知进程发生了特定的事件
信号处理器是用来处理信号的
信号是由特定的事件产生的
信号被传递给进程
信号一旦传递,就必须被处理
选项:
将信号传递给应用该信号的线程
将信号传递给进程中的每个线程
将信号传递给进程中的某些线程
指定一个特定的线程来接收进程的所有信号
线程池
在等待工作的池中创建多个线程
优点:
通常使用现有线程服务请求比创建新线程稍快一些
允许将应用程序中的线程数与池的大小绑定
线程特定数据
属于一个进程的线程共享该进程的数据。
但是,允许每个线程拥有自己的数据副本(线程特定的数据)是很有用的
例如,在一个事务处理系统中,我们可以在一个单独的线程中为每个事务服务。每个事务可能被分配一个唯一的ID。
要将每个线程与其唯一的ID关联起来,我们可以使用特定于线程的数据。
大多数线程库都为特定于线程的数据提供某种形式的支持。
调度程序激活
M:M和两级模型都需要内核和线程库之间进行通信,动态调整适当的内核线程数,以确保最佳性能。
轻量级进程(LWP)——一个介于使用线程和内核线程之间的中间数据结构。
对于用户线程库,LWP似乎是一个虚拟处理器,应用程序可以在其上调度用户线程运行。
每个LWP都附加到一个内核线程
如果内核线程阻塞LWP阻塞用户线程阻塞。
调度程序激活
一个应用程序可能需要任意数量的LWPs来有效地运行。
在单个处理器上运行的cpu绑定应用程序。
由于一次只能运行一个线程,所以一个LWP就足够了。
一个I/ o密集型应用程序可能需要多个LWPs来执行。
每个并发阻塞系统调用都需要一个LWP。
例如,五个不同的文件读取请求同时发生,然后需要五个LWPs,因为所有的LWPs都可能在内核中等待I/O完成。
调度程序激活:用户线程库和内核之间通信的一种方案
内核为应用程序提供一组虚拟处理器(LWPs),应用程序可以将用户线程调度到可用的虚拟处理器上。
内核必须通知应用程序某些事件- upcall
upcall由带有upcall处理程序的线程库处理,并且upcall处理程序必须运行在虚拟处理器上。
这种通信允许应用程序维护正确的内核线程数量
操作系统实例
Windows XP的线程
实现一对一的映射,
通过使用线程库,任何属于进程的线程都可以访问进程的地址空间。
每个线程都包含
一个线程id
表示处理器状态的寄存器集
分开的用户和内核堆栈
专用数据存储区
寄存器集、堆栈和私有存储区域称为线程的上下文
线程的主要数据结构包括:
thread(执行线程块)
KTHREAD(内核线程块)
TEB(线程环境块)
Linux线程
实现一对一的映射,
通过使用线程库,任何属于进程的线程都可以访问进程的地址空间。
每个线程都包含
一个线程id
表示处理器状态的寄存器集
分开的用户和内核堆栈
专用数据存储区
寄存器集、堆栈和私有存储区域被称为threadLinux的上下文。linux为fork()系统调用提供了复制进程的传统功能。
Linux还提供了使用clone()系统调用创建线程的能力
然而,Linux不区分进程和线程。
Linux将它们称为任务,而不是进程或线程
当clone()被调用时,它会被传递一组标志,这些标志决定父任务和子任务之间要进行多少共享。
线程的主要数据结构包括:
thread(执行线程块)
KTHREAD(内核线程块)
TEB(线程环境块)
例如,如果向clone()传递标志CLONE_FS、CLONE_VM、CLONE_SIGHAND和CLONE_FILES,它们将共享相同的文件系统信息、相同的内存空间、相同的信号处理程序和相同的打开文件集。