zoukankan      html  css  js  c++  java
  • 《POSIX多线程程序设计》读书笔记

    一.      概述

    1.    一个UNIX进程可以理解为一个线程加上地址空间、文件描述符和其他数据;

    2.    多个线程可以共享一个地址空间,而做不同的事情。在多处理器系统中,一个进程中的多个线程可以同时做不同的工作;

    3.    从某种成都上讲,线程只是构造异步应用程序的另一种方式,但是它却比其他用来构造异步程序程序的模型更有优势;

    4.    线程安全是指代码能够被多个线程调用儿不会产生灾难性结果。它不要求代码在多个线程中高效地运行。大部分现行函数可以利用PThreads提供的工具------互斥量、条件变了和线程私有数据,实现线程的安全。不需要保存永久状态的函数。可以通过整个函数调用的串行化来实现线程安全。比如,只要在进入函数时加锁,在退出函数时解锁。这样的函数可以被多个线程调用,但一次只能有一个线程调用它。

    更有效的方式是将线程安全函数分为多个小的临界区。这样就允许多个线程进入该函数,虽然不能同时进入一个临界区。更好的方式是将代码改造为对临界区数据的保护而不是对临界区代码的保护,这样就可以 令不会同时访问相同临界数据的线程完全并行地执行;

    正确的方法是将互斥量和数据流相关联,保护数据而不是代码;

    “可重入”优势用来表示“有效的线程安全”,即通过采用必将函数或库函数转换成一系列区域更加复杂的方式使代码成为线程安全的。通过引入互斥量和线程私有数据可以实现线程安全,但通常需要改变接口来使函数可重入。可重入的函数应该避免以来任何静态数据,最好避免依赖线程间任何形式的同步;

    通常,函数可以将状态保存在环境结构中,由调用者来负责状态数据的同步。例如,UNIX中的readdir函数依次返回队列中的每一个目录项。为了让readdir函数是线程安全的。你可以添加一个互斥量,每次当readdir被调用时,锁住该变量,在函数返回时解锁。另一种办法,也是Pthreads对其readdir_r函数的做法,是避免函数内部的任何锁操作,而是让调用者在搜索目录时分配一个数据结构来保存readdir_r的环境;

    5.    当一个线程要进入一个函数但是需要等待别的线程释放锁,等待结束进入该函数的结果和未发生过等待所产生的结果一致,那这个函数就是可重入的,可重入异地线程安全,但线程安全不一定可重入;

    6.    当你使用管道(pipe)将一个命令的输出重定位到另一个命令的输入是,就是启动了几个独立程序,他们彼此通过管道来同步;

    7.    当在脚本环境中输入ls|more,将ls命令的输出重定向到more命令的输入时,实际上是在通过设定数据依赖性来描述命令的同步;

    8.    线程就是进程里足以执行代码的部分,在大多数计算机系统中,这意味着线程应包括以下内容:当前指令位置指针(通常称为计数器或PC)、栈顶指针(SP)、通用寄存器、浮点或者地址寄存器。线程可能还包括像处理器状态和协处理器寄存器等数据。线程不包括进程中的其他数据,如地址空间和文件描述符。一个进程中的所有线程共享文件和内存空间,包括程序文本段和数据段;

    9.    当系统在进程间切换时,与进程相关的输油硬件状态都会失效。有些可能随环境切换过程而改变,如数据缓存和虚拟内存转换入口可能需要刷新;每一个进程有独立的虚拟内存空间,但是同一进程中的线程却共享相同的地址空间和其他进程数据;

    10.在Digital UNIX平台上,用标志CFLAGS = -pthread –stdl –wl来编译代码,在Solaris系统中,用标志CFLAGS=-D_FEENTRANT –D_POSIX_C_ SOURCE= 199506 –lpthread来编译;

    11.pthread_create 函数建立一个线程,运行并由第三个参数(alarm_thread)指定的例程,并返回线程标识符ID(保存在thread引用的变量中);

    pthread_detach 函数允许pthreads当前线程终止时立刻回收线程资源;

    pthread_exit 函数终止调用线程;

    12.barrier是一种简单的同步机制,它阻塞每个线程直到到达某个“限额”类,然后所有线程被解除阻塞。例如,可以使用barrier机制来确保:只有当所有线程都准备好了才执行一段并行代码;

    13.线程系统的三个几倍要素:执行环境、调度和同步;

    二.      线程

    1.    为建立线程,你需要在程序中声明一个pthread_t类型的变量。如果只需要在某个函数中使用线程ID,或者函数直到线程终止时才返回,则可以将线程ID声明为自动存储类型。不过大部分时间内,线程ID保存在共享(静态或者外部)变量中,或者保存在堆空间的结构体中。

    2.    线程可以通过调用pthread_self来获得自身的ID;

    3.    如果需要知道线程何时结束,就需要保存线程ID;

    4.    Pthreads提供了pthread_equal函数来比较两个线程ID,只能比较二者是否相同;

    5.    初始线程(主线程)是特殊的;

    6.    分离一个正在运行的线程不会对线程带来任何影响,仅仅是通过系统当该线程结束时,其所属资源可以被回收;

    7.    尽管“线程蒸发”有时有用,但大部分时间进程要比你创建的线程“长命”。为确保终止线程的资源对进程可用,应该在每个线程结束时分离它们。一个没有被分离的线程终止时会保留其虚拟内存,包括他们的堆栈和其他系统资源。分离线程意味着通知系统不再需要此线程。允许系统将分配给它的资源回收;(避免内存泄露)

    8.    pthread_join函数将阻塞其调用者直到制定线程终止,然后,可以选择地保存线程的返回值。调用pthread_join函数自动分离指定的线程;

    9.    当需要某个线程结束后我们才可以干某件事,就需要调用pthread_join函数;

    10.程序调用pthread_create函数创建线程,然后调用pthread_join等待线程结束;(join连接的概念类似于让被连接线程与当前发起join的线程(可能是主线程)发生连接,成为类似在主线程中同步运行的效果)

    11.可以在main函数中调用pthread_exit,这样进程就必须等到所有线程结束才能终止;

    12.在少数情况下,多个线程需要知道某个特定线程何时结束,则这些线程应该等待某个条件变量而不是调用pthread_join函数。被等待的线程应该将其返回(或任何其他信息)保存在某个公共的位置,并将条件变量广播给所有在其上等待的线程以唤醒它们;

    13.就绪(ready):线程能够允许,但是在等待可用的处理器。可能刚刚启动,货刚刚从阻塞中回复,或者被其他线程抢占;

    运行(running):线程正在运行。在多处理器系统中,可能有多个线程处于运行太;

    阻塞(blocked):线程由于等待处理器外的其他条件无法运行,如条件变量的改变、加锁互斥量或I/O操作结束;

    终止(terminated):线程从起始函数中返回,或调用pthread_exit,或者被取消,终止自己并完成所有资源清理工作。不是被分离,也不是被连接,一旦线程被分离或者连接,它就可以被回收;

    14.如果线程已被分离,则他立刻被回收重用(这难道不是比“销毁”线程好吗?大部分系统可以重用资源来建立新线程);否则,线程停留在终止态直到被分离或者被连接;

    15.有关线程创建最重要的是,在当前线程从函数pthread_create中返回以及新线程被调度执行之间不存在同步关系。即,新线程可能在当前线程从pthread_create返回之前就运行了,甚至在当前线程从pthread_create返回之前,新线程就可能已经运行完毕了;

    16.当创建不需连接的线程时,应该使用detachstate属性简历线程使其自动分离;

    17.如果使用detachstate属性(设为PTHREAD_CREATE_DETACH)建立线程或者调用pthread_detach分离线程,则当线程结束时将被立刻回收;

    18.只有互斥量的主任能够解锁它。如果线程终止时还有加锁的互斥量,则该互斥量就不能被再次使用(因为不会被解锁);

    三.      同步

    1.    可以拷贝指向互斥量的指针,这样就可以使多个函数或线程共享互斥量来实现同步;

    2.    当不再需要一个通过pthread_mutex_init调用动态初始化的互斥量时,应该调用pthread_mutex_destroy来释放它。不需要释放一个使用PTHREAD_MUTEX_INITIALIZER宏静态初始化的互斥量;

    3.    当确信没有线程在互斥量上阻塞时,可以立刻释放它;

    4.    当调用pthread_mutex_lock加锁互斥量的时候,如果此时互斥量已经被锁住,则调用线程将被阻塞;Pthreads提供了pthread_mutex_trylock函数,当地调用互斥量已被锁住时候调用该函数将返回错误代码EBUSY;

    5.    避免死锁的算法:

    固定加锁层次:所有需要同时加锁互斥量A和互斥量B的代码,必须首先加锁互斥量A然后加锁互斥量B;即,如果互斥量间不存在明显的逻辑层次,则可以建立任意的固定加锁层次。例如,你可以建立这样一个加锁互斥量集合的函数:将集合中的互斥量按照ID地址顺序排列,并以此顺序加锁互斥量;某某种程度上讲,只要总是保持住相同的顺序,顺序本身就并不真正重要;

    试加锁和回退:在锁住某个集合中的第一个互斥量后,使用pthread_mutex_trylock来加锁集合中的其他互斥量,如果失败则将集合中所有已加锁互斥量释放,并重新加锁;“回退”意味着你以正常的方式锁住集合中的第一个互斥量,而调用pthread_mutex_trylock函数有条件地加锁集合中其他互斥量。如果pthread_mutex_trylock返回EBUSY,则你必须释放已经拥有的所有属于该集合的互斥量并重新开始;

    死锁就是当你尝试调用一个mutex的lock的时候,想要进行lock,有可能这个mutex已经被别的线程lock了,你就无法再去lock这个mutex,就必须等另外那个线程unlock这个mutex,两个线程等对方锁住的mutex,就死锁了;mutex是系统级别的,所以线程彼此之间知道能否成功锁住一个mutex;

    比如代码:

    Mutex mutex_a, mutex_b;
    void function1()
    {
        pthread_mutex_lock(&mutex_a)         
    }
    void function2()
    {
        pthread_mutex_lock(&mutex_b);
    }
     
     //thread A callstack
     function1();
     function2(); 
    
     //thread B callstack
     function2();
     function1(); 

    以上代码中线程A和线程B如果同时走完了各自第一个函数,在准备走自己的第二个函数的时候就会造成死锁;

    6.    要小心使用锁链。很容易写出良妃处理器的代码:代码大部分时间再加锁和解锁时从来不会遇到竞争的互斥量。仅当多个线程几乎总是活跃在层次中的不同部分时才应该使用锁链;

    7.    条件变量总是返回锁住的互斥量;

    8.    条件变量是与互斥量相关、也与互斥量保护的共享数据相关的信号机制。再一个条件变量上等待会导致以下原子操作:释放相关互斥量,等待其他线程发给该条件变量的信号(唤醒一个等待者)或广播该条件变量(唤醒所有等待者)。当等待条件变量时,互斥量必须始终锁住;当线程从条件变量等待中醒来时,它重新继续锁住互斥量;

    9.    条件变量就是允许使用队列的线程之间交换队列状态信息的机制;

    10.条件变量的作用是发信号,而不是互斥;

    11.任何条件变量在特定时间只能与一个互斥量相关联,而互斥量则可以同时与多个条件变量关联;

    12.当线程醒来时,再次测试谓词同样重要;

    13.内存屏障(memory barrier)确保:所有在设置内存屏障之前发起的内存访问,必须限于在设置屏障之后发起的内存访问之前完成;

    内存屏障是一堵移动的墙,而不是刷新cache的命令;

    不像其他的内存访问,内存控制器不能删除内存屏障,不能越过它,直到完成在内存屏障之前的所有操作;

    14.如果其中一个内存单元同时被另外一个处理器写入,则将丢失一般数据。这杯成为“word tearing”;

    四.      使用线程的几种方式

    1.    流水线:每个线程反复地在数据系列集上执行同一种操作,并把操作结果传递给下一步骤的其他线程,这就是“流水线”(assembly line)方式;

    工作组:每个线程都在自己的数据上执行操作。工作组中的线程可能执行同样的操作,也可能执行不同的操作,但是他们一定独立运行;

    客户端/服务器:一个客户为每一件工作与一个独立的服务器“订契约”。通常“订契约”是匿名的-----一个请求通过某种接口提交;

    2.    在工作组中,数据由一组线程分别独立地处理。循环的“并行分解”通常就是属于这种模式;

    五.      线程高级编程

    1.    Pthreads允许每个线程控制自己的结束,它能回复程序不变量并且解锁互斥量,当线程完成一些重要的操作时,它甚至能推迟取消;

    2.    默认情况下,取消被推迟执行,并且仅仅能在重新中特定的点发生;

    3.    取消一个线程是异步的。即,当pthread_cancel调用返回时,线程未必已经被取消,可能仅仅通知有一个针对它的未解决的取消请求。如果需要知道线程在何时被实际终止,就必须在取消它之后调用pthread_join与它连接;

    4.    线程能清理并终止自己,而不必担心再次被取消;

    5.    没有方法“处理”取消并执行------线程必须或者被彻底推迟取消,或者终止;

    6.    在获得一个资源以后,并且在任何取消点之前,通过调用pthread_clearup_push生命一个清除处理函数。在释放资源前,但是在任何取消点以后,通过调用pthread_creanup_pop删除清理处理函数;

    7.    下列函数在任何Pthreads系统上总是取消点: pthread_cond_wait   fsync    sigwaitinfo pthread_cond_timedwait mq_receive  sigsuspend pthread_join      mq_send     sigtimedwait pthread_testcancel   msync        sleep    sigwait nanosleep   system       aio_suspend      open   tcdrain close    pause   wait     creat    read     waitpid fcntl(F_SETLCKW)    sem_wait    write

    8.    避免异步取消;很难正确使用异步取消,并且很少有用;除非你写了可以安全地异步取消的代码,否则在异步取消启动时不要调用任何代码,即使要这样做,也要三思;

    9.    可以把每个线程考虑为有一个活动的清除处理函数的栈。调用pthread_cleanup_push将清除处理函数加到栈中,调用pthread_cleanup_pop删除最近增加的处理函数。当线程被取消时或当它调用pthread_exit退出时,pthreads从最近增加的清除处理函数开始,一次调用各个活动的清除处理函数。当所有的活动的清除处理函数返回时,线程被终止;

    10.你不能在一个函数内压入一个清除处理函数而在另外的函数中弹出它。pthread_cleanup_pop操作可能作为宏被定义,这样pthread_cleanup_push中可能含块开始的大括号“{”而pthread_creanup_pop中则包含了匹配的块结束大括号“}”。当使用清除处理函数时,如果希望代码可移植,你必须总是记住这一限制;

    11.只要设置pthread_cleanup_pop的参数非零,则即使没有发生取消,清除处理函数仍将被调用;

    12.在进程内的所有线程都享有相同的地址空间,即意味着任何声明为静态或者外部的变量,或在进程堆生命的变量,都可以被进程内所有的线程读写;

    13.如果每个线程都需要一个私有变量值,则必须在某处存储所有的值(像UE4中多线程渲染就是好每个线程的回调函数都在一个类中,类成员变量存储该线程所需要的值,回调函数都是static的)。

    14.每个线程调用pthread_once保证线程私有数据键被创建;

    15.当程序不在需要时,pthreads允许你调用pthread_key_delete释放一个线程私有数据键;

    16.可以使用pthread_getspecific 函数来获得线程当前的键值,或调用pthread_setspecific 来改变当前的键值;

    17.如果你的线程私有数据是堆存储的地址,并且你想要在destructor函数中释放存储,就必须使用传递给destructor的参数,而非调用pthread_getspecific 的参数;

    18.Destructor功能仅仅当线程终止时被调用,而不是当线程私有数据键的值改变时;

    19.当你创建一个线程私有数据键时,pthreads允许你定义destructor函数。

    20.当一个线程退出时,pthreads在进程中检查所有的线程私有数据键,并且将所有不是空的线程私有数据键置为空,然后调用键的destructor函数;

    21.标准要求pthreads实现在核对列表某个固定的次数后再放弃。当它放弃时,最终的线程私有数据值没有被破坏。如果值是指向堆存储器的指针,结果可能是一个内存泄露,因此一定要小心;

    22.大多数操作系统至少在pthreads线程和处理器之间有一层附加的抽象,这就是我所说的核实体(因为那是pthreads使用的术语);

    六.      POSIX针对线程的调整

    1.    在一个forked的进程中,pthreads不会终止其他线程,好像他们调用pthread_exit推出了或是好像他们被取消。他们只是简单地不再存在。即,线程不再运用线程私有数据destructors或者清除处理函数。如果子进程准备调用exec运行一个新程序,这不是一个问题,但是如果你使用fork克隆一个多线程程序,注意你可能失去对存储器的存取,特别是仅仅存储线程私有数据值的堆存储器;

    2.    应避免在线程的代码中使用fork,除非子进程想很快地exec一个新程序;

    3.    exec 函数并没有因为引入线程而受到很多影响。exec函数的功能是取消当前程序的环境并且用一个新程序代替它;对exec的调用,将很快的充值进程内除了调用exec的线程外的所有线程。他们不执行清除处理器或线程私有数据destructors---线程只是简单地停止存在;

    4.    pthreads增加了pthread_exit函数,该函数能在进程继续运行的同时导致多个单个线程的退出;

    5.    线程不会执行清除处理器或线程私有数据destructors函数,调用exit具有同样的效果;

    6.    从主函数中调用pthread_exit将在不影响进程内其他线程的前提下终止起始线程,允许他们继续和正常完成;

    其他:

    1. 当一个线程A利用mutex lock住一块代码的时候,线程B虽然可以存储着这个mutex的指针,甚至可以在线程A锁住这个mutext后线程B即解锁它,但是最好不要这么做,也就是说,最好不要用其他线程解锁当前线程锁住的mutex;因为当线程B盲目解锁这个mutex后,线程A仍在临界区代码中运行,其他线程(甚至也可能是B线程)就可以因为这个mutex已经被解锁同时进入这块临界区代码,这样这个临界区会出现两个线程同时运行的情况,临界区也就名存实亡了。 

    上帝为我关上了窗,我绝不洗洗睡!
  • 相关阅读:
    2016多校赛1 A 期望 B SG博弈,状压 D 倍增,二分
    POWOJ 1739: 魔术球问题 DAG最小路径覆盖转最大流
    Codeforces 743D 树形dp
    线性规划与网络流24题 索引
    WangEditor富文本编辑器的简单使用,并将文本数据发往后台
    SSRF
    关于Blind XXE
    blind xxe攻击
    linux awk命令详解
    kali
  • 原文地址:https://www.cnblogs.com/DeanWang/p/6392253.html
Copyright © 2011-2022 走看看