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

进程的所有信息对该进程的所有线程都是共享的,包括可执行的程序文本、程序的全局内存和堆内存、栈以及文件描述符。 进程ID在整个系统中是唯一的,线程ID不同,后者只在它所属的进程环境中有效。 线程ID的类型为pthread_t,但不同系统实现方式不同,有的用int表示,有的用long,有的用结构指针,因此要注意可移植性。int pthread_equal(pthread_t pid1, pthread_t pid2)用于比较两个线程ID是否相等。 在程序调试过程中打印线程ID有时是非常有用的。pthread_t pthread_self()函数可以获取自身的线程ID。 程序以单进程中的单个控制线程启动。新增线程可以通过pthread_create()函数来创建。线程创建时并不能保证哪个线程会先运行:是新的线程还是调用线程(创建线程的线程)。 int pthread_create(pthread_t *restrict tidp, const pthread_attr_t *restrict attr, void *(*start_rtn)(void), void *restrict arg); 参数tidp:指向的内存单元被设置为新创建线程的线程ID。 参数attr:新创建线程的属性。 参数start_rtn:新创建线程从这个函数的地址开始运行。 参数arg:函数start_rtn的参数地址(可多参数打包为结构体)。 pthread函数在调用失败时通常会返回错误码,而不像其它的POSIX函数一样设置errno。从函数中返回错误码更为清晰整洁,不需要依赖那些随着函数执行不断变化的全局状态(比如errno),因此可以把错误的范围限制在引起出错的函数中。

相关阅读:
UNIX环境高级编程》(第二版)apue.h的错误
UNIX环境高级编程中文第二版PDF高清版

主线程在创建子线程之后,一般需要休眠,否则主线程可能很快退出了,这样在新线程有机会运行之前整个进程可能就已经终止了。 在Linux系统里,在主子线程里获取的线程ID可能不同(getpid()),这与linux的线程实现有关,linux使用clone系统调用来实现pthread_create。clone系统调用创建子进程,这个子进程可以共享父进程一定数量的执行环境(如fd,内存),这个数量是可配置的。相当于用子进程来模拟了子线程的概念。 不要在线程调度上做任何假设(主子线程的执行顺序)。 如果进程中的任一线程调用了exit, _Exit或_exit,那么整个进程就会终止。类似地,如果信号的默认动作是终止进程,那么把该信号发送到线程会终止整个进程。 单个线程有以下三种退出方式,而不会终止整个进程: 线程从启动例程中返回,返回值是线程的退出码。 线程可以被同一进程中的其他线程取消。 线程调用pthread_exit。 pthread_join(pthread_t thread, void **rval_ptr)用于阻塞调用进程,直到指定的线程从以上三种退出方式中的一种返回。此函数的第1个参数thread表示要等待的线程ID,第2个参数包含返回码,如果对线程的返回值并不感兴趣,则可以将rval_ptr设为NULL。 进程中的其它线程可以通过调用pthread_join函数来获得某线程的退出状态,rval_ptr也可以传递包含更复杂信息的结构的地址,但是注意这个结构所使用的内存在调用者完成调用以后必须仍然是有效的,否则就会出现无效或非法内存访问。比如,线程在自己的栈上分配了一个结构,然后把指向这个结构的指针传给pthread_exit,那么调用pthread_join来等待此线程的调用线程试图使用该结构时,这个栈有可能已经被撤销(被等待的线程已经从启动例程返回了,栈已收回),这块内存也已另作他用(访问得到的结构值无意义了)。为了解决这个问题,可以使用全局结构或者用malloc函数分配结构。 线程可以通过调用pthread_cancel(pthread_t pid)函数来请求取消同一进程中的其他线程,此函数并不等待线程终止,它仅仅提出请求。 线程可以安排它退出时需要调用的函数,这样的函数称为线程清理处理程序(thread cleanup handler)。线程可以建立多个清理处理程序。处理程序记录在栈中,也就是说它们的执行序列与注册的顺序相反。通过调用pthread_cleanup_push(void(*rtn)(void *), void *arg)注册。而当程序执行以下动作时会调用清理函数: 调用pthread_exit时。 响应取消请求时。 用非零execute参数调用pthread_cleanup_pop(int execute)。如果execute置为0,清理函数将不被调用,但会出栈删除。 如果线程是通过从它的启动例程中返回而终止的话,那么它的清理处理程序就不会被调用。 如果线程已经处于分离(detach)状态,则线程的底层存储资源可以在线程执行时(被系统自动)立即收回。当线程被分离时,并不能用pthread_join来等待它的终止状态。对分离状态的线程调用pthread_join会产生失败,返回EINVAL。pthread_detach(pthread_t tid)调用可以使线程处于分离状态,也可以在pthread_create创建线程时通过指定attr来完成。 当多个控制线程共享相同的内存时,需要确保每个线程看到一致的数据视图。当某个线程可以修改变量,而其他线程也可以读取或者修改这个变量时,就需要对这些线程进行同步,以确保它们在访问变量的存储内容时不会访问到无效的数值。 如果修改是原子操作,那么就不存在竞争。但是在现代计算机系统中,存储器访问需要多个总线周期,多处理器的总线周期通常在多个处理器上是交叉的,所以无法保证数据是顺序一致的。 互斥量(mutex)用pthread_mutex_t数据类型表示。使用之前需要初始化,可将其置为常量 PTHREAD_MUTEX_INITIALIZER(只对静态互斥量),也可通过pthread_mutex_init函数进行初始化。如果动态地分配互斥量,则在释放内存之前 需要调用pthread_mutex_destroy。 对互斥量加锁,需要调用pthread_mutex_lock,若互斥量已上锁,则调用线程阻塞。可调用pthread_mutex_trylock尝试加锁,若加锁失败,不会阻塞而是返回EBUSY。调用pthread_mutex_unlock解锁。 如果释放互斥锁时有多个线程阻塞,则这些线程均会变成可运行状态,第一个变为运行状态的线程可以对互斥量加锁,其它线程只能继续等待。 只有在一个线程试图以与另一个线程相反的顺序锁住互斥量时,才可能出现死锁。 当同时需要用两个互斥量时,总是让它们以相同的顺序加锁,以避免死锁。 如果锁的粒度太粗,就会出现很多线程阻塞等待相同的锁,源自并发性的改善微乎其微。如果锁的粒度太细,那么过多的锁开销会使系统性能受到影响,而且代码会变得相当复杂。作为一个程序员,需要在满足锁需求的情况下,在代码复杂性和优化性能之间找到平衡点。 虽然读写锁的实现各不相同,但当读写锁处于读模式锁住状态时,如果有另外的线程试图以写模式加锁,读写锁通常会阻塞随后的读模式锁请求,防止读模式锁长期占用,而等待的写模式锁请求一直得不到满足。 读写锁非常适合于对数据结构读的次数远大于写的情况。 读写锁相关调用函数:初始化 pthread_rwlock_init(rwlock,arr); 销毁锁pthread_rwlock_destroy(rwlock); 读模式锁定 pthread_rwlock_rdlock(rwlock); 写模式锁定 pthread_rwlock_wrlock(rwlock); 解锁 pthread_rwlock_unlock(); 读模式试锁定 pthread_rwlock_tryrdlock(rwlock); 写模式试锁定 pthread_rwlock_trywrlock(rwlock); 要在释放读写锁占用的内存之前调用destroy函数做清理工作,否则如果调用之前就释放了锁占用内存,则分配给这个锁的资源就丢失了(无法destroy了)。 条件变量给多个线程提供了一个会合的场所。条件变量与互斥量一起使用时,允许线程以无竞争的方式等待特定的条件发生。 初始化 pthread_cond_init(cond, attr); 销毁 pthread_cond_destroy(cond);等待条件变为真 pthread_cond_wait(cond,mutex); pthread_cond_timedwait(cond,mutex,timeout);后者指定了等待的时间。通知线程条件已经满足pthread_cond_signal(cond)和pthread_cond_broadcast(cond);前者是唤醒等待该条件的某线程,后者唤醒所有等待线程。

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/wwgpfd.html