认识C语言的线程

文章目录

      • 1. 线程背景知识
        • 1.1 串行计算与并行计算
        • 1.2 并行计算的内存架构
        • 1.3 线程模型
        • 1.4 进程和线程之的区别
        • 1.5 为什么要多线程
      • 2. POSIX线程(pthread)库
        • 2.1 线程基础
        • 2.2 线程创建和终止
        • 2.3 线程同步
      • 参考文献

1. 线程背景知识

1.1 串行计算与并行计算

    传统上,软件是为串行计算而编写的,有以下特点:

  • 一个问题被分解成一系列离散的指令
  • 指令依次执行
  • 在单个处理器上执行
  • 任何时候只能执行一条指令
    认识C语言的线程_第1张图片

    并行计算同时使用多个计算资源来解决计算问题。计算资源通常是具有多个处理器/核心的单台计算机,或者是通过网络连接的任意数量的此类计算机。并行计算的特点是:

  • 一个问题被分解成可以同时解决的多个部分
  • 每一部分都进一步细分为一系列说明(Instructions)
  • 每个部分的指令在不同的处理器上同时执行
  • 采用全面控制/协调机制

认识C语言的线程_第2张图片

1.2 并行计算的内存架构

    并行计算机的内存架构分为3种:共享内存、分布式内存和混合分布式共享内存。基于内存访问时间,共享内存分为 UMA 和 NUMA。
认识C语言的线程_第3张图片

共享内存(UMA)

认识C语言的线程_第4张图片

共享内存(NUMA)

    共享内存的优点是全局地址空间为存储器提供了用户友好的编程视角,同时由于内存与CPU接近,因此任务之间的数据共享既快速又统一。共享内存的缺点是内存和CPU之间缺乏可伸缩性。
    分布式内存系统需要通信网络来连接处理器间内存。因为每个处理器都有自己的本地内存,所以它独立运行,对其本地内存所做的更改不会影响其他处理器的内存。因此,缓存一致性的概念不适用。
认识C语言的线程_第5张图片
    其优点是:1、内存可随处理器数量扩展。增加处理器数量,内存大小按比例增加。2、每个处理器都可以快速访问自己的内存,而不会产生干扰,也不会尝试维护全局缓存一致性而产生开销。缺点是内存访问时间不一致。与节点本地数据相比,驻留在远程节点上的数据需要更长的访问时间。

1.3 线程模型

    并行编程模型作为硬件和内存体系结构之上的抽象而存在。几种常用的并行编程模型如下:

  • 无线程的共享内存(Shared Memory (without threads))
  • 线程(Threads)
  • 分布式内存/消息传递(Distributed Memory / Message Passing)
  • 数据并行(Data Parallel)
  • Hybrid
  • 单程序多数据(SPMD)
  • 多程序多数据(MPMD)
    认识C语言的线程_第6张图片

    线程模型是共享内存编程的一种。在并行编程的线程模型中,一个“重”(heavy weight)进程可以有多个“轻”(light weight)并发执行路径。例如:

  • 主程序 a.out 计划由本机操作系统运行。a.out 加载并获取运行所需的所有系统和用户资源。这是“heavy weight”过程。
  • a.out 执行一些串行工作,然后创建许多任务(线程),这些任务可以由操作系统同时调度和运行。
  • 每个线程都有本地数据,但也共享 a.out 的全部资源。这避免了为每个线程复制程序资源。每个线程还可以从全局内存视图中受益,因为它们共享 a.out 的内存空间。
  • 最好将线程的工作描述为主程序中的子程序。任何线程都可以与其他线程同时执行任何子程序。
  • 线程通过全局内存(更新地址位置)相互通信。这需要同步结构来确保同一时刻不会有多个线程更新相同的全局地址。
  • 线程可以产生或消亡,但是 a.out 需要一直存在,以提供必要的共享资源,直到应用程序结束。

    从编程的角度来看,线程实现方式通常包括:

  • 从并行程序的源代码中调用子程序库
  • 嵌入在串行或并行源代码中的一组编译器指令

    在这两种情况下,程序员负责确定并行性(尽管编译器有时可以提供帮助)。从历史上看,硬件供应商拥有各自专有的线程版本,这些版本实现方式彼此不同,这使程序员难以开发可移植的线程应用程序。相互独立的标准化工作导致了两种非常不同的线程实现:POSIX Threads 和 OpenMP。

1.4 进程和线程之的区别

    因为线程具有进程的某些属性,所以有时将它们称为轻量级进程。线程不像进程那样彼此独立,因为线程与其他线程共享它们的代码段,数据段和OS资源(如打开的文件和信号)。但是,与进程一样,线程具有其自己的程序计数器(PC),寄存器集和堆栈空间。

1.5 为什么要多线程

    线程是通过并行性改善应用程序的流行方法。例如,在浏览器中,多个选项卡可以是不同的线程。MS word使用多个线程,一个线程设置文本格式,其他线程处理进程输入,等等。
由于以下原因,线程比进程运行快:
1)线程创建快得多。
2)线程之间的上下文切换要快得多。
3)线程可以轻松终止。
4)线程之间的通信更快。
    例如,下表比较了fork()子程序和pthread_create()子程序的计时结果。时间反映50000个进程/线程创建,使用time命令测试,单位为秒。
    注意:不要期望sys和user时间加起来是real time,因为这些是多个CPU/内核同时处理问题的SMP系统。充其量,这些是过去和现在在本地计算机上运行的近似值。
认识C语言的线程_第7张图片
source code
    编译执行过程如下:

# gcc thread.c -lpthread -o thread
# gcc fork.c -o fork
# time ./thread
real    0m0.714s
user    0m0.137s
sys     0m0.379s
# time ./fork
real    0m3.918s
user    0m2.490s
sys     0m1.209s

2. POSIX线程(pthread)库

2.1 线程基础

    一个进程可以包含多个线程,所有线程都执行相同的程序。这些线程共享相同的全局内存(数据段和堆段),但是每个线程都有自己的栈(存放自动变量)。
    线程操作包括线程创建,终止,同步(joins,blocking),调度,数据管理和进程交互。线程不维护创建线程的列表,也不知道创建它的线程。同一进程中的线程共享:

  • 进程指令
  • 大多数数据
  • 打开的文件(描述符)
  • 信号和信号句柄
  • 当前工作目录
  • 用户和组id

    每个线程都有一个唯一的:

  • 线程ID
  • 一组寄存器,堆栈指针
  • 本地变量的堆栈,返回地址
  • 信号掩码
  • 优先级
  • 返回值:errno

    如果一切正常,pthread函数返回“0”。

2.2 线程创建和终止

    pthread_create() 语法:

int pthread_create(pthread_t * thread, 
                       const pthread_attr_t * attr,
                       void * (*start_routine)(void *), 
                       void *arg);

    thread返回线程ID。attr参数指向pthread_attr_t结构,该结构在线程创建时用于确定新线程的属性,使用pthread_attr_init和相关函数初始化。如果attr为NULL,则使用默认属性创建线程。pthread_create() 创建新线程后执行 start_routine() 函数,arg作为的唯一参数被传递给 start_routine() 函数,要传递多个参数,请发送指向结构的指针。
    线程以下列方式之一终止:

  • 调用pthread_exit(),指定退出状态。该退出状态可用于同一进程其他线程调用 pthread_join() 函数。
  • 从start_routine() 终止。这等效于使用return语句中提供的值调用 pthread_exit()。
  • 使用 pthread_cancel() 取消。
  • 进程中的任何线程都调用exit,或者main()函数调用return命令。这将导致进程中所有线程的终止。

    pthread_exit调用将导致当前线程退出并释放其占用的所有线程特定资源。
    示例:

#include 
#include 
#include 

void * PrintHello(void * data)
{
    int my_data = (int)data;

    printf("\n Hello from new thread - got %d !\n", my_data);
    printf("Before pthread_exit\n");
    pthread_exit(NULL);
    printf("After PrintHello() pthread_exit\n");
}

int main()
{
    int rc;
    pthread_t thread_id;

    int t1 = 11, t2 = 12;

    rc = pthread_create(&thread_id, NULL, PrintHello, (void*)t1);
    if(rc)
    {
      printf("\n ERROR: return code from pthread_create is %d \n", rc);
      exit(1);
    }
    printf("\n Created new thread (%u)... \n", thread_id);
    printf("After one pthread_create\n");

    rc = pthread_create(&thread_id, NULL, PrintHello, (void*)t2);
    printf("\n Created new thread (%u)... \n", thread_id);
    printf("After second pthread_create\n");

    pthread_exit(NULL);
    printf("After pthread_exit\n");
}

    编译执行时,会发现执行顺序是先执行主程序,再执行线程。输出完主程序结果,再输出子进程结果,并且输出线程的顺序是不固定的。

# gcc printHello.c -o printHello -lpthread
# ./printHello 

 Created new thread (56424192)... 
After one pthread_create

 Created new thread (48031488)... 
After second pthread_create

 Hello from new thread - got 11 !
Before pthread_exit

 Hello from new thread - got 12 !
Before pthread_exit
# ./printHello 

 Created new thread (4150454016)... 
After one pthread_create

 Created new thread (4142061312)... 
After second pthread_create

 Hello from new thread - got 12 !
Before pthread_exit

 Hello from new thread - got 11 !
Before pthread_exit

    将main()函数中的pthread_exit()函数注释掉

    //pthread_exit(NULL);
    printf("After pthread_exit\n");

    重新编译并运行,发现并主程序执行完成后就停止了,并不会执行线程代码:

# ./printHello    

 Created new thread (3315189504)... 
After one pthread_create

 Created new thread (3306796800)... 
After second pthread_create
After pthread_exit

2.3 线程同步

    线程库提供了三种同步机制:

  • 互斥锁(mutexes)-阻止其他线程访问变量。这将强制线程对变量或变量集进行独占访问。
  • 连接(joins)-创建一个线程,该线程负责等待其他线程完成。
  • 条件变量(condition variables)-数据类型pthread_cond_t

    互斥锁用于防止由于竞争条件导致的数据不一致。当两个或多个线程需要在同一内存区域上执行操作时,通常会出现竞争条件,但计算结果取决于执行这些操作的顺序。互斥量用于序列化共享资源。当一个全局资源被多个线程访问时,该资源应该有一个与之关联的互斥锁。可以应用互斥锁来保护内存段(“关键区域”,“critical region”)不受其他线程的影响。互斥锁只能应用于单个进程中的线程,而不像信号量那样在进程之间工作。
    “连接”(Joining)是完成线程之间同步的一种方法。
认识C语言的线程_第8张图片
    pthread_join()阻塞调用线程,直到指定Thread ID的线程终止。
    如果在目标线程对pthread_exit()的调用中指定了目标线程的终止返回状态,则程序员可以获取该状态。
    连接线程可以匹配一个pthread_join()调用。在同一线程上尝试多个联接是一个逻辑错误。
    条件变量为线程提供了另一种同步方式。互斥锁通过控制线程对数据的访问来实现同步时,条件变量允许线程根据数据的实际值进行同步。如果没有条件变量,程序员需要让线程不断地轮询(可能在关键部分),以检查条件是否满足。这可能会非常消耗资源,因为线程在该活动中会持续繁忙。条件变量是实现相同目标的一种方法,无需轮询。
    条件变量总是与互斥锁一起使用。
    系统级编程个人感觉较为复杂,目前仅仅接触了一点皮毛,差距还是很大的,还需要继续学习才能掌握。

参考文献

[1]Blaise Barney, Lawrence Livermore National Laboratory.Introduction to Parallel Computing[EB/OL].https://computing.llnl.gov/tutorials/parallel_comp/,2020-06-12.
[2]Blaise Barney.POSIX Threads Programming[EB/OL].https://computing.llnl.gov/tutorials/pthreads/,2020-06-12.
[3]Rahul Jain.Multithreading in C[EB/OL].https://www.geeksforgeeks.org/multithreading-c-2/,2020-01-01.
[4]Greg Ippolito.POSIX thread (pthread) libraries[EB/OL].https://www.cs.cmu.edu/afs/cs/academic/class/15492-f07/www/pthreads.html,2020-01-01.
[5]NULL.POSIX Threads[EB/OL].http://www.csc.villanova.edu/~mdamian/threads/posixthreads.html,2020-01-01.
[6]Linux man-pages project.PTHREAD_CREATE(3)[EB/OL].https://man7.org/linux/man-pages/man3/pthread_create.3.html,2020-06-09.
[7]Linux man-pages project.PTHREAD_JOIN(3)[EB/OL].https://man7.org/linux/man-pages/man3/pthread_join.3.html,2020-06-09.

你可能感兴趣的:(C)