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);
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);
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);
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);
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);
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);
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);
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);
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);
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);
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);
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);
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);
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);
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);
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);
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);
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);
if OK, error number on failure
  • 初始化屏障时,可以使用count参数指定,在允许所有线程继续运行之前,必须到达屏障的线程数目。
  • 使用attr参数指定屏障对象的属性
#include <pthread.h>
int pthread_barrier_wait(pthread_barrier_t *barrier);
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
     

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

  1. [置顶] 文件和目录(二)--unix环境高级编程读书笔记

    在linux中,文件的相关信息都记录在stat这个结构体中,文件长度是记录在stat的st_size成员中.对于普通文件,其长度可以为0,目录的长度一般为1024的倍数,这与linux文件系统中blo ...

  2. unix环境高级编程-读书笔记与习题解答-第一篇

    从这周开始逐渐的进入学习状态,每天晚上都会坚持写c程序,并且伴随对这本书的深入,希望能写出更高质量的读书笔记和程序. 本书的第一章,介绍了一些关于unix的基础知识,在这里我不想去讨论linux到底是 ...

  3. unix 环境高级编程-读书笔记与习题解答-第二篇

    第四节 输入与输出 上次的笔记中写到的 open, read, write, lseek 以及close ,都是不带缓存的IO函数,这些函数都使用文件描述符进行工作. 上一篇笔记用到的 read(ST ...

  4. [置顶] 文件io(一)--unix环境高级编程读书笔记

    unix-like(后面以linux为例)系统中的文件操作只需要五个函数就足够了,open.close.read.write以及lseek.这些操作被称为不带缓存的io,这里有必要说一下带缓存和不带缓 ...

  5. unix 环境高级编程 读书笔记与习题解答第四篇

    第一章 第六节 第一小节 这一章没有程序设计和API方面的深入学习,而是注重介绍了unix操作系统中的原始数据类型和系统原型函数,错误处理方面的知识. ____unistd.h____ 该文件包含了u ...

  6. unix进程的环境--unix环境高级编程读书笔记

    http://blog.csdn.net/xiaocainiaoshangxiao/article/category/1800937

  7. unix环境高级编程 读书笔记

    1.上班业余时间把书下载下来,第一章读完了,但是程序只能回家运行啦!Fighting!

  8. 《UNIX环境高级编程》笔记--errno是否是线程安全的?

    当UNIX函数出错时,常常返回一个负数,而且整形变量errno通常被设置为含有附加信息的一个值,例如,open函数如成功,返回 一个非负文件描述符,如果出错就返回-1,在open出错时,有大约15种不 ...

  9. Unix环境高级编程学习笔记——fcntl

    写这篇文正主要是为了介绍下fcntl,并将我自己在学习过程中的一些理解写下来,不一定那么官方,也有错误,希望指正,共同进步- fcntl: 一个修改一打开文件的性质的函数.基本的格式是 int fcn ...

随机推荐

  1. oracle插入或更新某一个指定列来执行触发器

    表结构: create table TZ_GXSX ( ID VARCHAR2(15), PROJECT VARCHAR2(50), TXYX NUMBER(22) default '0', CDAT ...

  2. C# 基础复习 四 多线程

    单线程和多线程的区别     单线程:         只用主线程处理,如果一个操作在占用主线程,那么其他操作则无法执行     多线程:         除了主线程外,还开启了子线程来执行操作,子线 ...

  3. Java压缩图片

    阅读目录 前言 压缩的要求 实现 优点 其他功能 前言 作为靠谱的java服务端程序员,图片这个事情一直是个头疼的事情. 现在很多网站上,都有上传图片这个功能,而图片对于现在的很多手机来说,拍摄出来的 ...

  4. jq——动画

    基本 1 show(可加时间)显示[在效果完成后可执行函数] 2 hide(可加时间)隐藏 3 toggle():切换效果 [在show和hide中切换] 有函数时 滑动动画 1 slideDown: ...

  5. HDU1027 Ignatius and the Princess II( 逆康托展开 )

    链接:传送门 题意:给出一个 n ,求 1 - n 全排列的第 m 个排列情况 思路:经典逆康托展开,需要注意的时要在原来逆康托展开的模板上改动一些地方. 分析:已知 1 <= M <= ...

  6. [USACO18JAN] MooTube (离线并查集)

    题目大意:给你一棵边权树,定义两点间距离为它们唯一路径上的最小路权,求与某点距离不大于K(k为已知)的点的数量 带权并查集维护集合内元素总数 路和问题 都按权值大到小排序,枚举问题, 建权值不小于K的 ...

  7. 一、frp官方中文文档

    frp 是一个可用于内网穿透的高性能的反向代理应用,支持 tcp, udp, http, https 协议. 目录 frp 的作用 开发状态 架构 使用示例 通过 ssh 访问公司内网机器 通过自定义 ...

  8. cron 和anacron 、日志转储的周期任务

    一.cron是开机自动启动的 [root@localhost ~]# chkconfig --list | grep "cron" crond 0:off 1:off 2:on 3 ...

  9. debian 9 安装后的配置,debian 9 开发环境。

    注意:以下命令用sudo或者以root用户进行 一.Xterm(在安装KDE桌面情况下)的配置(可以黏贴,复制): 首先在根目录下编辑文件.Xresources(没有可以创建) root@debian ...

  10. MyEclipse 设置JSP,HTML的默认打开方式,避免出现打开后上面出现浏览器

    1. 2. 3. jsp的设置一样,这样myeclipse打开jsp就不会出现上面的浏览器了