线程
1. 线程概述
进程是CPU分配资源的最小单位,线程是操作系统调度执行的最小单位。
线程是轻量级的进程,在Linux环境下线程的本质仍是进程。
线程和进程的区别:
- 进程间的信息难以共享。由于除去只读代码段外,父子进程并未共享内存,因此必须采用一些进程间通信方式,在进程间进行信息交换。
- 调用 fork() 来创建进程的代价相对较高,即便利用写时复制技术,仍然需要复制诸如内存页表和文件描述符表之类的多种进程属性,这意味着 fork() 调用在时间上的开销依然不菲。
- 线程之间能够方便、快速地共享信息。只需将数据复制到共享(全局或堆)变量中即可。创建线程比创建进程通常要快 10 倍甚至更多。
- 线程间是共享虚拟地址空间的,无需采用写时复制来复制内存,也无需复制页表。
1.1 线程的相关函数
/*一般情况下, main函数所在的线程称之为主线程(main线程),其余创建的线程称之为子线程
程序中默认只有一个进程,fork()函数调用,2进程
程序中默认只有一个线程,pthread_create函数调用,2个线程
*/
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
/*
功能: 创建一个子线程
参数:
- thread: 传出参数,线程创建成功后,子线程的线程ID被写到该变量中
- attr: 设置的线程属性,一般使用默认值 NULL
- start_routine: 函数指针,这个函数是子线程需要处理的逻辑代码
- arg: 给第三个参数使用, 传参 (给第三个参数函数里传参数)
返回值:
成功0, 失败错误号。这个错误号和之前的errno不太一样, 实现一样,但体系不是一个
获取错误号信息: char *strerror(int errnum);
Compile and link with -pthread. 编译链接时需要链接到-pthread库
主线程退出后子线程的状态依赖于它所在的进程,如果进程没有退出的话子线程依然正常运转。如果进程退出了,那么它的所有线程都会退出
*/
#include <pthread.h>
void pthread_exit(void *retval);
功能;终止一个线程,在哪个线程中调用就表示终止哪个线程
参数:
retval: 需要传递一个指针,作为一个返回值,可以在pthread_join()中获取到这个返回值
pthread_t pthread_self(void);
功能: 获取当前线程的id
int pthread_equal(pthread_t t1, pthread_t t2);
功能:比较两个线程id是否相等
不同的操作系统,pthread_t的实现不一样,有的是无符号的长整型,有的是结构体实现的,所以不能用==
#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);
功能: 和一个已经终止的线程进行连接
回收子线程的资源
这个函数是阻塞函数,调用一次只能回收一个子线程
一般在主线程中使用
参数:
-thread: 需要回收的子线程的id
-retval: 接收子线程退出时候的返回值(return xxx)
#include <pthread.h>
int pthread_detach(pthread_t thread);
功能: 分离一个线程。被分离的线程在终止的时候,会自动释放资源返回给系统, 是非阻塞的
1. 不能多次分离,会产生不可预料的行为
2. 不能去连接join一个已经分离的线程,会报错
参数:
thread: 需要分离的线程ID
返回值: 成功0, 失败返回错误号
#include <pthread.h>
int pthread_cancel(pthread_t thread);
功能: 取消线程,让线程终止
取消某个线程,可以终止某个线程的运行 【前提是可以取消,有两个属性 指定是否可以取消】
但是并不是立马终止,而是当子线程运行到一个取消点,线程才会终止
取消点: 系统规定好的的一些系统调用,我们可以粗略的理解从用户区到内核区的切换,这个位置称之为取消点
1.2 线程属性相关的函数
// 线程属性类型 pthread_attr_t
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);
- 设置线程分离的状态属性
detachstate:
PTHREAD_CREATE_DETACHED 设置线程分离
PTHREAD_CREATE_JOINABLE 可连接的,不分离的,默认是这个
2. 线程同步
临界区是指访问某一共享资源的代码片段,并且这段代码的执行应为原子操作,也就是
同时访问同一共享资源的其他线程不应终止该片段的执行。
线程同步:即当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作,其他线程才能对该内存地址进行操作,而其他线程则处于等待状态。
2.1互斥量
mutex 由互斥量包含在内的代码(即临界区)最多只能有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
restrict C语言的修饰符,被修饰的指针不能由另外的一个指针操作
int pthread_mutex_destroy(pthread_mutex_t *mutex);
- 释放互斥量的资源,销毁互斥量
int pthread_mutex_lock(pthread_mutex_t *mutex);
- 加锁,阻塞的,如果有一个线程加锁了,那么其他的线程只能阻塞等待
int pthread_mutex_trylock(pthread_mutex_t *mutex);
- 尝试加锁,如果加锁失败,不会阻塞,直接返回
int pthread_mutex_unlock(pthread_mutex_t *mutex);
- 解锁
2.2 死锁
有时,一个线程需要同时访问两个或更多不同的共享资源,而每个资源又都由不同的互斥量管理。当超过一个线程加锁同一组互斥量时,就有可能发生死锁。
两个或两个以上的进程在执行过程中,因争夺共享资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁。
死锁的几种场景:
- 忘记释放锁
- 重复加锁
- 多线程多锁,抢占锁资源
2.3 读写锁
在对数据的读写操作中,更多的是读操作,写操作较少,例如对数据库数据的读写应用。为了满足当前能够允许多个读出,但只允许一个写入的需求,线程提供了读写锁来实现。
读写锁的特点:
- 如果有其它线程读数据,则允许其它线程执行读操作,但不允许写操作。
- 如果有其它线程写数据,则其它线程都不允许读、写操作。
- 写是独占的,写的优先级高(也可以设置读的优先级更高,但是可能发生另一方饥饿的情况,因此也可以设置成公平读写锁,用队列把请求锁的线程排队,先入先出,会更公平)。
// 读写锁的类型 pthread_rwlock_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);
2.4 条件变量
注意:条件变量本身不是锁,要和互斥锁配合使用
条件变量cond使在多线程程序中用来实现“等待-->唤醒”逻辑常用的方法,是线程间同步的一种机制。条件变量用来阻塞一个线程,直到条件被满足触发为止,通常情况下条件变量和互斥量同时使用。
一般条件变量有两个状态:
(1)一个/多个线程为等待“条件变量的条件成立“而挂起;
(2)另一个线程在“条件变量条件成立时”通知其他线程。
// 条件变量的类型 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); // 唤醒等待的1个或多个
// 唤醒1个或多个等待的线程
int pthread_cond_broadcast(pthread_cond_t *cond); // 广播 唤醒所有的
// 唤醒所有的等待的线程
这里的条件变量应用在生产者消费者模型中时,并没有考虑篮子会满的情况;也没有体现出当前生产了多少个产品
当pthread_cond_wait()被唤醒后,必须重新抢夺互斥锁,不能直接执行任务,否则可能发生虚假唤醒。通常的解决办法是在线程被激活后还需要检测等待的条件是否满足
还有一个问题。唤醒丢失
详见下方链接:
https://blog.csdn.net/just_kong/article/details/98871393
2.5 信号量
PV操作
// 信号量的类型 sem_t
int sem_init(sem_t *sem, int pshared, unsigned int value);
/*
初始化信号量
参数:
-sem: 信号量变量的地址
-pshared: 0用在线程, 非0用在进程
-value: 信号量中的值 z
*/
int sem_destroy(sem_t *sem);
// 释放资源
int sem_wait(sem_t *sem);
// 对信号量加锁 调用一次对信号量的值减一, 若信号量的值是0则阻塞 相当于P操作
int sem_trywait(sem_t *sem);
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
int sem_post(sem_t *sem);
// 对信号量解锁,调用一次对信号量的值加一 相当于V操作
int sem_getvalue(sem_t *sem, int *sval);
// 获取信号量的值 成功返回0,失败返回-1
条件变量和信号量的区别:
- 条件变量可以一次唤醒所有等待者,信号量没有这个功能
- 信号量始终有一个值(状态的),而条件变量是没有的,没有地方记录唤醒(发送信号)过多少次,也没有地方记录唤醒线程(wait返回)过多少次。从实现上来说一个信号量可以是用mutex + counter + condition variable实现的。因为信号量有一个状态,如果想精准的同步,那么信号量可能会有特殊的地方。信号量可以解决条件变量中存在的唤醒丢失问题。
信号量最有用的场景是用以指明可用资源的数量。
标签:rwlock,int,mutex,pthread,线程,sem From: https://www.cnblogs.com/Yuqi0/p/17169377.html