zoukankan      html  css  js  c++  java
  • 学习笔记7

    思维导图

    知识归纳

    本章论述了并发编程,介绍了并行计算的概念。指出了并行计算的重要性∶比较了顺序算法与并行算法,以及并行性与并发性;解释了线程的原理及其相对于进程的优势;介绍了Pthread中的线程操作,包括线程管理函数,互斥量、连接、条件变量和屏障线程同步工具;解释了死锁问题,并说明了如何防止并发程序中的死锁问题;讨论了信号量,并论证了它们相对于条件变量的优点;还解释了支持Linux中线程的独特方式。

    并行计算基础

    begin-end代码块中的顺序算法可能包含多个步骤。所有步骤都是通过单个任务依次执行的, 每次执行一个步骤。当所有步骤执行完成时,算法结束”相反,图的右侧为并行算法的描 述,它使用cobegin-coend代码块来指定并行算法的独立任务。在cobegin-coend块中.所有 任务都是并行执行的,紧接着cobegin-coend代码块的下一个步骤将只在所有这些任务完成 之后执行。

    并行性与并发性

     

    通常,并行算法只识别可并行执行的任务,但是它没有规定如何将任务映射到处理组 件。在理想情况下,并行算法中的所有任务都应该同时实时执行。然而,真正的并行执行只 曲在有多个处理组件的系统中实现,比如多处理器或多核系统。在单CPU系统中,一次只 能执行一个任务。在这种情况下,不同的任务只能并发执行,即在逻辑上并行执行。在单 CPU系统中,并发性是通过多任务处理来实现的,该内容已在第3章中讨论过。在本章的 最后,我们将在一个编程项目中再次讲解和示范多任务处理的原理和方法。

    线程

    一个操作系统(OS)包含许多并发进程“在进程模型中,进程是独立的执行单元。所 有进程均在内核模式或用户模式下执行。在内核模式下,各进程在唯一地址空间上执行,与 其他进程是分开的。虽然每个进程都是一个独立的单元,但是它只有一个执行路径。当某进 程必须等待某事件时,例如I/O完成事件,它就会暂停,整个进程会停止执行匸线程是某进 程同一地址空间上的独立执行单元。创建某个进程就是在一个唯一地址空间创建一个主线 程。当某进程开始时,就会执行该进程的主线程。如果只有一个主线程,那么进程和线程实 际上并没有区别。但是,主线程可能会创建其他线程。每个线程又可以创建更多的线程等。 某进程的所有线程都在该进程的相同地址空间中执行,但每个线程都是一个独立的执行单 元。在线程模型中,如果一个线程被挂起,其他线程可以继续执行。除了共享共同的地址空 间之外.线程还共享进程的许多其他资源,如用户id打开的文件描述符和信号等。打个简 单的比方,进程是一个有房屋管理员(主线程)的房子,线程是住在进程房子里的人°房子 里的每个人都可以独立做自己的事情,但是他们会共用一些公用设施,比如同一个信箱、厨 房和浴室等。过去,大多数计算机供应商都是在自己的专有操作系统中支持线程。不同系统 之间的实现有极大的区别。目前,几乎所有的操作系统都支持Pthread,它是IEEE POSIX 1003.1c的线程标准(POS1X 1995 )o如需了解更多信息,读者可査阅更多关于Pthread编程 的书籍(Buttlar 1996)和在线文章(Pthreads 2017 )0

    线程的优点

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

    (1 )线程创建和切换速度更快:进程的上下文复杂而庞大。其复杂性主要来自管理进 程映像的需要。例如,在具有虚拟内存的系统中.进程映像可能由叫作页面的许多内存单元 组成。在执行过程中.有些页面在内存中,有些则不在内存中。操作系统内核必须使用多个 页表和多个级别的硬件辅助来跟踪每个进程的页面-要想创建新的进程,操作系统必须为进 程分配内存并构建页表。若要在某个进程中创建线程,操作系统不必为新的线程分配内存 和创建页表,因为线程与进程共用同一个地址空间。所以,创建线程比创建进程更快。另 .由于以下原因,线程切换比进程切换更快。进程切换涉及将一个进程的复杂分贞环境 替换为另一个进程的复杂分页环境,需要大量的操作和时间。相比之下,同一个进程中的 线程切换要简单得多、也快得多,因为操作系统内核只需要切换执行点,而不需要更改进 程映像。

    (2) 线程的响应速度更快:一个进程只有一个执行路径。当某个进程被挂起时,整个进 程都将停止执行。.相反,当某个线程被挂起时,同 进程中的其他线程可以继续执行。这使 得有多个线程的程序响应速度更快.例如,在一个多线程的进程中,当一个线程被阻塞以等 待I/O时,其他线程仍可在后台进行计算。在有线程的服务器中,服务器可同时服务多个客 户机。

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

    线程的缺点

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

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

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

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

    线程操作

    线程的执行轨迹与进程类似。线程可在内核模式或用户模式下执行 在用户模式下,线 程在进程的相同地址空间中执行,但每个线程都有自己的执行堆栈。线程是独立的执行単 元,可根据操作系统内核的调度策略,对内核进行系统调用,变为挂起、激活以继续执行 等。为了利用线程的共享地址空间,操作系统内核的调度策略可能会优先选择同-进程中的 线程,而不是不同进程中的线程。截至目前,几乎所有的操作系统都支持POSIX Pthread, 定义了一系列标准应用程序编程接口(API)来支持线程编程。下面,我们将讨论和演示 Linux 中的 Pthread 并发编程(Goldt 1995 : IBM  Love 2005 Linux Man Page Progect 2017 )0

    线程管理函数

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

    pthread_create(thread, attr, function, arg): create thread pthread_exit(status):terminate thread
    
    pthread_cancel(thread) : cancel thread
    
    pthread_attr_init(attr) : initialize thread attributes
    
    pthread_attr_destroy(attr): destroy thread attribute
    

    创建线程

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

    int pthread_create (pthread_t *pthread_id, pthread_attr_t *attr, void *(*func)(void *), void *arg); 

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

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

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

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

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

    void *func(void *arg)

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

    (1)定义一个 pthread 属性变量 pthread_attr_t attr,,

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

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

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

    下面列出了使用属性参数的一些示例。每个线程在创建时都默认可与其他线程连接。必 要时,可使用分离属性创建一个线程,使它不能与其他线程连接。下面的代码段显示了如何 创建一个分离线程。

    pthread_attr_t attr; // define an attr variable
    
    pthread_attr_init(&attr); // initialize attr
    
    pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); // set attr pthread_create(&thread_idz func, NULL); // create thread with attr
    
    pthread_attr_destroy; // optional: destroy attr
    

    每个线程都使用默认堆栈的大小来创建。在执行过程中,线程可通过函数找到它的堆栈 大小:

    size_t pthread_attr_getstacksize()

    它可以返回默认的堆栈大小。下面的代码段显示了如何创建具有特定堆栈大小的线程。

     

    线程 ID

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

    int pthread_equal (pthread_t tl, pthread_t t2);
    

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

     

    线程终止

     

     

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

     

    int pthread_exit (void *status);
    

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

     线程连接

     

     

    一个线程可以等待另一个线程的终止,通过:

     

    int pthread_join (pthread_t thread, void **status_ptr);

     

    终止线程的退出状态以status_ptr返回。

     

    线程示例程序

     

    用线程计算矩阵的和

     

    示例4.1 :假设我们要计算一个NxN整数矩阵中所有元素的和匚这个问题可通过使用 线程的并发算法来解决。在本示例中,主线程会先生成一个NxN整数矩阵。然后,它会创 建N个工作线程,将唯一行号作为参数传递给各工作线程,并等待所有工作线程终止。每 个工作线程计算不同行的部分和,并将部分和存入全局数组int sum[N]的相应行中。当所有 工作线程计算完成后,主线程继续进行计算。它将工作线程生成的部分和相加来计算总和。 下面列出了示例程序C4.1的完整C代码。

    线程同步由于线程在进程的同一地址空间中执行,它们共享同一地址空间中的所有全局变量和数 据结构。当多个线程试图修改同一共享变量或数据结构时,如果修改结果取决于线程的执行 顺序,则称之为竞态条件。在并发程序中,绝不能有竞态条件。否则,结果可能不一致。除 了连接操作之外,并发执行的线程通常需要相互协作。为了防止出现竞态条件并且支持线程 协作,线程需要同步。通常,同步是一种机制和规则,用于确保共享数据对象的完整性和并 发执行实体的协调性。它可以应用于内核模式下的进程,也可以应用于用户模式下的线程。 下面,我们将讨论Pthread中线程同步的具体问题。

    互斥量

    最简单的同步工具是锁,它允许执行实体仅在有锁的情况下才能继续执行,Pthread 中,锁被称为互斥量’意思是相互排斥。互斥变量是用pthread_mutex_t类型声明的,在使 用之前必须对它们进行初始化。有两种方法可以初始化互斥量。

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

    pthread_rautex_t m = PTHREAD_MUTEX_INITIALIZER;
    

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

    (2) 另一种是动态方法,使用pthread_mutex_init()函数,可通过attr参数设置互斥属

     

    性,如:

    pthread_mutex_init (pthread_mutex_t *m, pthread_mutexattr_t,*attr);
    

    通常,attr参数可以设置为NULL,作为默认属性。

    初始化完成后,线程可通过以下函数使用互斥量。

    int pthread_mutex_lock (pthread_mutex_t *m); 
    int pthread_mutex_unlock (pthread mutex t *m);
    int pthread_mutex_trylock (pthread_ioutex_t *m); int pthread_mutex_destroy (pthread_mutex_t *m);

    线程使用互斥量来保护共享数据对象互斥量的典型用法如下。线程先创建一个互斥量 并对它进行一次初始化。新创建的互斥量处于解锁状态,没有所有者。每个线程都试图访问 一个共享数据对象:

    pthread_mutex_lock(&m) ;〃 lock mutex
    
    access shared data object; II access shared data in a critical region pthread_mutex_unlock(;// unlock mutex
    

      当线程执行pthread_mutex_lock(&m)时,如果互斥量被解锁.它将封锁互斥量,成为互斥 量的所有者并继续执行。否则,它将被阻塞并在互斥量等待队列中等待。只有获取了互斥量 的线程才能访问共享数据对象。一次只能由一个执行实体执行的•系列执行通常称为临界区 (CR)Pthread中,互斥量用来保护临界区,以确保临界区内在任何时候最多只能有一个 线程。当线程完成共享数据对象时,它通过调用pthread_mutex_unlock(&m)来解锁互斥歐 从而退出临界区。封锁的互斥量只能由当前所有者解锁。在解锁某互斥量时,如果互斥量等 待队列中没有阻塞的线程,它就会解锁该互斥量,这时,互斥量没有所有者。否则、它会从 互斥量等待队列中解锁等待线程,解锁的线程成为新的所有者,互斥量继续保持封锁。当所 有线程都完成后,如果是动态分配的互斥量,则可能会被销毁。下面我们通过一个例子用互 斥量来演示线程同步。

    信号量

    struct sem(
    
    int value; // semaphore (counter) value;
    
    struct process *queue // a queue of blocked processes
    
    )s;
    

      在使用信号量之前,必须使用一个初始值和一个空等待队列进行初始化。不论是什么硬件平 台,即无论是在单CPU系统还是多处理器系统上,信号量的低级实现保证了每次只能由一 个执行实体操作每个信号量,并且从执行实体的角度来看,对信号量的操作都是(不可分割 的)原子操作或基本操作。读者可以忽略这些细节,将重点放在信号量的高级操作及其作为 进程同步机制的使用上。最有名的信号量操作是pV (Dijkstral965 )。

      信号量和条件变量之间的主要区别是,前者包含一个计数器,可操作计数器,测试计数 器值以做出决策等,所有这些都是临界区的原子操作或基本操作,而后者需要一个特定的互 斥量来执行临界区。在Pthreads中,互斥量严格用于封锁,而条件变量可用于线程协作。相 反,可以把使用初始值1计算信号量当作锁。带有其他初始值的信号量可用于协作。因此, 信号量比条件变量更通用、更灵活。下面的示例说明了信号量相对于条件变量的优势。

     

     

  • 相关阅读:
    今天 弄了一下ajax 里面的
    重修理解了一下js 控制treeview 菜单的子级和父级的关系 理解的还不够全 因为 html不够全 但是加个注释吧
    xml .net 用法
    这几天在做把数据库里的数据 导出 并且压缩 学到了一些东西
    今天实现了用按钮后台动态实现了 table里面内容的显示和隐藏 在实现过程中了解了updatepanel的简单的用法
    今天第一次参加了软件产品的讨论会议 收获
    收藏个debug的文章
    Animate.css
    flash遨游缓存问题
    URL短地址压缩算法 微博短地址原理解析 (Java实现)
  • 原文地址:https://www.cnblogs.com/stx3220665294/p/15491404.html
Copyright © 2011-2022 走看看