操作系统学习笔记2:多线程
概述
现代软件大多支持多线程,相比于进程切换,线程共享代码段,数据段以及其他系统资源,但是拥有单独的寄存器和堆栈。
服务器采用多线程,可以减少创建进程的资源消耗,同时处理多个并发请求。
优点
- 响应性提高
- 资源共享
- 创建与切换更加经济
多核编程
并行性 vs 并发性
并行性:是同时执行多个任务
并发性:是让每个任务都能取得进展,在单处理器上也能实现
Amdahl定理:程序中只有S%可以串行执行时,优化比
$$\eta \leq \frac{1}{S+\frac{1-S}{N}}$$
挑战
- 分析一个任务是否可以多核
- 平衡某些任务适合单独核心执行
- 数据分割
- 数据依赖,避免同步性受损
- 调试程序
并行类型
分为数据并行(把一个任务的不同部分数据分配到不同核心)
和任务并行(把多个任务分配到不同核心)
多线程模型
线程支持有两种方案:用户线程和内核线程。用户和内核线程有多重关系模型:
- 多对一模型
- 一个内核对应多个用户线程
- 线程被用户空间库管理
- 效率高
- 一个线程阻塞整个进程都会阻塞
- 同时只有一个线程访问内核,不支持并行
- 一对一模型
- 相比于多对一,一对一对并行的支持更好
- 但是系统内核线程会影响性能
- Linux Windows都实现了这个模型
- 多对多模型
- 对这个模型而言,创建多个用户线程同时保持高性能并发是可能的
- 一个变体是允许多对多模型和一对一模型同时存在
线程库
线程库的实现,有纯用户空间实现:即所有数据都位于用户空间,调用库函数不涉及系统调用。也有内核实现:库的代码和数据结构位于内核空间。POSIX线程库是在内核和用户空间都能实现的库,Windows则是只能在内核实现。JVM取决于宿主系统的库。
POSIX和Windows的库中可以声明全局变量,供所有线程访问。本地数据存放在堆栈,每个线程有自己的堆栈
线程分为同步和异步执行,同步执行的父线程需要等待子线程结束才能执行。
对于Pthread函数,pthread_t tid,pthread_attr_t 是参数类型,pthread_attr_init是初始化函数,pthread_create(&tid,&attr,&func,int)创建线程,使用pthread_join()等待tid的线程结束,pthread_exit()用于退出进程
windows api使用windows.h库
Java多线程使用Runnable接口的run方法实现。类需要实现Runnable接口的方法。
在Java中,把一个有Runnable接口的类通过Thread类进行实现,调用thrd的start方法即可自动启动子线程。
隐式多线程
这是把创建线程交给编译器和runtime进行
线程池
这个机制允许提前创建出来等待工作,如果池中没有可用线程,进程将会等待。
调用的方法类似QueueUserWorkItem(Function,Param,Flags)
OpenMP
openmp使用#pragma 的宏命令来只是openmp识别并行区域来执行代码。
例如
1 |
|
大中央调度
GCD,是MacOSX的一种技术,可以使用
^{}标记一个块,放置在调度队列(优先队列)来执行,分配给线程池的一个线程。
多线程问题
关于fork和exec
系统调用中,fork有两种形式:fork可以让新进程复制所有进程,或者只复制调用的进程
exec会取代所有线程
所以如果fork完立刻调用exec,就只复制一个线程就行。
信号处理
信号是一种UNIX用于通知进程的机制,分为同步信号和异步信号,同步信号发送到产生事件的同一进程,异步信号发送到其他进程。
信号处理程序分为缺省信号处理和用户定义处理程序。传递信号的函数为kill(pid,signal)。这规定了将信号传递到进程pid,事实上,信号传递到多线程中会有如下可能:
- 传递到信号适用的thread
- 传递到每个thread
- 传递到某些thread
- 传递到一个指定接受所有信号的thread
对于一个异步信号,因为信号只能处理一次,所以传递到第一个不拒绝的线程。
pthreads有一个函数:pthread_kill(pthread_t tid, int signal)
Windows支持异步过程调用来模拟信号机制
线程撤销
目标线程是被撤销的线程。撤销线程分为异步撤销(立即撤销)和延迟撤销(一个线程检查目标线程何时适合撤销)使用pthread_cancel来撤销。
默认pthread是延迟撤销的,创建线程也可以指定是否可以立刻撤销,如果不可以的话,pthread_testcancel()函数可以指定当前可以撤销。
TLS
线程本地存储,可以让一个变量作为线程的全局变量,但是其他线程无法访问
调度程序
为了保证内核线程的动态调整,系统实现了一个名为轻量级进程LWP的数据结构,对用户线程,其体现为虚拟处理器,每个LWP与一个内核线程相连(真正调用物理处理器)。一个进程的LWP数量有限。
用户线程和内核的通信是通过调度器激活的机制进行的。
内核分配一组LWP给应用程序。应用程序将线程分配给LWP。
当有事件发生时,例如阻塞,内核出发回调给应用程序,应用程序中的线程库出发回调处理程序来保存阻塞进程的内容,然后分配一个新的线程给原本阻塞线程所在的LWP。阻塞结束后,也是通过回调程序来恢复运行。
实例
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!