20191226刘煊赫

  博客园 :: 首页 :: 博问 :: 闪存 :: 新随笔 :: 联系 :: 订阅 订阅 :: 管理 ::

-

第四章、并发编程

一、并行计算导论

在早期,大多数计算机只有一个处理组件,称为处理器或中共处理器(CPU)。受这种硬件条件的限制,计算机程序通常是为串行计算机编写的。

  1. 顺行算法与并行算法

   在描述顺序算法时,常用的方法是用一个begin-end代码块列出算法,begin-end代码块中的顺序算法可能包含多个步骤。

  1. 并行性与突发性

   通常,并行算法只识别并可执行的任务,但是它没有规定如何将任务映射到处理组件。在理想情况下,并行算法中的所有任务都应该同时实时执行。然而,真正的并行执行只能在有多个处理器组件的系统中实现,比如多处理器或多核系统。在单CPU系统中,一次只能执行一个任务。

二、线程

  1. 线程的原理

   一个操作系统(OS)包含许多并发进程。在进程模型中,进程是独立的执行单元。所有进程均在内核模式或用户模式下执行。在内核模式下,各进程在唯一地址空间上执行,与其他进程是分开的。虽然每个进程都是一个独立的单元,但是它只有一个执行路径。

  1. 线程的优点

   与进程相比,线程有许多优点:

(1)线程创建和切换速度更快

     进程的上下文复杂而庞大。其复杂性主要来自管理进程映像的需要。例如,在具有虚拟内存的系统中,进程映像可能由叫做页面的许多内存单元组成。在执行过程中,有些页面在内存中,有些则不在内存中。操作系统内核必须使用多个页表和多个级别的硬件辅助来跟踪每个进程的页面。

(2)线程的相应速度更快:一个进程只有一个执行路径。当某个进程被挂起时,整个进程都将停止执行。相反,当某个线程被挂起时,同一进程的其他进程可以继续执行.

(3)线程更适合并行计算

     并行计算的目标是使用多个执行路径更快地解决问题。基于分治原则(如二叉树查找和快速排序等)的算法经常表现出高度的并行性,可通过使用并行或并发执行来提高计算速度。这种算法通常要求执行实体共享公用数据。在进程模型中,各进程不能有效共享数据,因为它们的地址空间都不一样。为了解决这个问题,进程必须使用进程间通信(IPC)来交换数据或使用其他方法将公用数据区包含到其地址空间中。相反,同一进程中的所有线程共享同一地址空间中的所有(全局)数据。因此,使用线程编写并行执行的程序比使用进程编写更简单、更自然。

  1. 线程的缺点

   另一方面,线程也有一些缺点,其中包括:

(1)由于地址空间共享,线程需要来自用户的明确同步。

(2)许多库函数可能对线程不安全,例如传统strtok()函数将一个字符串分成一连串令牌。通常,任何使用全局变量或依赖于静态内存内容的函数,线程都不安全。为了使库函数适应线程环境,还需要做大量的工作。

(3)在单CPU系统上,使用线程解决问题实际上要比使用顺序程序慢,这是由在运行时创建线程和切换上下文的系统开销造成的。

三、线程操作

线程的执行轨迹与进程类似。线程可在内核模式或用户模式下。在用户模式下,线程在进程相同的相同地址空间中执行,但每个线程都有自己的执行堆栈。线程是独立的执行单元,可根据操作系统内核的调度策略,对内核进行系统调用,变为挂起、激活以继续执行等。为了利用线程的共享地址空间,操作系统内核的调度策略可能会优先选择同一进程中的线程,而不是不同进程中的线程。

四、线程管理函数

Pthread库提供了用于线程管理的以下API

  1. 创建线程

   使用pthread_create()函数创建线程。

   int pthread_create (pthread_y *pthread_rd, pthread_attr_t *attr, void *(*func)(void *),void *arg);

   如果成功则返回0,如果失败则返回错误代码。pthread_create()函数的参数为

   pthread_id是指向pthread_t类型变量的指针。它会被操作系统内核分配的唯一进程ID填充。在POSIX中,pthread_t是一种不透明的类型。程序员应该不知道不透明对象的内容,因为它可能取决于实际情况。线程可通过pthread_self()函数获得自己的ID。在Linux中,pthread_t类型被定义为无符号长整型,因此线程ID可以打印为%lu

   attr是指向另一种不透明数据类型的指针,它指定线程属性,下面将对此进行更详细的说明。

   func是要执行的新线程函数的入口地址。

   arg是指向进程函数参数的指针,可写为:

 void *func(void *arg)

 其中,attr是参数最复杂。下面给出了attr参数的使用步骤。

(1) 定义一个pthread属性变量pthread_attr_tattr.

(2) pthread_attr_init(&attr)初始化属性变量。

(3) 设置属性变量并在pthread_read()调用中使用。

(4) 必要时,通过pthread_attr_destory(&attr)释放attr资源。

  1. 线程ID

   线程ID是一种不透明的数据类型,取决于实现情况。因此,不应该直接比较线程ID。如果需要,可以使用pthread_equal()函数对它们进行比较。

int pthread_equal(pthread_t  t1,pthread_t t2);

如果是不同的进程,则返回0,否则返回非0.

  1. 线程终止

   线程函数结束后,线程即终止。或者,线程可以调用函数

   int pthread_exit(void *status);

   进程显式终止,其中状态是线程的退出状态。通常,0退出值表示正常终止,非0值表示异常终止。

  1. 线程连接

   一个线程可以等待另一个线程的终止

五、线程示例程序

  1. 用线程计算矩阵的和
  2. 用线程快速排序

六、进程同步

由于线程在进程的同一地址空间中执行,它们共享同一地址空间中的所有全局变量和数据结构。当多个线程试图修改同一共享变量或数据结构时,如果修改结果取决于线程的执行顺序,则称之为竞态条件。

  1. 互斥量

最简单的同步工具是锁,它允许执行实体仅在有锁的情况下才能继续执行.Pthread中,锁被称为互斥量,意思是相互排斥。有两种方法可以初始化互斥量。

(1)一种是静态方法,如:

         pthread_mutex_t m = PTHREAD_MUTEX_INITIALIZER

         定义互斥量m,并使用默认属性对其进行初始化。

(2)另一种是动态方法

  1. 死锁预防

   互斥量使用封锁协议。如果某线程不能获取互斥量,就会被堵塞,等待互斥量解锁后再继续。在任何封锁协议中,误用加锁可能会产生一些问题。最常见和突出的问题是死锁。死锁是一种状态,在这种状态下,许多执行实体相互等待,因此都无法继续下去。有多种方法可以解决能解决的死锁问题,其中包括死锁预防、死锁规避、死锁检测和恢复等。

  1. 条件变量

   和互斥量一样,条件变量也可以通过两种方法进行初始化。

(1)一种是静态方法

(2)另一种是动态方法

  1. 信号量

   信号量是进程同步的一般机制。(计数)信号量是一种数据结构,对信号量的操作都是(不可分割的)原子操作或基本操作。

  1. 屏障

   线程连接操作允许某进程(通常是主线程)等待其他线程终止。在Pthreads中,可以采用的机制是屏障以及一系列屏障函数。

  1. 用并发线程解线性方程组

   求解线性方程组最著名的算法是高斯消元算法。该算法包含两个主要步骤,即行简化和回代:行简化将组合矩阵[A/B]简化为一个上三角矩阵,然后进行回代,计算解向量X。在行简化步骤中,可采用部分选主元法,确保用于简化其他行的首行元素有最大绝对值。

  1. Linux中的线程

   与其他操作系统不同,Linux不区分进程和线程。对于Linux内核,线程只是一个与其他进程共享某些资源的进程。在Linux中,进程和线程都是由close()系统调用创建的,具有以下原型:

int clone(int (*fn)(void *), void *child_stack, int flags, void *arg)

可以看出,clone()更像是一个线程创建函数。它创建一个子进程来执行带有child_stack的函数fn(arg)flags字段详细说明父进程和子进程共享的资源,包括:

CLONE_VM:父进程和子进程共享地址空间

CLONE_FS:父进程和子进程共享文件系统信息,例如根节点、CWD

CLONE_FILES:父进程和子进程共享打开的文件

CLONE_SIGHAND:父进程和子进程共享信号处理函数和已屏蔽信号

如果指定了任何标志寄存器,两个进程会共享同一套资源,而不是单独的一套资源副本。

七、编程项目 :用户级线程

  1. 用户级线程

   整个基本代码程序在用户模式下对Linux进程运行。Linux进程内有独立的执行实体,相当于传统的线程。为了不与Linux进程或线程混淆,我们把执行实体称为任务

posted on 2021-10-31 22:15  20191226刘煊赫  阅读(5)  评论(0编辑  收藏  举报