传统上,软件是为串行计算而编写的,有以下特点:
并行计算同时使用多个计算资源来解决计算问题。计算资源通常是具有多个处理器/核心的单台计算机,或者是通过网络连接的任意数量的此类计算机。并行计算的特点是:
并行计算机的内存架构分为3种:共享内存、分布式内存和混合分布式共享内存。基于内存访问时间,共享内存分为 UMA 和 NUMA。
共享内存的优点是全局地址空间为存储器提供了用户友好的编程视角,同时由于内存与CPU接近,因此任务之间的数据共享既快速又统一。共享内存的缺点是内存和CPU之间缺乏可伸缩性。
分布式内存系统需要通信网络来连接处理器间内存。因为每个处理器都有自己的本地内存,所以它独立运行,对其本地内存所做的更改不会影响其他处理器的内存。因此,缓存一致性的概念不适用。
其优点是:1、内存可随处理器数量扩展。增加处理器数量,内存大小按比例增加。2、每个处理器都可以快速访问自己的内存,而不会产生干扰,也不会尝试维护全局缓存一致性而产生开销。缺点是内存访问时间不一致。与节点本地数据相比,驻留在远程节点上的数据需要更长的访问时间。
并行编程模型作为硬件和内存体系结构之上的抽象而存在。几种常用的并行编程模型如下:
线程模型是共享内存编程的一种。在并行编程的线程模型中,一个“重”(heavy weight)进程可以有多个“轻”(light weight)并发执行路径。例如:
从编程的角度来看,线程实现方式通常包括:
在这两种情况下,程序员负责确定并行性(尽管编译器有时可以提供帮助)。从历史上看,硬件供应商拥有各自专有的线程版本,这些版本实现方式彼此不同,这使程序员难以开发可移植的线程应用程序。相互独立的标准化工作导致了两种非常不同的线程实现:POSIX Threads 和 OpenMP。
因为线程具有进程的某些属性,所以有时将它们称为轻量级进程。线程不像进程那样彼此独立,因为线程与其他线程共享它们的代码段,数据段和OS资源(如打开的文件和信号)。但是,与进程一样,线程具有其自己的程序计数器(PC),寄存器集和堆栈空间。
线程是通过并行性改善应用程序的流行方法。例如,在浏览器中,多个选项卡可以是不同的线程。MS word使用多个线程,一个线程设置文本格式,其他线程处理进程输入,等等。
由于以下原因,线程比进程运行快:
1)线程创建快得多。
2)线程之间的上下文切换要快得多。
3)线程可以轻松终止。
4)线程之间的通信更快。
例如,下表比较了fork()子程序和pthread_create()子程序的计时结果。时间反映50000个进程/线程创建,使用time命令测试,单位为秒。
注意:不要期望sys和user时间加起来是real time,因为这些是多个CPU/内核同时处理问题的SMP系统。充其量,这些是过去和现在在本地计算机上运行的近似值。
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
一个进程可以包含多个线程,所有线程都执行相同的程序。这些线程共享相同的全局内存(数据段和堆段),但是每个线程都有自己的栈(存放自动变量)。
线程操作包括线程创建,终止,同步(joins,blocking),调度,数据管理和进程交互。线程不维护创建线程的列表,也不知道创建它的线程。同一进程中的线程共享:
每个线程都有一个唯一的:
如果一切正常,pthread函数返回“0”。
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调用将导致当前线程退出并释放其占用的所有线程特定资源。
示例:
#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
线程库提供了三种同步机制:
互斥锁用于防止由于竞争条件导致的数据不一致。当两个或多个线程需要在同一内存区域上执行操作时,通常会出现竞争条件,但计算结果取决于执行这些操作的顺序。互斥量用于序列化共享资源。当一个全局资源被多个线程访问时,该资源应该有一个与之关联的互斥锁。可以应用互斥锁来保护内存段(“关键区域”,“critical region”)不受其他线程的影响。互斥锁只能应用于单个进程中的线程,而不像信号量那样在进程之间工作。
“连接”(Joining)是完成线程之间同步的一种方法。
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.