《UNIX环境高级编程》 第12章 线程控制 【读书笔记】

  • 线程限制也可以通过sysconf()函数进行查询。这些限制的使用是为了增强应用程序在不同OS间的可扩展性。
  • 线程属性类型为pthread_attr_t,设置它后可创建(pthread_create)特定属性的线程类型(比如分离的detached线程)。通过pthread_attr_init(attr)初始化一个线程属性,这样attr里就是OS实现的一些默认属性。pthread_attr_destroy去除对它的初始化,若分配了动态内存,也会destroy掉。
  • POSIX.1线程属性包括四种:detachstate(线程的分离状态属性); guardsize(线程栈末尾的警戒缓冲区大小byte); stackaddr(线程栈的最低地址); stacksize(线程栈的大小byte)。有相应的接口用于获取和设置这些属性。
  • pthread_attr_getdetachstate(attr,detachstate)可从attr中获取线程的状态(detached or joinable),pthread_attr_setdetachstate(atrr,detachstate)可设置attr的状态为detachstate。
  • 新接口是pthread_attr_getstack(attr,stackaddr,stacksize),pthread_attr_setstack(attr,stackaddr,stacksize),来操控线程栈的最低地址和栈大小。
  • 对于一个进程来讲,虚拟地址空间大小是固定的,进程中只有一个栈,所以进程栈大小通常不是问题。但是对于进程中的线程来讲,由于线程的所有栈共享同一进程地址空间,因此线程多了,则每个线程栈就小了。另外,如果线程调用的函数占用了过多的栈空间(过多的局部变量或者递归),则要将它的栈设置得大一些。
  • 如果用完了线程栈的虚拟地址空间,则可以使用malloc或mmap来为其他栈分配空间,并用pthread_attr_setstack函数来改变新建线程的栈位置。
  • 线程属性guardsize控制着线程栈末尾之后用以避免栈溢出的扩展内存的大小。这个属性默认设置为PAGESIZE字节。如果将其设置为0,则表示不提供警示缓冲区。同样,如果对线程属性stackaddr作了修改,则系统也会将guardsize设置为0。可使用pthread_attr_setguardsize()和pthread_attr_setguardsize()进行操控。
  • 线程还有其他的一些属性。比如可取消状态,可取消类型,并发度。并发度控制着用户级线程可以映射的内核线程或进程的数目,通过pthread_getconcurrency(void)和pthread_setconcurrency(int level)来操控。
  • 就像线程具有属性一样,线程的同步对象也有属性,如互斥量、读写锁和条件变量。
  • 互斥量属性类型pthread_mutexattr_t,用pthread_mutexattr_init()和pthread_mutexattr_destroy()操控。
  • 互斥量有两种值得注意的属性:进程共享属性类型属性
  • 进程共享属性默认为PTHREAD_PROCESS_PRIVATE,即在同一个进程中,多个线程可以访问同一个同步对象。然而存在这样的机制,允许相互独立的多个进程把同一个内存区域映射到它们各自独立的地址空间中。如果进程共享属性设置为PTHREAD_PROCESS_SHARED,则从多个进程共享的内存区域中分配的互斥量就可以用于这些进程的同步(类型于多个线程的同步)。可通过pthread_mutexattr_getpshared()和pthread_mutexattr_setpshared()对进程共享属性进行操控。
  • 类型属性控制着互斥量的特性。通过pthread_mutexattr_gettype()和pthread_mutexattr_settype()操控。共有4种类型:PTHREAD_MUTEX_NORMAL/ERRORCHECK/RECURSIVE/DEFAULT。
  • 读写锁属性为pthread_rwlockattr_t,条件变量属性类型为pthread_condattr_t,可通过pthread_[rwlock/cond]attr_init()和pthead_[rwlock/cond]attr_destroy()进行初始化和清除。它们目前均只支持进程共享属性,可通过pthread_[rwlock/cond]attr_[get/set]pshared进行操纵。
  • 许多函数是不可重入的,其原因为:(a)已知它们使用静态数据结构(b)调用malloc或free(c)它们是标准IO函数,标准IO库的很多实现都以不可重入方式使用全局数据结构。
  • 每个线程只有一个errno变量,信号处理程序可能会修改其原先值。因此作为一个通用规则,当在信号处理程序中调用可重入函数时,应当在调用之前保存,在调用之后恢复errno值。
  • 在信号处理程序中调用一个不可重入函数时,其结果是不可预见的。
  • 如果一个函数在同一时刻可以被多个线程安全地调用,就称该函数是线程安全的。
  • 很多函数并不是线程安全的,因为他们返回的数据是存放在静态的内存缓冲区中的。通过修改接口,要求调用者自己提供缓冲区就可以使函数变为线程安全的。(比如strtok_r是作为替代strtok的线程安全函数)
  • 如果一个函数对于多个线程来说是可重入的,则说这个函数是线程安全的,但这并不能说明对信号处理程序来说该函数也是可重入的。如果函数对异步信号处理程序的重入是安全的,那么就可以说函数是异步-信号安全的。
  • POSIX 1.1提供了以线程安全的文莱为管理FILE对象:可以使用flockfile(FILE *fp)和ftrylockfile(fp)来获取与给定FILE对象关联的锁,这个锁是递归的,不会导致死锁。funlockfile(fp)释放锁。要注意的是,如果标准IO例程都获得它们各自的锁,那么在做一次一个字符的IO操作时性能就会出现严重的下降,因为需要对每个字符的读或写进行获取和释放锁操作。在这种情况下出现了不加锁版本的基于字符的标准IO例程:getchar_unlocked(void);getc_unlocked(FILE *fp);putchar_unlocked();putc_unlocked(fp);由于它们会导致不可预期的结果(多个控制线程非同步地访问),因此最好被flockfile/ftrylockfile和funlockfile的调用包围。
  • 一旦对FILE对象进行加锁,就可以在释放锁之前对这些函数进行多次调用,这样就可以在多次的数据读写上分摊加解锁的开销。
  • 线程私有数据,是希望每个线程可以独立地访问数据副本,而不需要担心与其他线程的同步访问问题。为什么需要线程私有数据呢?其一,有时候需要维护基于每个线程的数据;其二,它提供了让基于进程的接口适应多线程环境的机制,比如errno是一个全局变量,系统调用或库函数在调用或执行失败时都会设置它。把errno重定义为线程私有数据后,每个线程对errno的设置操作并不会影响其他线程中的errno值,是不可见的。
  • 在分配线程私有数据前,需要创建与该数据关联的键(key),这个键将用于获取对线程私有数据的访问权。通过pthread_key_create(pthread_key_t *keyp, void(*destructor)(void *))创建一个键。这个键可以被进程中的所有线程使用,但每个线程把这个键与不同的线程私有数据地址进行关联。也就是说在不同线程中通过同一个键访问到的关联地址都是不一样的。
  • pthread_key_delete(key)取消键与线程私有数据值之间的关联关系,但它并不会激活与键关联的析构函数。
  • pthread_once(pthread_once_t *initflag, void (*initfn)(void))可以确保对于所有线程来说,初始化例程initfn只被调用一次,避免初期出现竞争(比如只需要初始化一次的全局变量)。
  • pthread_getspecific(pthread_key_t key)和thread_setspecific(pthread_key_t key, const void *value)是获取/设置与键关联的线程私有数据的地址。
  • 可取消状态可取消类型,是两个没有包含在pthread_attr_t结构体中的线程属性,这两个属性影响着线程在响应pthread_cancel函数调用时所呈现的行为(即在被请求取消的线程中进行设置)。接口分别是pthread_setcancelstate()和pthrad_setcanceltype。
  • pthread_cancel调用并不等待线程终止,在默认情况下,线程在取消请求发出以后还是继续运行,直到到达某个取消点。取消点是线程检查是否被取消并按照请求进行动作的一个位置。
  • 取消点有一系列函数,当这些函数被调用时,取消点都会出现。若长时间不调用这些函数,也可以通过pthread_testcancel在程序中自己添加取消点。
  • 默认取消类型也称延迟取消,因为在调用pthread_cancel后,在线程到达取消点之前,并不会出现真正的取消。可设置取消类型。
  • 每个线程都有自己的信号屏蔽字,但是信号的处理是进程中所有线程共享的。这意味着尽管单个线程可以阻止某些信号,但是是当线程修改了与某个信号相关的处理行为以后,所有的线程都必须共享这个处理行为的改变。这样如果一个线程选择忽略某个信号,而其他的线程可以恢复信号的默认处理行为,或者为信号设置一个新的处理程序,从而可以撤销上述线程的信号选择。
  • 进程通过使用sigprocmask来阻止信号发送,线程必须使用pthread_sigmask。两者用法类似,除了前者在失败时设置errno变量,后者失败时返回错误码。
  • 线程可以通过调用sig_wait()等待一个或多个信号的发生。使用sigwait的好处在于它可以简化信号处理, 允许把异步产生的信号用同步的方式处理。
  • 为了防止信号中断线程,可以把信号加到线程的信号屏蔽字中,然后安排专门的线程作信号处理。这些专门线程可以进行函数调用,不需要担心在信号处理程序中调用哪些函数是安全的,因为这些函数调用来自正常的线程环境,而非传统的信号处理程序,传统信号处理程序通常会中断线程的正常执行(而这里没有线程阻塞,所以不用担心)。
  • 如果多个线程在sigwait调用时等待的是同一个信号,就会出现线程阻塞。当信号递送时,只能有一个线程可以从sigwait中返回。
  • 要把信号发送到进程,可以调用kill;要把信号发送到线程,可以调用pthread_kill。
  • 闹钟定时器是进程资源,并且所有的线程共享相同的alarm。
  • 当线程调用fork时,就为子进程创建了整个进程地址空间的副本。子进程通过继承整个地址空间的副本,也从父进程那里继承了所有互斥量、读写锁和条件变量的状态。
  • 在子进程内部只存在一个线程,是由父进程调用fork的线程的副本构成的。如果父进程中的线程占有锁,子进程同样占有这些锁。问题是子进程并不包含占有锁的线程的副本(如果锁不是由调用fork的线程所占有),所以子进程没有办法知道它占有了哪些锁并且需要释放哪些锁。因此如果子进程不是fork返回以后马上调用某个exec函数,就需要清除锁状态先,可以通过调用pthread_atfork(prepare,parent,child)来建立fork处理程序。perpare是父进程在fork子进程前调用,它的任务是获取父进程定义的所有锁。parent是在fork创建了子进程以后,但在fork返回之前在父进程环境中调用,它的任务是对prepare获得的所有锁进行解锁。child是在fork返回之前在子进程环境中调用,它也是释放prepare获得的所有锁。注意不会出现加锁一次解锁两次的情况。因为通过prepare,子进程可以得到父进程定义的所有锁的副本。所以子进程释放的是父进程锁的副本。
  • 由于进程中的所有线程共享相同的文件描述符,pread和pwrite函数在多线程环境下是非常有帮助的,用来解决并发线程对同一文件进程读写操作的问题(考虑一个线程在lseek和read之间被另一个线程lseek和read,则产生结果不符合预期,但pread可使得lseek和read成为原子操作)。















你可能感兴趣的:(思考/翻译/总结)