操作系统学习笔记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
2
3
4
5
#pragma omp parallel for
for (i=0;i<N;i++)
{
c[i]=a[i]+b[i]
}

大中央调度

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。阻塞结束后,也是通过回调程序来恢复运行。

实例