概述
进程是系统中程序执行和资源分配的基本单位。 每个进程有自己的数据段、 代码段和堆栈段。 这就造成进程在进行切换等操作时都需要有比较多的上下文切换等动作。 为了进一步减少处理器的空转时间支持多处理器和减少上下文切换开销, 也就出现了线程。
线程是操作系统能够进行运算调度的最小单位。 它被包含在进程之中, 是进程中的实际运作单位。是进程的基本调度单元, 每个进程至少都有一个 main 线程, 它与同进程中的其他线程共享进程空间{ 堆代码 数据 文件描述符 信号等} , 只拥有少量的栈空间, 大大减少了上下文切换的开销。
线程和进程在使用上各有优缺点: 线程执行开销小, 占用的 CPU 少, 线程之间的切换快, 但不利于资源的管理和保护; 而进程正相反。
同进程一样, 线程也将相关的变量值放在线程控制表(TCB) 内。 一个进程可以有多个线程, 也就是有多个线程控制表及堆栈寄存器, 共享一个用户地址空间。 要注意的是, 由于线程共享了进程的资源和地址空间, 因此, 任何线程对系统资源的操作都会给其他线程带来影响。
线程分类
按调度者分为用户级线程和核心级线程
用户级线程: 主要解决上下文切换问题, 调度算法和调度过程全部由用户决定, 在运行时不需要特定
的内核支持。 缺点是无法发挥多处理器的优势
核心级线程: 允许不同进程中的线程按照同一相对优先调度方法调度, 发挥多处理器的并发优势现在大
多数系统都采用用户级线程和核心级线程并存的方法。 对应方式有:
多对一模型: 将多个用户级线程映射到一个内核级线程;
一对一模型: 将一个用户级线程映射到一个内核级线程;
多对多模型: 将多个用户级线程映射到多个内核级线程, 用户级线程数量 > 核心级线程数量
Linux实现线程创建
Linux 的线程是通过用户级的函数库实现的, 一般采用 pthread 线程库实现线程的访问和控制。 它用第 3 方 posix 标准的 pthread, 具有良好的可移植性。 编译的时候要在后面加上 –lpthread
创建 退出 等待
进程 fork() exit() wait()
线程 pthread_create() pthread_exit() pthread_join()
线程的创建和退出
创建线程实际上就是确定调用该线程函数的入口点, 线程的创建采用函数 pthread_create。 在线程创建以后, 就开始运行相关的线程函数, 在该函数运行完之后, 线程就退出, 这也是线程退出的一种方式。另一种线程退出的方式是使用函数 pthread_exit()函数, 这是线程主动退出行为。 这里要注意的是, 在使用线程函数时, 不能随意使用 exit 退出函数进行出错处理, 由于 exit 的作用是使调用进程终止, 往往一个进程包括了多个线程, 所以在线程中通常使用 pthread_exit 函数来代替进程中的退出函数 exit。
由于一个进程中的多个线程是共享数据段的, 因此通常在线程退出之后, 退出线程所占用的资源并不会随着线程的终止而得到释放。 正如进程之间可以通过 wait()函数系统调用来同步终止并释放资源一样, 线程之间也有类似的机制, 那就是 pthread_join 函数。 pthread_join 函数可以用于将当前线程挂起,等待线程的结束。 这个函数是一个线程阻塞函数, 调用它的函数将一直等待直到被等待的线程结束为止,当函数返回时, 被等待线程的资源被回收。
1.线程创建
//函数原型
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
Compile and link with -pthread.
2.线程退出
//函数原型
#include <pthread.h>
void pthread_exit(void *retval);
线程等待与取消
1.线程等待
线程从入口点函数自然返回, 或者主动调用 pthread_exit()函数, 都可以让线程正常终止线程调用 pthread_exit()函数返回时, 函数返回值可以被其它线程用pthread_join 函数获取
#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);
//thid 传递 0 值时, join 返回 ESRCH 错误。
该函数是一个阻塞函数, 一直等到参数 thread 指定的线程返回; 与多进程中的 wait 或 waitpid 类似。retval 是一个传出参数, 接收线程函数的返回值。 如果线程通过调用 pthread_exit()终止, 则pthread_exit()中的参数相当于自然返回值, 照样可以被其它线程用 pthread_join 获取到。
该函数还有一个非常重要的作用, 由于一个进程中的多个线程共享数据段, 因此通常在一个线程退出后, 退出线程所占用的资源并不会随线程结束而释放。 如果 thread 线程类型并不是自动清理资源类型的,则 thread 线程退出后, 线程本身的资源必须通过其它线程调用 pthread_join 来清除,这相当于多进程程序中的waitpid。
2.线程的取消
线程也可以被其它线程杀掉, 在 Linux 中的说法是一个线程被另一个线程取消(cancel)。
线程取消的方法是一个线程向目标线程发 cancel 信号, 但是如何处理 cancel 信号则由目标线程自己决定, 目标线程或者忽略、 或者立即终止、 或者继续运行至 cancelation-point(取消点)后终止。 默认的行为是运行到取消点。
什么是取消点呢?
根据POSIX标准:一些会引起阻塞的系统调用都是取消点。不过经过测试一些非阻塞性函数也可以是取消点。可以通过man 7 pthreads查看,不过由于Linux线程库和C库结合的不是很好,有很多函数没有明确是否为取消点。
总之, 线程的取消一方面是一个线程强行杀另外一个线程, 从程序设计角度看并不是一种好的风格, 另一方面目前 Linux 本身对这方面的支持并不完善, 所以在实际应用中应该谨慎使用!!!
线程同步与互斥
1.线程的互斥
在 Posix Thread 中定义了一套专门用于线程互斥的 mutex 函数。 mutex 是一种简单的加锁的方法来控制对共享资源的存取, 这个互斥锁只有两种状态(上锁和解锁) , 可以把互斥锁看作某种意义上的全局变量。 为什么需要加锁, 就是因为多个线程共用进程的资源, 要访问的是公共区间时(全局变量) ,当一个线程访问的时候, 需要加上锁以防止另外的线程对它进行访问, 实现资源的独占。 在一个时刻只能有一个线程掌握某个互斥锁, 拥有上锁状态的线程能够对共享资源进行操作。 若其他线程希望上锁一个已经上锁了的互斥锁, 则该线程就会挂起, 直到上锁的线程释放掉互斥锁为止。
互斥锁的创建
有两种方法创建互斥锁, 静态方式和动态方式
//静态方式:POSIX 定义了一个宏 PTHREAD_MUTEX_INITIALIZER 来静态初始化互斥锁
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
//动态方式:采用 pthread_mutex_init()函数来初始化互斥锁
//pthread_mutex_init()原型
#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t
*mutexattr)
//方式
pthread_mutex_t mutex;
pthread_mutex_init(&mutex, NULL)
互斥锁的销毁
销毁一个互斥锁即意味着释放它所占用的资源, 且要求锁当前处于开放状态 。
//pthread_mutex_destroy()用于注销一个互斥锁
int pthread_mutex_destroy(pthread_mutex_t *mutex);
互斥锁的属性
互斥锁的属性在创建锁的时候指定, 不同的锁类型在试图对一个已经被锁定的互斥锁加锁时表现不同也就是是否阻塞等待。 有三个值可供选择:
PTHREAD_MUTEX_NORMAL, 这是缺省值(直接写 NULL 就是表示这个缺省值),也就是普通锁(或快速锁)。 普通锁只能对一把锁连续加锁一次,否则就会阻塞,造成死锁。
PTHREAD_MUTEX_RECURSIVE, 嵌套锁, 允许同一个线程对同一个锁成功获得多次, 并通过多次 unlock 解锁。 如果是不同线程请求, 则在加锁线程解锁时重新竞争。
PTHREAD_MUTEX_ERRORCHECK, 检错锁, 如果一个锁已经被使用, 同一个线程再次请求这把锁, 则返回 EDEADLK, 否则与PTHREAD_MUTEX_TIMED_NP 类型动作相同。 这样就保证当不允许多次加锁时不会出现最简单情况下的死锁。 如果锁的类型是快速锁, 一个线程加锁之后, 又
加锁, 则此时就是死锁。
2.死锁
1.死锁产生的原因
- 系统资源的竞争
系统资源的竞争导致系统资源不足, 以及资源分配不当, 导致死锁。 - 进程运行推进顺序不合理
进程在运行过程中, 请求和释放资源的顺序不当, 会导致死锁
2.死锁的四个必要条件
- 互斥条件: 一个资源每次只能被一个进程使用, 即在一段时间内某资源仅为一个进程所占有。 此
时若有其他进程请求该资源, 则请求进程只能等待。 - 请求与保持条件: 进程已经保持了至少一个资源, 但又提出了新的资源请求, 而该资源已被其他
进程占有, 此时请求进程被阻塞, 但对自己已获得的资源保持不放。 - 不可剥夺条件: 进程所获得的资源在未使用完毕之前, 不能被其他进程强行夺走, 即只能由获得该
资源的进程自己来释放(只能是主动释放)。 - 循环等待条件: 若干进程间形成首尾相接循环等待资源的关系
这四个条件是死锁的必要条件, 只要系统发生死锁, 这些条件必然成立, 而只要上述条件之一不满足, 就不会发生死锁。
3.死锁的预防
可以通过破坏死锁产生的 4 个必要条件来预防死锁, 由于资源互斥是资源使用的固有特性是无法改变的。
3.互斥锁的同步
条件变量是利用线程间共享的全局变量进行同步的一种机制, 主要包括两个动作: 一个线程等待条件变量上的条件成立而挂起; 另一个线程使条件成立(给出条件成立信号) 。 为了防止竞争, 条件变量的使用总是和一个互斥锁结合在一起。
1.条件变量的创建
条件变量和互斥锁一样, 都有静态、 动态两种创建方式
//静态方式使 PTHREAD_COND_INITIALIZER 常量, 如下:
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
//动态方式调用 pthread_cond_init()函数, API 定义如下:
int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *cond_attr);
pthread_cond_t cond;
pthread_cond_init(&cond, NULL)
2.条件变量的销毁
注销一个条件变量需要调用 pthread_cond_destroy(), 只有在没有线程在该条件变量上等待的时候能注销这个条件变量, 否则返回 EBUSY。
#include <pthread.h>
int pthread_cond_destroy(pthread_cond_t *cond);
3.条件变量的等待与激发
等待条件有两种方式: 无条件等待 pthread_cond_wait()和计时等待pthread_cond_timedwait();
无条件等待是只要等待的条件得不到满足就一直等待,计时等待是等待一段时间后如果条件不满足就返回。
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex,
const struct timespec *abstime);
无论哪种等待方式, 都必须和一个互斥锁配合, 以防止多个线程同时请求的竞争条件 ;mutex互斥锁必须是普通锁 ,且等待函数内的参数的锁必须和为了互斥访问资源而加的锁是同一把锁!
激发条件有两种形式, pthread_cond_signal()激活一个等待该条件的线程, 存在多个等待线程时按入队顺序激活其中一个; 而 pthread_cond_broadcast()则激活所有等待线程 。
int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond);