操作系统-线程概述

为什么使用线程

avatar

我们需要一种新的实体(线程),既能够满足不同实体之间可以并发执行,同时可以共享相同的地址空间和文件资源等,相对于进程,减少创建和切换时的开销

什么是线程

Thread: 进程当中的一条执行流程

从两个维度来理解进程:

  1. 从资源组合的角度:进程把一组相关的资源组合起来,构成了一个资源平台(环境),包括地址空间(代码段,数据段),打开的文件等各种资源。
  2. 从运行的角度:代码在这个资源平台上的一条执行流程。(这个执行流程既是线程 )

即:线程 = 进程 - 共享资源 (完成一个控制流的管理)

1. 线程优点:

  • 一个进程中可以同时存在多个线程
  • 各个线程之间可以并发执行
  • 各个线程之间可以共享地址空间和文件等资源

2.线程缺点:

  • You have to explicitly coordinate access to shared date with locks. If you forget a lock, you’ll end up with corrupted data. (共享数据需加锁)
  • Circular dependencies amongst locks can lead to deadlocks. (导致死锁)
  • They are hard to debug with subtle timing issues. (执行时序不确定,难以调试)
  • Callbacks don’t work with locks. (回调不适用于锁)
  • It’s hard to get good performance. (很难有好的表现)
  • They are “too hard for most programmers to use, and even for experts development is painful.” (难以使用)

上述缺点是 1995 年 John Ousterhout 在一个演说中提到的,因为当时时间较早,所以其中有些缺点,比如说难用,回调不适用于锁 (有待考证),表现不好等,现在可能已经克服了,像 Linux 系统中 NPTL 模型实现的 pthreads 库,以及其他语言实现的多线程库,例如:java 线程库,封装的都很好,使用起来也都十分方便; 但是 共享数据需加锁死锁, 执行时序不确定,难以调试,这三点依然是现在多线程编程中面临的最大挑战。

Attention:线程和进程比较:

  • 进程是资源分配单位,线程是CPU调度单位;
  • 进程拥有一个完整的资源平台,而线程只独享必不可少的资源,如寄存器和栈,来达到独立的控制流;
  • 线程同样具有就绪,阻塞和执行三种基本状态,同样具有状态之间的转换关系;
  • 线程能减少并发执行的时间和空间开销:
    - 线程的创建时间比进程短;
    - 线程的终止时间比进程短;
    - 同一进程内的线程切换时间比进程短;
    - 由于同一进程的各线程间共享内存和文件资源,可直接进行不通过内核的通信
    

线程的实现

传统的操作系统主要有三种线程的实现方式:
1. 用户线程:在用户空间实现

1
2
3
4
5
6
7
8
9
10
11
12
POSIX Pthreads, Mach C-threads, Solaris threads

在用户空间实现的线程机制,它不依赖于操作系统的内核,由一组用户级线程库函数来完成线程的管理,
包括线程的创建,终止,同步和调度等。

* 缺点
1. 一个线程发起系统调用而阻塞,则整个进程等待
2. 当一个线程运行,除非主动交出CPU使用权,否则它所在进程中的其他线程将无法运行
3. 由于时间片是分给进程,故与其他进程相比,在多线程执行时,每个线得到的时间片较少,执行会比较慢
* 优点
1. 无需用户态和内核态切换,速度快
2. 允许每个进程拥有自己的调度算法

2. 内核线程:在内核中实现

1
2
3
windwos ,Solaris, Linux

指在操作系统内核当中实现的一种线程机制,由操作系统的内核来完成线程的创建,终止和管理,开销较大

3. 轻量级进程:在内核中实现,支持用户线程

1
2
3
Solaris (LightWeight Process)

内核支持的用户线程。一个进程可以有一个或者多个轻量级进程,每个轻量级进程由一个单独的内核线程来支持。

Linux 系统线程实现

那么最新版本的 Linux 系统中的 pthreads 线程库中的线程是如何实现的呢 ? 我参考了一下书籍 << Linux 内核设计与实现>> 以及 Linux Programmer's Manual 当中的描述, 实现流程大致如下:

Linux Programmer’s Manual 描述的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Linux implementations of POSIX threads
Over time, two threading implementations have been provided by the GNU C library on Linux:

LinuxThreads
This is the original Pthreads implementation. Since glibc 2.4, this implementation is no longer supported.

NPTL (Native POSIX Threads Library)
This is the modern Pthreads implementation. By comparison with LinuxThreads, NPTL provides closer conformance to the requirements of the POSIX.1
specification and better performance when creating large numbers of threads. NPTL is available since glibc 2.3.2, and requires features that are
present in the Linux 2.6 kernel.

Both of these are so-called 1:1 implementations, meaning that each thread maps to a kernel scheduling entity. Both threading implementations employ the
Linux clone(2) system call. In NPTL, thread synchronization primitives (mutexes, thread joining, and so on) are implemented using the Linux futex(2) sys‐
tem call.

Linux 2.6 kernel 版本后, 采用的是 NPTL 模型来实现。该模型是一个所谓的 1×1 线程库,其中由用户创建的线程(通过pthread_create() 库函数)与内核中的 可调度实体Linux中的任务)进行1-1对应,是十分简单的线程实现方式。

假如我们要在一个进程中创建四个线程, 在 Linux 系统中,实现思路是:创建四个进程并分配四个普通的 task_struct (PCB 在代码中的表示) 结构, 然后在建立这个四个进程时,指定它们共享某些资源即可。 也就是说 Linux 系统中创建进程和线程是类似的,最终都是调用 clone(),只是对外封装接口表现不一样,传入参数不一样而已。

参考