首页 > 代码库 > POSIX 线程详解(1-概述)

POSIX 线程详解(1-概述)

线程是有趣的

线程类似于进程。如同进程,线程由内核按时间分片进行管理。在单处理器系统中,内核使用时间分片来模拟线程的并发执行,这种方式和进程的相同。而在多处理器系统中,如同多个进程,线程实际上一样可以并发执行。

那么为什么对于大多数合作性任务,多线程比多个独立的进程更优越呢?这是因为,线程共享相同的内存空间。不同的线程可以存取内存中的同一个变量。所以,程序中的所有线程都可以读或写声明过的全局变量。如果曾用 fork() 编写过重要代码,就会认识到这个工具的重要性。为什么呢?虽然 fork() 允许创建多个进程,但它还会带来以下通信问题: 如何让多个进程相互通信,这里每个进程都有各自独立的内存空间。对这个问题没有一个简单的答案。虽然有许多不同种类的本地 IPC (进程间通信),但它们都遇到两个重要障碍:

  • 强加了某种形式的额外内核开销,从而降低性能。
  • 对于大多数情形,IPC 不是对于代码的“自然”扩展。通常极大地增加了程序的复杂性。

双重坏事: 开销和复杂性都非好事。如果曾经为了支持 IPC 而对程序大动干戈过,那么您就会真正欣赏线程提供的简单共享内存机制。由于所有的线程都驻留在同一内存空间,POSIX 线程无需进行开销大而复杂的长距离调用。只要利用简单的同步机制,程序中所有的线程都可以读取和修改已有的数据结构。而无需将数据经由文件描述符转储或挤入紧窄的共享内存空间。仅此一个原因,就足以让您考虑应该采用单进程/多线程模式而非多进程/单线程模式。

线程是快捷的

不仅如此。线程同样还是非常快捷的。与标准 fork() 相比,线程带来的开销很小。内核无需单独复制进程的内存空间或文件描述符等等。这就节省了大量的 CPU 时间,使得线程创建比新进程创建快上十到一百倍。因为这一点,可以大量使用线程而无需太过于担心带来的 CPU 或内存不足。使用 fork() 时导致的大量 CPU 占用也不复存在。这表示只要在程序中有意义,通常就可以创建线程。

当然,和进程一样,线程将利用多 CPU。如果软件是针对多处理器系统设计的,这就真的是一大特性(如果软件是开放源码,则最终可能在不少平台上运行)。特定类型线程程序(尤其是 CPU 密集型程序)的性能将随系统中处理器的数目几乎线性地提高。如果正在编写 CPU 非常密集型的程序,则绝对想设法在代码中使用多线程。一旦掌握了线程编码,无需使用繁琐的 IPC 和其它复杂的通信机制,就能够以全新和创造性的方法解决编码难题。所有这些特性配合在一起使得多线程编程更有趣、快速和灵活。

线程是可移植的

如果熟悉 Linux 编程,就有可能知道 __clone() 系统调用。__clone() 类似于 fork(),同时也有许多线程的特性。例如,使用 __clone(),新的子进程可以有选择地共享父进程的执行环境(内存空间,文件描述符等)。这是好的一面。但 __clone() 也有不足之处。

线程的主要学习内容

  1. 线程管理  
    1. 创建和终止线程  
    2. 向线程传递参数  
    3. 连接(Joining)和分离( Detaching)线程  
    4. 栈管理  
    5. 其它函数  
  2. 互斥量(Mutex Variables)  
    1. 互斥量概述  
    2. 创建和销毁互斥量  
    3. 锁定(Locking)和解锁(Unlocking)互斥量  
  3. 条件变量(Condition Variable)  
    1. 条件变量概述 
    2. 创建和销毁条件变量  
    3. 等待(Waiting)和发送信号(Signaling)  

什么是线程

  • 技术上,线程可以定义为:可以被操作系统调度的独立的指令流。但是这是什么意思呢? 
  • 对于软件开发者,在主程序中运行的“函数过程”可以很好的描述线程的概念。 
  • 进一步,想象下主程序(a.out)包含了许多函数,操作系统可以调度这些函数,使之同时或者(和)独立的执行。这就描述了“多线程”程序。 
  • 怎样完成的呢? 
  • 在理解线程之前,应先对UNIX进程(process)有所了解。进程被操作系统创建,需要相当多的“额外开销”。进程包含了程序的资源和执行状态信息。如下: 
    • 进程ID,进程group ID,用户ID和group ID 
    • 环境 
    • 工作目录  
    • 程序指令 
    • 寄存器 
    • 栈 
    • 堆 
    • 文件描述符 
    • 信号动作(Signal actions) 
    • 共享库 
    • 进程间通信工具(如:消息队列,管道,信号量或共享内存) 

   Unix Process

 Process-thread relationship

UNIX PROCESS 

THREADS WITHIN A UNIX PROCESS 

  • 线程使用并存在于进程资源中,还可以被操作系统调用并独立地运行,这主要是因为线程仅仅复制必要的资源以使自己得以存在并执行。 
  • 独立的控制流得以实现是因为线程维持着自己的: 
    • 堆栈指针 
    • 寄存器 
    • 调度属性(如:策略或优先级) 
    • 待定的和阻塞的信号集合(Set of pending and blocked signals) 
    • 线程专用数据(TSD:Thread Specific Data.) 
  • 因此,在UNIX环境下线程: 
    • 存在于进程,使用进程资源 
    • 拥有自己独立的控制流,只要父进程存在并且操作系统支持 
    • 只复制必可以使得独立调度的必要资源 
    • 可以和其他线程独立(或非独立的)地共享进程资源 
    • 当父进程结束时结束,或者相关类似的 
    • 是“轻型的”,因为大部分额外开销已经在进程创建时完成了 
  • 因为在同一个进程中的线程共享资源: 
    • 一个线程对系统资源(如关闭一个文件)的改变对所有其它线程是可以见的 
    • 两个同样值的指针指向相同的数据 
    • 读写同一个内存位置是可能的,因此需要成员显式地使用同步 

使用线程设计程序 

  • 在现代多CPU机器上,pthread非常适于并行编程。可以用于并行程序设计的,也可以用于pthread程序设计。 
  • 并行程序要考虑许多,如下: 
    • 用什么并行程序设计模型? 
    • 问题划分 
    • 加载平衡(Load balancing) 
    • 通信 
    • 数据依赖 
    • 同步和竞争条件 
    • 内存问题 
    • I/O问题 
    • 程序复杂度 
    • 程序员的努力/花费/时间 
    • ...  
  • 包含这些主题超出本教程的范围,有兴趣的读者可以快速浏览下“Introduction to Parallel Computing”教程。 
  • 大体上,为了使用Pthreads的优点,必须将任务组织程离散的,独立的,可以并发执行的。例如,如果routine1和routine2可以互换,相互交叉和(或者)重叠,他们就可以线程化。 

 

  • 拥有下述特性的程序可以使用pthreads: 
    • 工作可以被多个任务同时执行,或者数据可以同时被多个任务操作。 
    • 阻塞与潜在的长时间I/O等待。 
    • 在某些地方使用很多CPU循环而其他地方没有。 
    • 对异步事件必须响应。 
    • 一些工作比其他的重要(优先级中断)。 
  • Pthreads 也可以用于串行程序,模拟并行执行。很好例子就是经典的web浏览器,对于多数人,运行于单CPU的桌面/膝上机器,许多东西可以同时“显示”出来。 
  • 使用线程编程的几种常见模型: 
    • 管理者/工作者(Manager/worker):一个单线程,作为管理器将工作分配给其它线程(工作者),典型的,管理器处理所有输入和分配工作给其它任务。至少两种形式的manager/worker模型比较常用:静态worker池和动态worker池。 
    • 管道(Pipeline):任务可以被划分为一系列子操作,每一个被串行处理,但是不同的线程并发处理。汽车装配线可以很好的描述这个模型。 
    • Peer: 和manager/worker模型相似,但是主线程在创建了其它线程后,自己也参与工作。 

共享内存模型(Shared Memory Model):  

  • 所有线程可以访问全局,共享内存 
  • 线程也有自己私有的数据 
  • 程序员负责对全局共享数据的同步存取(保护) 

 

线程安全(Thread-safeness):  

  • 线程安全:简短的说,指程序可以同时执行多个线程却不会“破坏“共享数据或者产生“竞争”条件的能力。 
  • 例如:假设你的程序创建了几个线程,每一个调用相同的库函数: 
    • 这个库函数存取/修改了一个全局结构或内存中的位置。 
    • 当每个线程调用这个函数时,可能同时去修改这个全局结构活内存位置。 
    • 如果函数没有使用同步机制去阻止数据破坏,这时,就不是线程安全的了。