# Processes and Threads ## Motivation for threads 为什么我们需要线程 - 操作系统必须支持多个任务同时运行。例如,进程、终端或系统维护。 - 操作系统必须支持多个连接同时运行。例如,多用户、Web连接等。 - 进程必须能够同时执行多个任务。例如,一个游戏必须同时有图形、声音与运算逻辑等。 - 操作系统必须同时访问多个硬件,或者至少看起来像是同时访问。你不会希望动鼠标、按键盘时,系统不能访问网络或磁盘。 一些定义: - Multi-Processing:多个 CPU(核心) - Multi-Programming:多个作业/进程 - Multi-Threading:多个线程/进程 ### Concurrency 并发 **并发**运行两个线程意味着什么? - 调度器可以自由地,以任意顺序和交错方式,运行线程 - 线程可能会运行至完成,或者以大块或小块的时间片运行 ![MultiProcessing vs MultiProgramming](images/L03-MultiProcessing-vs-MultiProgramming.png) ### Computes that never finish 永远无法完成的计算 想象如下代码,其中ComputePi()会尝试计算π的最后一位,并将计算过程写入文件pi.txt。 ```C main() { ComputePI("pi.txt"); PrintClassList("classlist.txt"); } ``` 这个例子中,显然ComputePi是个不可能完成的函数。因此我们完全没有办法print ClassList!程序也无法停止,除非你Ctrl-C或者关闭电脑。 但是使用线程,我们可以这么做: ```C main() { create_thread(ComputePI, "pi.txt"); create_thread(PrintClassList, "classlist.txt"); } ``` 这样,即便ComputePI永远无法完成,我们也可以得到ClassList。 ### IO / Compute Overlap 这是一个各种操作的时间表: | Operation | Time | | :--- | :--- | | L1 cache reference | 0.5 ns | | Branch mis-predict | 5 ns | | L2 cache reference | 7 ns | | Mutex lock/unlock | 25 ns | | Main memory reference | 100 ns | | Compress 1K bytes with Zippy | 3,000 ns | | Send 2K bytes over 1 Gbps network | 20,000 ns | | Read 1 MB sequentially from DDR2 memory | 250,000 ns | | Round trip within same datacenter | 500,000 ns | | Disk seek on HDD | 10,000,000 ns | | Read 1 MB sequentially from HDD disk | 20,000,000 ns | | Send packet CA->Netherlands->CA | 150,000,000 ns | Disk Seek(硬盘寻址)等操作显然需要很久,比如10ms。我们显然不希望CPU的一个核心等待这么久,不做任何计算。 ## Introduction to threads 线程简介 线程有三个状态。如下: - Running 运行态: 线程正在使用CPU的时间片 - Ready 就绪态: 线程准备好运行了,但还没有上CPU - Blocked 阻塞态: 线程不能继续运行 我们可以基于此设计调度器。例如,如果两个线程都不需要IO,它们很可能会轮流使用同一个CPU核心: ![Thread Scheduling Without IO](images/L03-Thread-Scheduling-Without-IO.png) 但是如果其中一个线程需要执行IO操作,那么它就会被阻塞,另一个线程就可以继续使用CPU核心: ![Thread Scheduling With IO](images/L03-Thread-Scheduling-With-IO.png) 注: Blocked状态不会直接转为Running状态,必须先转为Ready状态。 ### How to manage threads 如何管理线程 一个程序默认就含有一个线程。如果想要创建其他线程,我们必须使用一次System Call。 在C语言中,使用```pthread_create()```函数来创建线程。 ```C int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void*), void *arg); ``` - 创建线程,并以 `arg` 为唯一参数执行 `start_routine`。 - `return` 相当于隐式调用 `pthread_exit`。 ```C void pthread_exit(void *value_ptr); ``` - 终止线程,并使 `value_ptr` 对任何成功的 `join` 操作可用 ```C int pthread_join(pthread_t thread, void **value_ptr); ``` - 挂起调用线程的执行,直到目标线程终止。 - 返回时,若 `value_ptr` 非 `NULL`,则终止线程传递给 `pthread_exit()` 的值将存储在 `value_ptr` 指向的位置。 #### `void *(*start_routine)(void*)`是什么玩意? 我们从内向外看这个东西。 `*start_routine` 是一个函数指针,指向一个函数,这个函数接受一个 `void*` 类型的参数,并返回一个 `void*` 类型的值。 ## System Calls System Call 接口隔开了用户程序与操作系统内核。 这使得系统设计看起来像一个沙漏,而System Call正式沙漏中间最窄的地方, 如图所示: ![System is the interface between user and kernel](images/L03-System-Call-In-OS.png) 你会注意到System Call之上,用户程序之下还有一层Portable OS Library。这个库允许程序员使用同一套API来编写程序。 - C程序猿们会使用`libc`,提供很多以某种方式包装的系统调用。