<OS Concepts 9th> Chap 4 线程与并发

本章目标

  • 明确线程的基本组成,以及与进程之间对比
  • 描述设计多线程的好处和挑战
  • 隐式多线程的多种方式:线程池、fork-join、Grand Central Dispatch
  • 描述windows和linux操作系统如何使用线程
  • 使用Pthreads、Java和Windows的线程API设计多线程应用

Abbr.

GCD: Grand Central Dispatch

TBB: Thread Building Blocks

TLS: Thread-Local Storage

LWP: LightWeight Process 

4.1 Overview

  • 线程是CPU调度的基本单元
  • 组成
    • 线程ID
    • PC(Program Counter)
    • 寄存器组
  • 同个进程下的线程共享
    • 代码段
    • 数据段
    • 其余OS资源,比如打开的文件和信号等
  • 好处
    • 响应度:一个线程宕机了别的线程可以继续执行
    • 资源共享:进程只能通过共享内存和信息传递共享资源,但线程默认是共享同个进程下的内存和资源的
    • 性价比高:创建线程分配内存和资源、切换上下文所需的消耗比进程少得多
    • 可拓展性scalability:多核架构下方便并行化

下图例子:客户端每发送一个请求,服务器就创建一个新的线程

 

4.2 多核编程

  • 多核编程的挑战
    • ·辨别可以分离、并发的任务
    • 平衡:并行化所带来的收益是否值得
    • 数据划分:如何将数据分配到不同的task里
    • 数据依赖:多个task之间的数据若有依赖性需要做好同步(chap 6)等操作
    • 测试和debug更具挑战性

Amdahl’s Law: 当我们对系统的某个部分加速时,其对系统整体性能的影响取决于该部分的重要性和加速程度

  • 并行化种类
    • 数据并行:每个task处理的数据是分隔开的
    • 任务并行:数据不分割,多个task可能处理同一数据

4.3 多线程模型

  • 线程类别
    • 用户线程
    • 内核线程:直接由OS支持和管理
  • 多线程模型
    • 多对一

      • 由用户管理,高效
      • 本质上还是只有一个kernel,并发性弱
      • 一个线程block就会导致整个进程block
      • 例子:Solaris Green Threads;GNU Portable Threads
    • 一对一

      • 用户开启的线程数量受到kernel资源的限制
      • 例子:Windows NT/XP/2000、Linux、Solaris 9 and later
    • 多对多

      • 灵活但实现困难
    • Two-level

      • 多对多,同时也允许一个用户线程绑定一个内核线程

4.4 线程库

线程库在用户曾难免提供创建和管理线程的API。

两种实现方式:库完全在用户层面;内核层面的库由OS直接支持。

以下代码段的例子为求和计算。

POSIX Pthreads

  • 提供用户层面和内核层面库。
  • A POSIX standard (IEEE 1003.1c) API for thread creation and synchronization
#include <pthread.h>
#include <stdio.h>

int sum;    // 线程间共享的数据
void *runner(void *param);  // 线程

int main(int argc, char *argv[]){
    pthread_t tid;
    pthread_attr_t attr;

    if(argc != 2){
        fprintf(stderr, "usage: a.out <integer value>\n");
        return -1;
    }
    if(atoi(atgv[1]) > 0){
        fprintf(stderr, "%d must be >= 0\n", atoi(argv[1]));
        return -1;
    }

    pthread_attr_init(&attr);   // 获取默认属性
    pthread_create(&tid, &attr, runner, argv[1]);   // 创建线程,产生对应id
    pthread_join(tid, NULL);    // 主程序main等待线程结束后继续

    printf("sum = %d\n", sum);
}

void *runner(void *param){
    int i, upper = atoi(param);
    sum = 0;

    for(i = 1; i <= upper; i ++)
        sum += i;
    
    pthread_exit(0);
}

Win32 

内核层面库。

#include <windows.h>
#include <stdio.h>

DWORD Sum;  // 线程间共享的数据

DWORD WINAPI Summation(LPVOID Param){
    DWORD Upper = *(DWORD*)Param;
    for(DWORD i = 0; i <= Upper; i ++)
        Sum += i;
    return 0;
}

int main(int argc, char *argv[]){
    DWORD ThreadId;
    HANDLE ThreadHandle;
    int Param;
    // 略一些参数检查

    // 创建线程
    ThreadHandle = CreateThread(
        NULL,   // 默认的安全属性
        0,      // 默认的栈的size
        Summation,  // 线程所要执行的函数
        &Param,     // 函数所需的参数
        0,          // 默认的创建flag
        &ThreadId   // 返回线程id
    );
    if(ThreadHandle != NULL){
        // 等待线程执行结束
        WaitForSingleObject(ThreadHandle, INFINITE);
        // 关闭线程句柄
        CloseHandle(ThreadHandle);
        printf("sum = %d\n", Sum);
    }
}

Java

直接通过Java程序创建和管理线程。可以

1. 创建一个新的class,继承Thread class并重写run 方法

2. 定义一个实现runnable interfave的class

4.5 隐式多线程

为了更高效更可靠地创建和管理线程,现在逐步将这些任务从开发者身上转移到编译器和run-time库,这种策略就叫做隐式多线程(Implicit Threading)。

线程池

创建一定数量的线程放到pool里待命,有request就在pool里找空闲的线程执行。线程执行完后会再次被放进pool里。如果没有空闲的线程,task会被放到一个队列里等待被执行。

Fork Join

在chap 3中系统调用fork表示创建一个新的进程,在多线程程序下,fork和exec的语义有所不同。

一个线程fork,有两种情形,如果fork之后立即执行exec,则只复制该线程,否则,会复制线程所在进程下的所有线程。

 

OpenMP

Open Multi-Processing

参考:(46条消息) 并行编程OpenMP基础及简单示例_-牧野-的博客-CSDN博客_openmp

OpenMP是一种用于共享内存并行系统的多线程程序设计方案,支持的编程语言包括C、C++和Fortran。OpenMP提供了对并行算法的高层抽象描述,特别适合在多核CPU机器上的并行程序设计。编译器根据程序中添加的pragma指令,自动将程序并行处理,使用OpenMP降低了并行编程的难度和复杂度。当编译器不支持OpenMP时,程序会退化成普通(串行)程序。程序中已有的OpenMP指令不会影响程序的正常编译运行。

例如,运行以下代码,系统有几个core就会产生几个线程,输出几次。

#pragma omp parallel
{
printf("I am a parallel region.");
}

Grand Central Dispatch

由Apple开发,通过将任务放置到派遣队列dispatch queue里来进行运行时调度。略。

Intel TBB

由C++编写的设计并行程序的模板库,不需要特殊的编译器或语言。不必关注线程,专注任务本身;抽象层仅需很少的接口代码;多平台适用

4.6 线程问题

  • fork 和 exec的语义问题:见前文
  • 信号处理
    • 信号用来通知一个进程某个特定的事件发起了
    • 方式
      • 发送给指定线程/所有线程
      • 可以指定一个线程来接受进程监听的所有信号
  • 线程取消
    • 在线程结束前终止
    • 方式
      • 异步取消操作立即取消目标线程 pthread cancel(tid);
      • deferred 取消周期性地检查目标线程是否需要取消 pthread testcancel();
  • 线程局部存储thread-local storage (or TLS)
    • 线程特有的数据
  • 调度激活
    • 定义:用户线程库和内核之间的通信
    • 多对多和TwoLevel模型都需要通信,以维护分配给应用程序的适当数量的内核线程
      • 调度程序激活提供了回调upcall——从内核到线程库的通信机制; 这种通信允许应用程序维护正确的内核线程数
    • Lightweight process (LWP): 用户线程和内核线程之间的中间数据结构,每个LWP对应一个内核线程
    • CPU-bound程序使用一个线程(一个LWP)就足够;I/O-bound程序一般需要多个LWP来执行,例如打印机。

 

4.7 OS例子

Windows XP Threads

 

用户层面:

TEB(thread environment block): 线程环境块

内核层面:

Ethread:执行线程块

Kthread:内核线程块

如上图,各个块之间通过指针关联。

Linux Threads

Linux不区分进程和线程,都看作task看待。

系统调用clone用来创建新的task,创建时通过flag指定子任务和父任务共享多少资源。

 Takeaways

  1. 四种多线程模型
  2. 一些常用线程库
  3. 线程和进程之间的资源关系
    1. 一个进程下的所有线程共享的数据有哪些?
    2. 哪些是线程specific的数据?
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值