操作系统思考 第九章 线程

第九章 线程

作者:Allen B. Downey

原文:Chapter 9 Threads

译者:飞龙

协议:CC BY-NC-SA 4.0

当我在2.3节提到线程的时候,我说过线程就是一种进程。现在我会更仔细地解释它。

当你创建进程时,操作系统会创建一块新的地址空间,它包含text段、static段、和堆区。它也会创建新的“执行线程”,这包括程序计数器和其它硬件状态,以及运行时栈。

我们目前为止看到的进程都是“单线程”的,也就是说每个地址空间中只运行一个执行线程。在这一章中,你会了解“多线程”的进程,它在相同地址空间内拥有多个运行中的线程。

在单一进程中,所有线程都共享相同的text段,所以它们运行相同的代码。但是不同线程通常运行代码的不同部分。

而且,它们共享相同的static段,所以如果一个线程修改了某个全局变量,其它线程会看到改动。它们也共享堆区,所以线程可以共享动态分配的内存块。

但是每个线程都有它自己的栈。所以线程可以调用函数而不相互影响。通常,线程并不能访问其它线程的局部变量。

这一章的示例代码在本书的仓库中,在名为counter的目录中。有关代码下载的更多信息,请见第零章。

9.1 创建线程

C语言使用的所普遍的线程标准就是POSIX线程,简写为pthread。POSIX标准定义了线程模型和用于创建和控制线程的接口。多数UNIX的版本提供了POSIX的实现。

译者注:C11标准也提供了POSIX线程的实现。为了避免冲突,函数的前缀改为了thrd

使用pthread就像使用大多数C标准库那样:

  • 你需要将头文件包含到程序开头。
  • 你需要编写调用pthread所定义函数的代码。
  • 当你编译程序时,需要链接pthread库。

例如,我包含了下列头文件:

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>

前两个是标准库,第三个就是pthread。为了在gcc中和pthread一起编译,你可以在命令行中使用-l选项:

gcc -g -O2 -o array array.c -lpthread

这会编译名为array.c的源文件,带有调试信息和优化,并链接pthread库,之后生成名为array的可执行文件。

9.2 创建线程

用于创建线程的pthread函数叫做pthread_create。下面的函数展示了如何使用它:

pthread_t make_thread(void *(*entry)(void *), Shared *shared)
{
  int n;
  pthread_t thread;

  n = pthread_create(&thread, NULL, entry, (void *)shared);
  if (n != 0) {
    perror("pthread_create failed");
    exit(-1);
  }
  return thread;
}

make_thread是一个包装,我编写它便于使pthread_create更加易用,并提供错误检查。

pthread_create的返回类型是pthread_t,你可以将其看做新线程的ID或者“句柄”。

如果pthread_create成功了,它会返回0,make_pthread也会返回新线程的句柄。如果出现了错误,pthread_create会返回错误代码,make_thread会打印错误消息并退出。

pthread_create的参数需要一些解释。从第二个开始,Shared是我定义的结构体,用于包含在两个线程之间共享的值。下面的typedef语句创建了这个新类型:

typedef struct {
  int counter;
} Shared;

这里,唯一的共享变量是countermake_sharedShared结构体分配空间,并且初始化其内容:

Shared *make_shared()
{
  int i;
  Shared *shared = check_malloc(sizeof (Shared));
  shared->counter = 0;
  return shared;
}

entry的参数声明为void指针,但在这个程序中我们知道它是一个指向Shared结构体的指针,所以我们可以对其做相应转换,之后将它传给执行实际工作的child_code

作为一个简单的示例,child_code打印了共享计数器的值,并增加它。

void child_code(Shared *shared)
{
  printf("counter = %d\n", shared->counter);
  shared->counter++;
}

child_code返回时,entry调用了pthread_exit,它可以用于将一个值传递给回收(join)当前线程的线程。这里,子线程没有什么要返回的,所以我们传递了NULL

最后,下面是创建子线程的代码:

int i;
pthread_t child[NUM_CHILDREN];

Shared *shared = make_shared(1000000);

for (i=0; i<NUM_CHILDREN; i++) {
    child[i] = make_thread(entry, shared);
}

NUM_CHILDREN是用于定义子线程数量的编译期常量。child是线程句柄的数组。

9.3 回收线程

当一个线程希望等待其它线程执行完毕,它需要调用pthread_join。下面是我对pthread_join的包装:

void join_thread(pthread_t thread)
{
  int ret = pthread_join(thread, NULL);
  if (ret == -1) {
    perror("pthread_join failed");
    exit(-1);
  }
}

参数是你想要等待的线程句柄。这个包装所做的事情就是调用pthread_join之后检查结果。

任何线程都可以回收其它线程,但是多数普遍的情况下,父线程创建并回收所有子线程。我们继续使用上一节的例子,下面是等待子线程的代码:

for (i=0; i<NUM_CHILDREN; i++) {
    join_thread(child[i]);
}

这个循环一次等待一个子线程,以它们创建的顺序。没有办法来保证子线程按照顺序执行完毕,但是这个循环在它们不这样的时候也会正确执行。如果某个子线程迟于其它线程,这个循环会等待它,其它子线程也会在同时执行完毕。但是无论如何,所有子线程执行完毕后,循环才会退出。

如果你下载这本书的仓库,你可以在counter/counter.c中找到它。你可以像这样编译并运行它:

$ make counter
gcc -Wall counter.c -o counter -lpthread
$ ./counter

当我以5个子线程运行它时,我获得了如下输出:

counter = 0
counter = 0
counter = 1
counter = 0
counter = 3

当你运行它时,你可能得到了不同的结果。并且如果你再次运行它,你可能每次都得到不同的结果。到底发生了什么呢?

9.4 同步错误

上一个程序的问题就是,子线程访问了共享变量counter,不带任何同步机制,所以在任何线程增加counter之前,这些线程读取到了它的相同值。

下面是一个事件序列,这可以解释上一节的输出:

Child A reads 0
Child B reads 0
Child C reads 0
Child A prints   0
Child B prints   0
Child A sets counter=1
Child D reads 1
Child D prints   1
Child C prints   0
Child A sets counter=1
Child B sets counter=2
Child C sets counter=3
Child E reads 3
Child E prints   3
Child D sets counter=4
Child E sets counter=5

每次你运行这个程序的时候,线程都会在不同时间点上中断,或者调度器可能选择不同的线程来运行,所以时间序列和结果都是不同的。

假设我们需要强行规定一个顺序。例如,我们想让每个线程读到counter的不同值并增加它,让counter的值反映出执行child_code的线程数量。

为了达到这一要求,我们可以使用“互斥体”(mutex),它提供了互斥体对象,来保证一段代码是“互斥”的,也就是说,一次只有一个线程可以执行这段代码。

我编写了一个叫做mutex.c的小型模块,来提供互斥体对象。我会首先向你展示如何使用,之后再展示工作原理。

下面是child_code使用互斥体同步线程的版本:

void child_code(Shared *shared)
{
  mutex_lock(shared->mutex);
  printf("counter = %d\n", shared->counter);
  shared->counter++;
  mutex_unlock(shared->mutex);
}

在任何线程访问counter之前,它们需要对互斥体“上锁”,这样可以阻塞住所有其它线程。假设线程A对互斥体上锁,并且执行到child_code的中间位置。如果线程B到达并执行了mutex,它会被阻塞。

当线程A执行完毕后,它执行了mutex_unlock,它允许线程B继续执行。实际上,一次只有一个排队中的线程会执行child_code,所以它们不会互相影响。当我以5个子线程运行这段代码时,我会得到:

counter = 0
counter = 1
counter = 2
counter = 3
counter = 4

这样就满足了要求。为了使这个方案能够工作,我向Shared结构体中添加了Mutex:

typedef struct {
  int counter;
  Mutex *mutex;
} Shared;

之后在make_shared中初始化它:

Shared *make_shared(int end)
{
  Shared *shared = check_malloc(sizeof(Shared));
  shared->counter = 0;
  shared->mutex = make_mutex();   //-- this line is new
  return shared;
}

这一节的代码在counter_mutex.c中,Mutex的定义在mutex.c中,我会在下一节解释它。

9.5 互斥体

我的Mutex的定义是pthread_mutex_t类型的包装,它定义在POSIX线程API中。

为了创建POSIX互斥体,你需要为pthread_mutex_t分配空间,之后调用pthread_mutex_init

一个问题就是在这个API下,pthread_mutex_t表现为结构体,所以如果你将它作为参数传递,它会复制,这会使互斥体表现不正常。你需要传递pthread_mutex_t的地址来避免这种情况。

我的代码更加容易正确使用。它定义了一个类型,Mutex,它是pthread_mutex_t的更加可读的名称:

#include <pthread.h>

typedef pthread_mutex_t Mutex;

之后它定义了make_mutex,它为mutex分配空间并初始化:

Mutex *make_mutex()
{
  Mutex *mutex = check_malloc(sizeof(Mutex));
  int n = pthread_mutex_init(mutex, NULL);
  if (n != 0) perror_exit("make_lock failed");
  return mutex;
}

返回值是一个指针,你可以将其作为参数传递,而不会有非预期的复制。

对互斥体上锁和解锁的函数都是POSIX函数的简单包装:

全选<button href="javascript:void(0);" _xhe_href="javascript:void(0);" class="copyCode btn btn-xs" data-clipboard-text="" void="" mutex_lock(mutex="" *mutex)"="" data-toggle="tooltip" data-placement="bottom" title="" style="color: rgb(255, 255, 255); font-style: inherit; font-variant: inherit; font-stretch: inherit; font-size: 12px; line-height: 1.5; font-family: inherit; margin: 0px 0px 0px 5px; overflow: visible; cursor: pointer; vertical-align: middle; border: 1px solid transparent; white-space: nowrap; padding-right: 5px; padding-left: 5px; border-radius: 3px; -webkit-user-select: none; box-shadow: rgba(0, 0, 0, 0.0980392) 0px 1px 2px; background-image: none; background-color: rgba(0, 0, 0, 0.74902);">复制放进笔记

void mutex_lock(Mutex *mutex)
{
  int n = pthread_mutex_lock(mutex);
  if (n != 0) perror_exit("lock failed");
}

void mutex_unlock(Mutex *mutex)
{
  int n = pthread_mutex_unlock(mutex);
  if (n != 0) perror_exit("unlock failed");
}

代码在mutex.c和头文件mutex.h中。

时间: 2024-10-03 23:04:46

操作系统思考 第九章 线程的相关文章

java并发的艺术-读书笔记-第九章线程池

使用线程池的好处: 1.降低资源消耗:减少了线程创建和销毁的资源消耗 2.提高响应速度,当任务到达时,线程可以不尽兴创建直接处理 3.提高线程的可管理性.使用线程池可以对线程进行统一的管理,监控,使用. 线程池的源码分析: public void execute(Runnable command){ if(command==null){ throw new NullPointerException(); } //如果执行线程数小于基本线程,则创建线程,并执行任务 if(poolsize>=cor

操作系统思考 第一章 编译

第一章 编译 作者:Allen B. Downey 原文:Chapter 1 Compilation 译者:飞龙 协议:CC BY-NC-SA 4.0 1.1 编译语言和解释语言 人们通常把编程语言描述为编译语言或者解释语言.前者的意思是程序被翻译成机器语言,之后由硬件执行:而后者的意思是程序被软件解释器读取并执行.例如,C被认为是编译语言,而Python被认为是解释语言.但是二者之间的界限并不总是那么明显. 首先,许多语言既可以编译执行也可以解释执行.例如,存在C的解释器,和Python的编译

Java 线程第三版 第九章 Thread调度 读书笔记

一.Thread调度的概述 import java.util.*; import java.text.*; public class Task implements Runnable { long n; String id; private long fib(long n) { if (n == 0) return 0L; if (n == 1) return 1L; return fib(n - 1) + fib(n - 2); } public Task(long n, String id)

第九章 C语言在嵌入式中的应用

上章回顾 编码的规范和程序版式 版权管理和申明 头文件结构和作用 程序命名 程序注释和代码布局规范 assert断言函数的应用 与0或NULL值的比较 内存的分配和释放细节,避免内存泄露 常量特性 [email protected]:Kevin-Dfg/[email protected]:Kevin-Dfg/Data-Structures-and-Algorithm-Analysis-in-C.git 第九章 第九章 C语言在嵌入式中的应用 C语言在嵌入式中的应用 [email protecte

[深入理解Android卷一全文-第九章]深入理解Vold和Rild

由于<深入理解Android 卷一>和<深入理解Android卷二>不再出版,而知识的传播不应该因为纸质媒介的问题而中断,所以我将在CSDN博客中全文转发这两本书的全部内容. 第9章  深入理解Vold和Rild 本章主要内容 ·  介绍Vold. ·  介绍Rild. 本章涉及的源代码文件名称及位置 下面是本章分析的源码文件名及其位置. ·  Main.cpp system/vold/Main.cpp ·  NetlinkManager.cpp system/vold/Netli

现代软件工程讨论第九章-十七章

第九章 9.5.1  PM们的故事 9.5.2  我是做PM 的料么? 在校学生如何为成为PM做准备 你是否觉得你的长处不在于写代码和debug,而是协调.沟通,让一个团队或组织有效运转起来?你是否喜欢表达,善于和各种专业背景的人沟通?你是否经常思考如何改进生活中点点滴滴的小问题?你会思考这样的问题么:新浪微博.豆瓣.qq.微信都可以社交,它们的定位.产品特性.用户群.解决的需求,有什么不同?你是否对以下领域感兴趣,甚至自己找过相关的书来看:心理学.社会学.组织行为学.统计学.商业模式? 如果你

《从0到1》笔记 第九章 基础决定命运

第九章 基础决定命运----每个成功企业都是独一无二的,而要做好每个事业,有些事情在起步阶段就必须做好:----基础没有打好的初创企业是无法挽救的. 开头很特殊,它在本质上有别于之后的阶段.如美国的<独立宣言>的制定,后来的几百年都只是小修改. 公司初创时,对合伙人的选择,制度的制定,产品的方向都是基础性的,决定企业最终命运的.现在我考虑投资一家初创公司时,会考察其创立团队.技术能力和才华互补固然重要,但创始人之间的了解程度和他们合作的默契程序也同样重要.创始人在共同创业前应有深厚的交情,否则

javascript高级程序设计 第九章-- 客户端检测

javascript高级程序设计 第九章-- 客户端检测 客户端检测是javascript开发中最具争议的一个话题,由于浏览器间存在差别,通常需要根据不同浏览器的能力分别编写不同的代码.有下列常使用的客户端检测方法:能力检测:在编写代码之前先检测特定浏览器的能力.例如,脚本在调用某个函数之前,可能要先检测该函数是否存在.这种检测方法将开发人员从考虑具体的浏览器类型和版本中解放出来,让他们把注意力集中到相应的能力是否存在上.能力检测无法精确地检测特定的浏览器和版本.怪癖检测:怪癖实际上是浏览器中存

第九章 用多线程来读取epoll模型下的客户端数据

#include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <assert.h> #include <stdio.h> #include <unistd.h> #include <errno.h> #include <string.h> #include