首页 > 系统 > Unix > 正文

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

2024-06-28 13:24:24
字体:
来源:转载
供稿:网友
《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 thread4. 线程创建
  • 在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 failure6.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种状态:读模式下加锁状态、写模式下加锁状态、不加锁状态。
  • 一次只有一个线程可以占有写模式的读写锁,但是多个线程可以同时占有读模式的读写锁

发表评论 共有条评论
用户名: 密码:
验证码: 匿名发表