- 与进程(process)类似,线程(thread)是允许应用程序并发执行多个任务的一种机制。一个进程可以包含多个线程。同一个程序中的所有线程均会独立执行相同程序,且共享同一份全局内存区域,其中包括初始化数据段、未初始化数据段,以及堆内存段。(传统意义上的 UNIX 进程只是多线程程序的一个特例,该进程只包含一个线程)
进程是 CPU 分配资源的最小单位,线程是操作系统调度执行的最小单位。线程是轻量级的进程(LWP: Light Weight Process),在 Linux 环境下线程的本
质仍是进程。
查看指定进程的 LWP 号: ps –Lf pid
- 线程和进程的区别
进程间的信息难以共享,需要采取一些进程间通信方式来实现信息交换。当调用fork
创建进程时,需要复制内存页表、文件描述符表等进程属性以及对应的虚拟地址空间(写时复制),其效率较低。
线程之间的通信很方便,因为线程会共享全局和堆变量,其虚拟地址空间也是共享的,因而相对于进程而言,省去了复制虚拟地址空间的开销。创建了线程后,线程共享全局和堆变量,而栈变量则是区分开的,具体的方式是将栈区分成若干个区域分给各个线程使用(.text段同样也是分成若干个区来存储各个线程的代码).
线程共享资源:pid,ppid, pgid,sid, uid, ugid, 文件描述符表,信号处置(sighandler), 文件系统信息(umask, 当前工作目录)以及虚拟地址空间(除栈,.text)
线程非共享资源:线程ID,信号掩码,线程特有数据,errnum,实时调度策略和优先级,实时调度策略和优先级
- 线程操作函数
NPTL库是第三方库,编译时需要手动指定包含该库,具体的方式是gcc a.c -o app -l pthread
或gcc a.c -o app -pthread
创建子线程(一般情况下,main函数所在的线程为主线程,其余线程为子线程)
- 原型:
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
- 参数:
- thread: 传出参数,创建成功后子线程的线程号就写入该变量中
- attr: 设置线程的属性,一般使用默认值即NULL
- start_routine: 子线程需要处理的逻辑代码
- arg: 给第三个参数使用的
- 返回值:成功返回0,失败返回错误号,和前边的errno意义不一样,错误应该使用
char *strerror(int)
而不是void perror(char *)
, 且thread是未指定的
- 原型:
退出子线程:在哪个线程调用标识终止哪个线程
- 原型:
void pthread_exit(void *retval);
- 参数: retval,指针,作为一个返回值,可以在pthread_join()获取.
- 原型:
- 获取当前线程的线程号
- 原型:
pthread_t pthread_self(void);
- 原型:
- 比较两个线程的线程ID是否一样
- 原型:
int pthread_equal(pthread_t t1, pthread_t t2);
- 注意:不同操作系统的pthread_t类型实现不一样,有的是无符号长整型,有的是结构体
- 原型:
- 和一个已经终止的线程进行连接
- 原型:
int pthread_join(pthread_t thread, void **retval);
- 功能:连接其他线程然后释放回收该线程的资源,阻塞函数,和wait一样,调用一次只会回收一个子线程,一般只在主线程中使用
- 参数:
- thread: 需要回收的子线程线程号
- retval:接受子线程的返回值
- 返回值:0表示成功,非零表示错误号
- 原型:
- 分离和一个线程的连接
- 原型:
int pthread_detach(pthread_t thread);
- 功能:分离后该线程会被标记,被标记的线程终止时会自动释放资源,而不需要人为调用
pthread_join
来释放。注意不能进行多次分离,且标记后不能进行连接。 - 参数:需要分离的线程id
- 返回值:成功为0,失败为错误号
- 原型:
- 取消子线程: 相当于停止线程,但不是调用完该函数就立刻终止对应的子线程,而是需要子线程运行到取消点的时候才会终止。(取消点是系统规定号的一些系统调用,可以粗略认为由用户态切换到内核态的地方)
- 原型:
int pthread_cancel(pthread_t thread);
- 参数:指定线程的id
- 原型:
- 线程属性操作函数(shell中输入
man pthread_attr_
加两次tab
可以查看有哪些函数)
int pthread_attr_init(pthread_attr_t *attr);
:初始化线程属性变量int pthread_attr_destroy(pthread_attr_t *attr);
:释放线程属性变量int pthread_attr_getdetachstate(const pthread_attr_t *attr, int *detachstate);
:获取线程分离的状态属性int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);
: 设置线程分离的状态属性
- 线程同步
线程可以通过全局变量或堆变量来共享信息,但是带来一个坏处就是数据的安全性,由于每个线程都可能在同一时间对数据进行修改,会出现某些线程数据不正确的情况。临界区就是指线程中访问某一共享资源的代码片段且这段代码的执行应该是原子操作,即同时访问同一共享资源的其他线程不应终端该片段的执行。
线程同步:即当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作,其他线程才能对该内存地址进行操作,而其他线程则处于等待状态。线程同步是以降低并发效率为前提,但为了数据安全性,这是必须的,且线程同步只是应用于临界区,其他区域不进行同步。
5.1 互斥量
为避免线程更新共享变量时出现问题,可以使用互斥量/互斥锁(mutex 是 mutual exclusion 的缩写)来确保同时仅有一个线程可以访问某项共享资源。可以使用互斥量来保证对任意共享资源的原子访问。互斥量有两种状态:已锁定(locked)和未锁定(unlocked)。任何时候,至多只有一个线程可以锁定该互斥量。试图对已经锁定的某一互斥量再次加锁,将可能阻塞线程或者报错失败,具体取决于加锁时使用的方法。
一旦线程锁定互斥量,随即成为该互斥量的所有者,只有所有者才能给互斥量解锁。一般情况下,对每一共享资源(可能由多个相关变量组成)会使用不同的互斥量,每一线程在访问同一资源时将采用如下协议
- 针对共享资源锁定互斥量
- 访问共享资源
- 对互斥量解锁
互斥量相关函数(类型为pthread_mutex_t)
- 初始化互斥量
- 原型:
int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);
- 参数:
- mutex: 需要初始化的互斥量变量
- attr:互斥量属性,NULL
- 原型:
- 销毁互斥量
- 原型:
int pthread_mutex_destroy(pthread_mutex_t *mutex);
- 参数:mutex待销毁的互斥量变量
- 原型:
- 锁定互斥量:阻塞
- 原型:
int pthread_mutex_lock(pthread_mutex_t *mutex);
- 参数:mutex为待上锁的互斥量变量
- 原型:
- 尝试锁定互斥量:非阻塞
- 原型:
int pthread_mutex_trylock(pthread_mutex_t *mutex);
- 参数:mutex为想要尝试上锁的互斥量变量
- 原型:
- 解锁互斥量
- 原型:
int pthread_mutex_unlock(pthread_mutex_t *mutex);
- 参数:mutex为待解锁的互斥量变量
- 原型:
5.2 死锁
有时,一个线程需要同时访问两个或更多不同的共享资源,而每个资源又都由不同的互斥量管理。当超过一个线程加锁同一组互斥量时,就有可能发生死锁。
两个或两个以上的进程在执行过程中,因争夺共享资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁
死锁的几种场景:1)忘记释放锁;2)重复加锁;3)多线程多锁,抢占锁资源。
5.3 读写锁
当有一个线程已经持有互斥锁时,互斥锁将所有试图进入临界区的线程都阻塞住。但是考虑一种情形,当前持有互斥锁的线程只是要读访问共享资源,而同时有其它几个线程也想读取这个共享资源,但是由于互斥锁的排它性,所有其它线程都无法获取锁,也就无法读访问共享资源了,但是实际上多个线程同时读访问共享资源并不会导致问题。
在对数据的读写操作中,更多的是读操作,写操作较少,例如对数据库数据的读写应用。为了满足当前能够允许多个读出,但只允许一个写入的需求,线程提供了读写锁来实现。
读写锁的特点:
- 如果有其它线程读数据,则允许其它线程执行读操作,但不允许写操作。
- 如果有其它线程写数据,则其它线程都不允许读、写操作。
- 写是独占的,写的优先级高。
相关操作函数(类型为pthread_relock_t)
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock);
const pthread_rwlockattr_t *restrict attr)
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
- 生产者消费者模型
生产者消费者模型中的对象分别有:生产者、消费者以及容器。当容器中没有产品时,消费者应该被阻塞并且发送信号到生产者;相对的,容器中的产品满了之后,生产者应该被阻塞,并且发送信号到消费者(通过条件变量或信号量来实现)。
6.1 条件变量(pthread_cond_t,
当条件满足时才执行相关代码,否则阻塞线程。条件变量并不是锁,只是配合互斥变量等方法来实现线程同步,本身并不能保证数据安全。
int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);
:初始化条件变量int pthread_cond_destroy(pthread_cond_t *cond);
:销毁条件变量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 abstime);
:至多等待多长的时间,阻塞线程直至到点int pthread_cond_signal(pthread_cond_t *cond);
:至少唤醒一个消费者int pthread_cond_broadcast(pthread_cond_t *cond);
: 唤醒所有消费者
6.2 信号量(sem_t,
信号量,也称为信号灯,作用类似于条件变量,同样是阻塞线程,不能保证数据安全。
- 初始化信号量
- 原型:
int sem_init(sem_t *sem, int pshared, unsigned int value);
- 参数:
- sem为信号量变量的地址
- pshared表示sem被线程共享还是进程共享,为0则线程间共享,非零则是进程间共享
- value为初始状态下的值,可以理解为上述模型中的产品数量
- 原型:
int sem_destroy(sem_t *sem);
:释放资源int sem_wait(sem_t *sem);
: 对信号量加锁,当sem对应的value大于0时,则将value减1并直接返回,否则阻塞线程直至value大于0int sem_trywait(sem_t *sem);
:和sem_wait
类似,但当value为0时返回EAGAIN
而不再阻塞线程int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
:至多阻塞线程一定时长,如果由于时长退出阻塞则会置位错误号ETIMEOUT
int sem_post(sem_t *sem);
:对信号量解锁,调用一次信号量的值+1int sem_getvalue(sem_t *sem, int *sval);
- 其他
getconf GNU_LIBPTHREAD_VERSION
:查看当前 pthread 库版本