《Unix环境高级编程》读书笔记 第11章-线程

1. 引言

  • 了解如何使用多个控制线程在单进程环境中执行多个任务。
  • 不管在什么情况下,只要单个资源需要在多个用户键共享,就必须处理一致性问题。

2. 线程概念

  • 典型的Unix进程可以看成只有一个控制线程:一个进程在某一时刻只能做一件事情。
  • 多线程带来的好处:
    1. 通过为每种事件类型分配单独的处理线程,可以简化处理异步事件的代码。每个线程在进行事件处理时可以采用同步编程模式。
    2. 多个进程必须使用操作系统提供的复制机制才能实现内存和文件描述符的共享。而多个线程自动地可以访问相同的存储空间和文件描述符。
    3. 有些问题可以分解从而提高整个程序的吞吐量。将原来串行化执行的任务变成交叉进行,当然,这些任务必须相互独立、互不依赖。
    4. 交互的程序同样可以通过使用多线程来改善响应时间,多线程可以把程序中处理用户输入输出的部分与其他部分分开。
  • 处理器的数量并不影响程序结构,所以不管处理器的个数多少,程序都可以通过使用线程得以简化。而且,即使多线程程序在串行化任务时不得不阻塞,在某些线程在阻塞的时候还有另外一些线程可以运行,所以多线程程序在单处理器上运行还是可以改善响应时间和吞吐量。
  • 我们讨论的线程接口来自POSIX.1-2001,称之为pthread。功能测试宏是_POSIX_THREADS,也可以使用_SC_THREADS常数调用sysconf函数。

3. 线程标识

  • 进程ID在整个系统中是唯一的。而线程ID只有在它所属的进程上下文中才有意义。
  • 线程ID使用数据类型pthread_t表示,可以用一个结构来代表pthread_t,故须使用下面的函数来对两个线程ID进行比较

#include <pthread.h>

int pthread_equal(pthread_t tid1, pthread_t tid2);

 Returns: nonzero if equal, 0 otherwise
  • 通过pthread_self函数获得自身的线程ID

#include <pthread.h>

pthread_t pthread_self(void);

 Returns: the thread ID of the calling thread

4. 线程创建

  • 在POSIX线程的情况下,程序开始运行时,它也是以单进程中的单个控制线程启动的。在创建多个控制线程之前,程序的行为与传统的进程并没有什么区别。
  • 通过调用pthread_create函数创建新的线程

#include <pthread.h>

int pthread_create(pthread_t *restrict tidp, const pthread_attr_t *restrict attr, void *(*start_rtn)(void *), void *restrict arg);

 Returns: 0 if OK, error number on failure
  • 新创建线程的线程ID会设置到tidp指向的内存单元中
  • atrr参数用于定制各种不同的线程属性。直NULL时,创建一个具有默认属性的线程
  • 新创建的线程从start_rtn函数的地址开始运行,该函数只有一个无类型指针参数arg。故如果需要向start_rtn函数传递的参数有一个以上,需要把这些参数放到一个结构中,传递该结构的地址
  • 线程创建时并不能保证哪个线程先运行:是新创建的线程,还是调用线程。
  • 新创建的线程可以访问进程的地址空间,并且继承调用线程的浮点环境和信号屏蔽字,但是该线程的挂起信号集会被清除,即被原线程阻塞之后收到的信号集不会被新线程继承。
  • 注意:pthread函数在调用失败时通常会返回错误码,它们并不像其他的POSIX函数一样设置errno。每个线程都提供errno的副本,这只是为了与使用errno的现有函数兼容。

5. 线程终止

  • 如果进程中的任意线程调用了exit、_Exit、_exit,那么整个进程就会终止
  • 如果默认的动作是终止进程,那么,发送到某个线程的信号就会终止整个进程
  • 单个线程可以通过3种方式退出,而不终止整个进程
    1. 线程可以简单地从启动例程中返回,返回值是线程的退出码
    2. 线程可以被同一进程中的其他线程取消
    3. 线程调用pthread_exit

#include <pthread.h>

void pthread_exit(void *rval_ptr);

#include <pthread.h>

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

 Returns: 0 if OK, error number on failure
  • 进程中的其他进程可以通过调用pthread_join函数访问到pthread_exit函数的指针参数rval_ptr
  • 调用线程将一直阻塞,直到指定的线程调用pthread_exit、从启动例程中返回或者被取消。
    1. 如果线程从启动例程中返回,rval_ptr包含返回码
    2. 如果线程被取消,由rval_ptr指定的内存单元就设置为PTHREAD_CANCELED
    3. 如果线程调用pthread_exit,rval_ptr指向的内存单元作为返回值传递给调用pthread_join函数的其他线程
  • 线程可以通过调用pthread_cancel函数来请求取消同一进程中的其他线程

#include <pthread.h>

int pthread_cancel(pthread_t tid);

 Returns: 0 if OK, error number on failure
  • 在默认情况下,pthread_cancel函数会使得由tid标识的线程的行为表现为如同调用了参数为PTHREAD_CANCELED的pthread_exit函数。但是,线程可以选择忽略取消或控制如何被取消。
  • 注意:pthread_cancel并不等待线程终止,它仅仅提出请求。

#include <pthread.h>

void pthread_cleanup_push(void (*rtn)(void *), void *arg);

void pthread_cleanup_pop(int execute);
  • 线程可以安排它退出时需要调用的函数,类似于进程的atexit函数。这样的函数称为线程清理处理程序
  • 一个线程可以建立多个清理处理程序。处理程序记录在栈中,执行顺序与注册顺序相反。
  • 当线程执行以下动作时,由pthread_cleanup_push函数安排的清理函数rtn以单个参数arg被调用:
    1. 调用pthread_exit时
    2. 响应取消请求时
    3. 用非零execute参数调用pthread_cleanup_pop时(以0调用pthread_cleanup_pop函数时,清理函数不被调用)
  • 这些函数有一个限制,因其可以实现为宏,故必须在与现场相同的作用于中以匹配对的形式使用
  • 进程和线程原语的对比
  • 默认情况下,线程的终止状态会保存直到对该线程调用pthread_join。如果线程已经被分离,线程的底层存储资源可以在线程终止时立即被收回。在线程被分离后,不能用pthread_join函数等待它的终止状态,调用该函数会产生未定义行为。
  • 可以调用函数pthread_detach分离线程

#include <pthread.h>

int pthread_detach(pthread_t tid);

 Returns: 0 if OK, error number on failure
  • 可以通过修改传给函数pthread_create的线程属性,创建一个已处于分离状态的线程。

6. 线程同步

  • 当多个控制线程共享相同的内存时,需要确保每个线程看到一致的数据视图。故当一个线程可以修改的变量,其他线程也可以读取或修改时,需要对这些线程进行同步,确保它们在访问变量的存储内容时不会访问到无效的值。
  • 在变量修改时间多于一个存储器访问周期的处理器结构中,当存储器读与存储器写这两个周期交叉时,这种不一致就会出现。
  • 两个或多个线程试图在同一时间修改同一变量时,也需要进行同步。考虑变量增量操作的情况:
  • 5个基本的同步机制:互斥量、读写锁、条件变量、自旋锁、屏障

6.1 互斥量

  • 互斥量从本质上说是一把锁,在访问共享资源前对互斥量进行加锁,在访问完成后释放互斥量。
  • 只有将所有线程都设计成遵守相同数据访问规则的,互斥机制才能正常工作。操作系统并不会为我们做数据访问的串行化。如果允许其中某个线程在没有得到锁的情况下也可以访问共享资源,那么即使其他的线程在使用共享资源前都申请锁,也还是会出现数据不一致的问题。
  • 互斥变量使用数据类型pthread_mutex_t表示,使用互斥变量之前,必须对它进行初始化:
    1. 如果是静态分配的互斥量,可以把它设置为常量PTHREAD_MUTEX_INITIALIZER
    2. 如果是动态分配(通过malloc函数)的互斥量,可以通过调用函数pthread_mutex_init进行初始化;在释放内存前(通过free函数)需要调用pthread_mutex_destroy

#include <pthread.h>

int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);

int pthread_mutex_destroy(pthread_mutex_t *mutex);

 Both return: 0 if OK, error number on failure
  • 要用默认的属性初始化互斥量,只需把attr设为NULL

#include <pthread.h>

int pthread_mutex_lock(pthread_mutex_t *mutex);

int pthread_mutex_trylock(pthread_mutex_t *mutex); // 成功锁住返回0;锁住失败返回EBUSY,不会阻塞

int pthread_mutex_unlock(pthread_mutex_t *mutex);

 All return: 0 if OK, error number on failure

6.2 避免死锁

  • 如果线程试图对同一个互斥量加锁两次,那么它自身就会陷入死锁状态。
  • 还有其他情况也会产生死锁,如线程1先锁住互斥量A,再锁互斥量B;而线程2先锁住互斥量B,再锁住互斥量A。可以通过限制加锁的顺序避免。
  • 有时候,对互斥量的加锁进行排序是很困难的。这种情况下,可以先释放占有的锁,然后过一段时间再试。

6.3 函数pthread_mutex_timedlock


#include <pthread.h>

#include <time.h>

int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex, const struct timespec *restrict tsptr);

 Returns: 0 if OK, error number on failure
  • 该函数允许绑定线程阻塞时间,超时后返回错误码ETIMEDOUT
  • 指定愿意等待的绝对时间

6.4 读写锁

  • 读写锁允许更高的并行性
  • 读写锁可以有3种状态:读模式下加锁状态、写模式下加锁状态、不加锁状态。
  • 一次只有一个线程可以占有写模式的读写锁,但是多个线程可以同时占有读模式的读写锁
  • 当读写锁是写加锁状态下,在这个锁被解锁之前,所有试图对这个锁加锁的线程都会阻塞
  • 当读写锁是读加锁状态下, 继续以读模式加锁可以得到访问权,而以写模式加锁则阻塞。为避免读模式长期占用,当写模式锁的请求到达后,阻塞之后到达的读模式锁请求。
  • 读写锁非常适合于对数据结构读的次数远大于写的情况。
  • 读写锁也称为共享互斥锁,以共享模式锁住,以互斥模式锁住。

#include <pthread.h>

int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);

int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);

 Both return: 0 if OK, error number on failure
  • 数据类型pthread_rwlock_t
  • 读写锁在使用之前必须初始化,在释放它们的底层内存之前必须销毁。

    如果默认属性就足够的话,可以使用常量PTHREAD_RWLOCK_INITIALIZER对静态分配的读写锁进行初始化
    在释放动态分配的读写锁占有内存之前,需要调用pthread_rwlock_destroy函数做清理工作。否则,会导致分配给该锁的资源丢失


#include <pthread.h>

int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);

int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);

int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);

 All return: 0 if OK, error number on failure

#include <pthread.h>

int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);

int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);

 Both return: 0 if OK, error number on failure

6.5 带有超时的读写锁


#include <pthread.h>

#include <time.h>

int pthread_rwlock_timedrdlock(pthread_rwlock_t *restrict rwlock, const struct timespec *restrict tsptr);

int pthread_rwlock_timedwrlock(pthread_rwlock_t *restrict rwlock, const struct timespec *restrict tsptr);

 Both return: 0 if OK, error number on failure
  • 超时指定的是绝对时间。超时后返回ETIMEOUT

6.6 条件变量

  • 条件变量是线程可用的另一种同步机制。
  • 条件变量本身是由互斥量保护的。线程在改变条件状态之前必须首先锁住互斥量。否则可能:一个线程预备等待一个条件变量,当它在真正进入等待之前,另一个线程恰好触发了该条件。

#include <pthread.h>

int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);

int pthread_cond_destroy(pthread_cond_t *cond);

 Both return: 0 if OK, error number on failure
  • 数据类型pthread_cond_t
  • 在使用条件变量之前,必须对它进行初始化:
    1. 如果条件变量是静态分配的,可以赋值为常量PTHREAD_COND_INITIALIZER
    2. 如果条件变量是动态分配的,需要使用函数pthread_cond_init对它进行初始化;在释放条件变量底层的内存空间之前,需要使用pthread_cond_destroy对条件变量反初始化

#include <pthread.h>

int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);

int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec *restrict tsptr);

 Both return: 0 if OK, error number on failure
  • 传递给pthread_cond_wait的互斥量对条件进行保护。调用者把锁住的互斥量传给函数,函数然后自动把调用线程放到等待条件的线程列表上,对互斥量解锁。这就关闭了条件检查和线程进入休眠状态等待条件改变这两个操作之间的时间通道。pthread_cond_wait返回时,互斥量再次被锁住。
  • 超时值指定了绝对时间

#include <pthread.h>

int pthread_cond_signal(pthread_cond_t *cond);

int pthread_cond_broadcast(pthread_cond_t *cond);

 Both return: 0 if OK, error number on failure
  • 给线程或条件发信号

6.7 自旋锁

  • 自旋锁与互斥量类似,但它不是通过休眠使进程阻塞,而是在获取锁之前一直处于忙等(自旋)阻塞状态。
  • 自旋锁可用于以下情况:锁被持有的时间短,而且线程并不希望在重新调度上花费太多的时间。
  • 当自旋锁用在非抢占式内核中是非常有用的。但在用户层,自旋锁并不是非常有用,除非运行在不允许抢占的实时调度类中。

#include <pthread.h>

int pthread_spin_init(pthread_spinlock_t *lock, int pshared);

int pthread_spin_destroy(pthread_spinlock_t *lock);

 Both return: 0 if OK, error number on failure

#include <pthread.h>

int pthread_spin_lock(pthread_spinlock_t *lock);

int pthread_spin_trylock(pthread_spinlock_t *lock);

int pthread_spin_unlock(pthread_spinlock_t *lock);

 All return: 0 if OK, error number on failure

6.8 屏障barrier

  • 屏障是用户协调多个线程并行工作的同步机制。屏障允许每个线程等待,直到所有的合作线程都到达某一点,然后从该点继续执行。pthread_join函数就是一种屏障,允许一个线程等待,直到另一个线程退出。
  • 但是屏障对象的概念更广,它们允许任意数量的线程等待,直到所有的线程完成处理工作,而线程不需要退出。

#include <pthread.h>

int pthread_barrier_init(pthread_barrier_t *restrict barrier, const pthread_barrierattr_t *restrict attr, unsigned int count);

int pthread_barrier_destroy(pthread_barrier_t *barrier);

 Both return: 0 if OK, error number on failure
  • 初始化屏障时,可以使用count参数指定,在允许所有线程继续运行之前,必须到达屏障的线程数目。
  • 使用attr参数指定屏障对象的属性

#include <pthread.h>

int pthread_barrier_wait(pthread_barrier_t *barrier);

 Returns: 0 or PTHREAD_BARRIER_SERIAL_THREAD if OK, error number on failure
  • pthread_barrier_wait函数表明,线程已完成工作,准备等所有其他线程赶上来;在屏障计数未满足条件时,该线程会进入休眠状态;如果该线程是最后一个调用pthread_barrier_wait的线程,就满足了屏障计数,所有的线程都被唤醒。


    原创文章,转载请声明出处: http://www.cnblogs.com/DayByDay/p/3948399.html
时间: 2024-10-14 12:34:57

《Unix环境高级编程》读书笔记 第11章-线程的相关文章

UNIX环境高级编程学习笔记(第一章UNIX基础知识)

总所周知,UNIX环境高级编程是一本很经典的书,之前我粗略的看了一遍,感觉理解得不够深入. 听说写博客可以提高自己的水平,因此趁着这个机会我想把它重新看一遍,并把每一章的笔记写在博客里面. 我学习的时候使用的平台是Windows+VMware+debian,使用secureCRT来连接(可以实现多个终端连接). 因为第一章是本书大概的描述,所以第一章的我打算写得详细一点,而且书本的原话占的比例会比较多,重点的东西会用粗体显示出来. 1.1  引言 所有操作系统都为他们所运行的程序提供服务.典型的

《UNIX环境高级编程》笔记——3.文件IO

一.引言 说明几个I/O函数:open.read.write.lseek和close,这些函数都是不带缓冲(不带缓冲,只调用内核的一个系统调用),这些函数不输入ISO C,是POSIX的一部分: 多进程共享资源(包括文件)时,会有很多额外的烦恼,需要对共享资源.原子操作等概念深入理解,需要理解涉及的内核有关数据结构,这些数据结构对理解文件.共享有重要作用: 最后介绍dup.fcntl.sync.fsync和ioctl函数. 二.文件描述符 open或creat文件时,内核--文件描述符fd-->

Unix环境高级编程学习笔记(五):进程控制

1 getpid函数,getppid函数,得到进程id,得到父进程id #include<unistd.h> pid_t getpid(void) pid_t getppid(void) uid_t getuid(void)得到实际用户id uid_t geteuid(void)得到有效用户id gid_t getgid(void)得到实际组id gid_t getegid(void)得到有效组id 2 fork函数,当前进程创建新进程 #include<unistd.h> pid

Unix环境高级编程学习笔记(四):进程环境

1 exit函数与_Exit函数 #include<stdlib.h> void exit(int status) void _Exit(int status) 这两个函数的不同之处在于exit函数先执行清理工作后再进入内核(清理I/O缓冲),_Exit函数直接进入内核 2 atexit函数,登记函数,在exit的时候执行 int atexit(void (* func) (void)); 被登记的函数称为终止处理函数,这些函数的调用顺序与登记顺序相反,如果一个函数被登记多次,也会被调用多次

Unix环境高级编程学习笔记(七):线程

1 线程包含线程ID,一组寄存器的值,栈,调度优先级和策略,信号屏蔽字,errno变量,以及线程私有数据.进程的所有信息对于该进程的所有线程都是共享的,包括可执行程序文本,程序全局内存和堆内存,栈以及文件描述符. 线程可以通过pthread_self函数获得自身线程ID #include<pthread.h> pthread_t pthread_self(void) 新增进程可以通过pthread_create函数创建 #include <pthread.h> int pthrea

Unix环境高级编程学习笔记(三):标准I/O , 系统数据文件和信息

1 标准I/O函数不同于read,write函数,是其在流上进行操作, 当首次调用标准I/O函数时,系统会首先调用malloc,为流创造缓冲区, 2 fopen函数 #include<stdio.h> file * fopen(const char* pathname, const char * restrict name); 打开返回指针,出错返回NULL, type的取指有r(读),w(写),a(追加),r+/w+(读+写),a+(读+写+追加) int fclose(file* fp)

UNIX环境高级编程(阅读笔记)---多线程信号

多线程信号 1.默认情况下,信号将由主进程接收处理,就算信号处理函数是由子线程注册的 2. 每个线程均有自己的信号屏蔽字,可以使用sigprocmask函数来屏蔽某个线程对该信号的响应处理,仅留下需要处理该信号的线程来处理指定的信号. 3. 对某个信号处理函数,以程序执行时最后一次注册的处理函数为准,即在所有的线程里,同一个信号在任何线程里对该信号的处理一定相同 4. 可以使用pthread_kill对指定的线程发送信号 5. 在主进程中对sigmask进行设置后,主进程创建出来的线程将继承主进

apue读书笔记 - 第11章 线程

实例11-1 为在Ubuntu12.04上的运行结果与书中描述的不一致呢? 从pid来看这两个线程属于同一个进程,且线程ID也是指针形式的,Google后得知,书上讲的是以前的LinuxThreads实现,现在linux使用NPTL线程. 参考:Linux 线程模型的比较:LinuxThreads 和 NPTL 习题11.4 在回答该问题之前,我觉得得先弄清楚"互斥量"与"条件变量"之间的关系.因为书上说这两者一般是配套使用的. 考虑如下情况: 子线程B和子线程C都

Windows核心编程读书笔记-第六章线程基础

1.相较于线程,进程所使用的系统资源更多.其原因在于地址空间.为一个进程创建一个虚拟的地址空间需要大量系统资源.线程只有一个内核对象和一个栈. 2.线程的入口函数 DWORD WINAPI ThreadFunc(PVOID pvParam){ DWORD dwResult = 0; ... return(dwResult); } 线程函数的几点说明 线程函数可以任意命名. 线程函数只有一个参数,而其意义由我们(而非操作系统)来定义.因此,我们不必担心ANSI/Unicode问题. 线程函数必须返