Lec 10 线程

License

本内容版权归上海交通大学并行与分布式系统研究所所有

使用者可以将全部或部分本内容免费用于非商业用途

使用者在使用全部或部分本内容时请注明来源

资料来自上海交通大学并行与分布式系统研究所+材料名字

对于不遵守此声明或者其他违法使用本内容者,将依法保留追究权

本内容的发布采用 Creative Commons Attribution 4.0 License

完整文本

1 为什么需要线程

  • 进程的开销较大

    • 包括了数据,代码,堆栈等。
  • 进程的隔离性过强
    • 通过进程间通信(IPC),但是开销太大
  • 进程内部无法支持并行。

1.1 简单方法:进程+调度

  • 进程数量远远超过CPU的核数目

    • 简单分配,每个核都至少分到一个进程
  • 调度器分时复用,增加计算资源利用的效率
    • 通过调度策略,在进程需要等待的时候切换到其他进程执行。

  • 局限: 但一进程无法利用多核资源

    • 一个进程同一时刻只能被调度到其中一个核上运行
    • 如果一个程序想要同时利用多核怎么办?
    • Sol:采用fork()创建相似进程。创建的进程与原来的进程行为类似,可以用于其他核心的运行。

1.2 Fork 方法存在局限性

  • 进程间隔离过强,数据共享十分困难

    • 每个进程具有独立的虚拟地址空间,共享以页为粒度
    • 协调困难,需要复杂的通信机制。(pipe)
  • 进程管理开销大
    • 创建:地址空间的复制
    • 切换:页表切换

1.3 如何使得进程跨核心运行

  • Pros:无需使用fork创建新的进程

    • 降低进程管理的开销
    • 同一个地址空间数据共享和同步方便
  • 需要什么支持?
    • 处理器上下文:不同核心执行状态不同,需要独立处理器的上下文。

1.4 线程:更加轻量级的运行时抽象

  • 仅包含运行时状态

    • 静态部分通过进程提供
    • 包含了执行所需的最小状态
  • 一个进程可以包含多个线程

    • 每个线程共享同一个地址空间
    • 允许进程内并行

2 如何使用线程

  • 常用库:POSIX threads(pthreads)

    • 包含约60个函数的标准接口
    • 实现的功能与进程相关系统调用相似
      • 创建:pthread_create
      • 回收:pthread_join
      • 退出:pthread_exit
      • 暂停:pthread_yield
  • 注意:一个线程执行系统调用,可能影响该进程的所有线程

    • 如exit会使所有线程退出
/* 创建线程,打印"hello world!" */
#include <pthread.h>
#include <stdio.h> /* 进程执行 */
// 子线程。
void *thread(void *args)
{
// detach 分离线程
pthread_detach(pthread_self());
printf("Hello world!\n");
return;
} int main(int args, char *argv[])
{
pthread_t tid;
// 创建线程接口,赋予线程id,执行起点为thread函数
// 属性通常为NULL,参数在第二个NULL传入。
// 主线程。
pthread_create(&tid, NULL, thread, NULL);
// 解决线程提前结束的情况:等待对应的tid子线程结束后继续进行。
pthread_join(tid, NULL); // 第二个参数接受返回值。
exit(0);
}
  • 有时候没有输出!
  • 主线程创建子线程后,两线程独立执行
  • 若子线程先于exit执行,则printf顺利输出
  • exit会导致主线程和子线程全部终止。有可能会导致没有执行完成printf
  • 解决办法:加入join操作。

2.1 基于join的方法存在问题

  • 手动调用join回收资源,有可能导致资源溢出。(例如多次while循环创建进程导致报错:(stuck; errno:11)
  • 采用detach()操作:在线程函数内第一行增加pthread_detach(pthread_self());一行来完成分离。分离后的线程不会被其他线程杀死或回收,退出时资源自动回收。
  • detach后因为子线程与主线程分离,相当于进入了幕后,因此无法跟踪,就不能再使用join操作了。
  • 将join操作改为detach操作,我们发现有时候还是没有输出结果。这是因为main函数返回后有隐式调用的exit操作终止所有线程。因此并没有使得子线程完全独立。
  • 我们可以改成:将join改为detach后,在后面加上pthread_exit(0),只退出当前线程。输出为Hello, world!\n(stuck)
#include <pthread.h>
#include <stdio.h>
void *thread(void *vargp) {
printf("Hello, world!\n");
while(1);
return NULL;
}
int main() {
pthread_t tid;
pthread_create(&tid, NULL, thread, NULL);
pthread_detach(tid);
pthread_exit(0); // 只退出当前线程。
}

2.2 小结

  • 常用接口:pthread_create, pthread_join, pthread_detach, pthread_exit
  • 线程资源默认手动回收
    • 可以使用pthread_join回收其他子线程
    • 可以使用pthread_detach+pthread_exit来自动回收其他线程
    • 将exit改为pthread_exit来仅退出主线程。

3 线程

3.1 线程历史

略过,感兴趣自行STFW(search the fu***** fantastic web)。

3.2 多线程的进程

  • 一个进程可以拥有多个线程

  • 多线程进程可以跨处理器执行。

    • 调度基本单元从进程变成了线程。
    • 每个线程都有自己的执行状态。
    • 切换的单位从进程变成了线程。
  • 每个线程都有自己的栈

  • 内核中也有为线程准备的内核栈

  • 其他区域共享(数据,代码,堆)

3.3 对比进程与线程

  • 线程和进程的相似之处:

    • 都可以与其他进程/线程并发执行(可能在不同核心上)
    • 都可以进行切换
      • 引入线程后,调度管理单位由进程变为线程
  • 线程和进程的不同之处:

    • 同一进程的不同线程共享代码和部分数据

      • 不同进程不共享虚拟地址空间
    • 线程与进程相比开销较低
      • 进程控制(创建和回收)通常比线程更耗时
      • Linux的数据:
        • 创建和回收进程:~20K cycle
        • 创建和回收线程:~10K cycle(或更少)

4 TLS:线程本地存储

观察下面的程序:

#include <errno.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
void *thread(void *vargp) {
void *addr = malloc(0);
printf("peer:errno=%d\n", errno);
return NULL;
}
int main() {
pthread_t tid;
pthread_create(&tid, NULL, thread, NULL);
void *addr = malloc(-1);
pthread_join(tid, NULL);
printf("main:errno=%d\n", errno);
}

我们会发现输出结果可能为两种情况:

# 第一种情况:
peer:errno=0
main:errno=0
# 第二种情况:
peer:errno=0
main:errno=12

前者是由于主线程执行后,再执行子线程导致的。errno为一个全局变量,因此子线程将其修改成了0。而后者先执行了子线程,再继续执行主线程。因此errno的值分别为0和12。这是因为每个线程具有不同的虚拟地址空间,这些不同的虚拟地址空间映射到了相同的真实物理地址。

5 线程的实现

5.1 进程控制块PCB到线程控制块TCB

  • PCB的部分内容转移到TCB

    • 每个线程TCB保存自己的处理器上下文,内核栈,退出/执行状态
    • PCB维护共享地址空间
    • PCB与TCB相互引用
  • 每个线程适应独立的内核栈

5.2 进行线程创建

比进程创建步骤少(无需加载可执行文件)。

  1. TCB相关内容初始化
  2. 维护进程,线程关系
  3. 准备运行环境。

Linux中采用clone(本用于创建进程)实现。

创建进程需要多个特殊标记。

  • CLONE_VM: 线程(进程?)共享同一地址空间
  • CLONE_THREAD: 新的线程(进程?)与原进程同时属于同一进程。

为什么这里会打上问号?因为Linux内部实际上并没有抽象一个新的模型来描述线程,而是以轻量级进程来将他们和线程相互关联起来。

5.3 进程退出与合并

不需要销毁虚拟地址空间vmspace



5.4 与进程管理接口的关系

  • 一个多线程的程序调用fork会出现什么情况?

    • 所有线程都被拷贝导致重复读/写同一个文件
  • 只拷贝了父进程中调用fork的线程
    • 新进程中只出现了一个线程,不会出现反直觉的重复操作
    • 其他线程内存状态被拷贝并且不被主动释放
  • posix:尽量不要使用fork拷贝多线程
    • 希望使用多进程:使用fork
    • 希望多线程:pthread_create

5.5 用户态线程与内核态线程

  • 根据线程是否受到内核管理将线程分为两类

    • 内核态线程:内核可见,受到内核管理
    • 用户态线程:内核不可见,不受内核直接管理
  • 内核态线程
    • 内核创建,线程相关信息存放在内核中
  • 用户态线程(纤程)
    • 在应用态创建,线程相关信息主要存放在应用数据中

5.6 线程模型

多对一模型
  • 将多个用户态线程映射给单一的内核线程

    • pros:管理内核简单
    • cons:可扩展性差,无法适应多核机器的发展
    • 主流操作系统中被弃用,用于各种用户态线程库中
一对一模型
  • 每个用户线程映射单一的内核线程

    • 优点:解决了多对一模型中的可扩展问题
    • 缺点:数量大,开销大
  • 主流OS采用
多对多模型(Scheduler Activation)
  • N个用户态线程映射到M个内核态进程(N>M)

    • 优点:解决了可扩展性问题(多对一)和线程过多问题(一对一)
    • 缺点:管理复杂
  • 虚拟化中广泛应用

5.7 TCB

  • 一对一的模型结构TCB氛围两部分
  • 内核态和PCB结构相似,进程和线程在linux中使用的是同一种数据结构,线程切换中使用
  • 应用态使用线程库定义。例如pthread结构体,可以认为是TCB(内核)的扩展

Lec 10 线程的更多相关文章

  1. java基础(10) -线程

    线程 相当于轻量级进程,线程在程序中是独立的.并发的执行路径,每个线程有它自己的堆栈.自己的程序计数器和自己的局部变量.但是,与分隔的进程相比,进程中的线程之间的隔离程度要小.它们共享内存.文件句柄和 ...

  2. 10.线程通信CountDownLatch

    CountDownLatch 1.一个同步的辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个.多个线程去一直等待,用给定的计数.初始化“CountDownLatch”. 由于调用 count ...

  3. 算法导论——lec 10 图的基本算法及应用

    搜索一个图是有序地沿着图的边訪问全部定点, 图的搜索算法能够使我们发现非常多图的结构信息, 图的搜索技术是图算法邻域的核心. 一. 图的两种计算机表示 1. 邻接表: 这样的方法表示稀疏图比較简洁紧凑 ...

  4. 《java.util.concurrent 包源码阅读》10 线程池系列之AbstractExecutorService

    AbstractExecutorService对ExecutorService的执行任务类型的方法提供了一个默认实现.这些方法包括submit,invokeAny和InvokeAll. 注意的是来自E ...

  5. Java线程的概念

    1.      计算机系统 使用高速缓存来作为内存与处理器之间的缓冲,将运算需要用到的数据复制到缓存中,让计算能快速进行:当运算结束后再从缓存同步回内存之中,这样处理器就无需等待缓慢的内存读写了. 缓 ...

  6. Java并发编程:线程池的使用

    Java并发编程:线程池的使用 在前面的文章中,我们使用线程的时候就去创建一个线程,这样实现起来非常简便,但是就会有一个问题: 如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了, ...

  7. Java线程:概念与原理

    Java线程:概念与原理 一.操作系统中线程和进程的概念 现在的操作系统是多任务操作系统.多线程是实现多任务的一种方式. 进程是指一个内存中运行的应用程序,每个进程都有自己独立的一块内存空间,一个进程 ...

  8. Java线程:线程状态的转换

    Java线程:线程状态的转换   一.线程状态   线程的状态转换是线程控制的基础.线程状态总的可分为五大状态:分别是生.死.可运行.运行.等待/阻塞.用一个图来描述如下:   1.新状态:线程对象已 ...

  9. TCMalloc:线程缓冲的Malloc

    这段时间比较闲,研究下内存管理,从官方文档开始啃起<TCMalloc : Thread-Caching Malloc>. 1.动机 TCMalloc要比glibc 2.3的malloc(可 ...

  10. 深入理解Java之线程池

    原作者:海子 出处:http://www.cnblogs.com/dolphin0520/ 本文归作者海子和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则 ...

随机推荐

  1. centos8安装puppeteer

    背景 我计划使用puppeteer爬点html数据,结果windows11上没问题 但在我的服务器centos8上确报错. [root@104 auto-task]# npm run start &g ...

  2. vue打包后文件自定定义命名规则

    js文件和svg以及图片 module.exports = { chainWebpack: config = >{ const svgRule = config.module.rule('svg ...

  3. 模块化html代码

    HTML 页面模块化实现方案 将 HTML 页面进行模块化是前端开发中的常见需求,它可以提高代码的可维护性和复用性.针对你提供的这个建筑辅助设计工具集合页面,我们可以采用多种模块化方案,下面我将介绍几 ...

  4. POLIR-Society--Networking- {关系、理论与管理}: 权利 - Giving(给于)+Taking(取得)+运用好+"人性"

    POLIR-Society--Networking- {关系.理论与管理}: 权利 - Giving(给于)+Taking(取得) 运用好"权利"+"人性" E ...

  5. Statistics110@Harvard: Clarity•Honesty•Words•Practices: MATH=LOGIC OF CERTAINTY and STATISTICS=LOGIC OF UNCERTAINTIES

    Statistics 110 of Harvard University: Math is the logic of certainty, Statistics is the logic of unc ...

  6. Win10专业版无线网络老是掉线的问题

    有一位电脑基地的用户,使用win10专业版系统笔记本电脑的时候,总是出现无线网卡掉线的问题,这该怎么办呢?接下来,技术员小编就来分享具体的解决方法. Win10 专业版下无线总是掉线,可能是由电源管理 ...

  7. Unity计时器系统

    很好用,方便 using System; using System.Collections.Generic; using UnityEngine; public class TimerManager ...

  8. vue03-directives 指令

    directives 指令 v-for 循环 v-on:click 点击事件 v-model model绑定 methods 方法 const app = new Vue({ el : '#app', ...

  9. Freemarker的时间相关

    1.freemarker的时间转换 //标准日期转日期字符串 ${parameters.fieldDate?date} //标准日期转日期+时间 字符串 ${parameters.fieldDate? ...

  10. 会话跟踪技术之Cookie、Session 和 Token三种技术对比

    Cookie.Session 和 Token 是解决 HTTP 无状态性的三种核心会话技术,各有其原理.优缺点和适用场景.以下综合分析: ​​1. Cookie​​ ​​原理​​ ​​存储位置​​:客 ...