一、线程参数传递
线程传递的参数类型为void*,传递方式有值传递、指针传递、引用传递
-
值传递:拷贝一份值给新的线程,多线程间不共享
-
址传递(指针传递):
- 连续创建多个线程中如果传入的是同一变量地址,多线程共享这变量,由于哪个线程先运行是不确定的,变量的值是不可控的,并且会造成共享资源竞争问题,因为修改变量的值不是原子操作
- 如果主线程先结束,其内存空间会被释放,子进程中的指针就成了野指针,可想而知,继续操作这块内存会造成意想不到的后果。
结果方案:定义的变量使用堆内存分配,可以避免栈内存释放,也可以使用全局变量
二、线程分离
线程分离后,线程退出自动释放全部资源:pthread_detach()
三、线程资源的回收
非分离状态的线程才可以被join回收资源
-
阻塞回收:pthread_join()
-
非阻塞回收:pthread_tryjoin_np()
-
限时阻塞回收:pthread_timedjoin_np()
四、线程清理函数
线程终止的时候,可以调用清理函数释放资源,入栈和出栈函数必须成对的出现
-
清理函数入栈:pthread_cleanup_push()
-
清理函数出栈:pthread_cleanup_pop()(0:出栈不执行 非零:出栈并执行)
五、线程取消
-
线程在运行过程中可以调用被取消:pthread_cancel()
-
线程被取消后,join返回值为PTHREAD_CANCELED 即 -1
-
设置线程的取消状态:pthread_setcancelstate()
宏:PTHREAD_CANCEL_ASYNCHRONOUS: 立即取消
宏:PTHREAD_CANCEL_DEFERRED: 到达取消点(例如sleep())才取消
设置线程的取消点:pthread_testcancel()
六、线程与信号
- 向指定线程发送信号:pthread_kill()
- 信号屏蔽:进程:sigpromask() 线程:pthread_sigmask()
- 阻塞等待信号:sigwait()、sigwaitinfo()、sigtimedwait()
- 在多线程中,外部向进程发送信号(ctl+c)不会中断系统调用(sleep())
- 在多线程中,信号的处理是所有线程共享的(即信号注册在任一线程中注册都可以)
- 进程中的信号可以送达单个线程,会中断系统调用
- 如果某个线程因为信号(SIGTERM默认处理)而终止,整个进程将终止
七、线程安全
-
多个线程访问共享资源(全局和静态变量)的时候会冲突
例:定义全局变量int a=0; 线程1和线程2同时:循环执行 a++ 一万次 得到a的值小于2万,a并不会自动到两万 -
三个概念:原子性、可见性、顺序性
- 原子性:
一个操作(有可能包含有多个子操作)要么全部执行(生效)要么全部都不执行(都不生效)
CPU执行指令:读取指令、读取内存、执行指令、写回内存
例(非原子性):第一不读取指令:i++ 第二步:从内存中读取i的值 第三步:把i+1 第四步:把结果写回内存
-
可见性:
当多个线程并发访问共享变量时,一个线程对共享变量的修改,其它线程能够立即看到。
CPU有高速缓存。每个线程读取共享变量时,会将该变量从内存加载到CPU的缓存中,修改该变量后,CPU会立即更新缓存,但不一定会立即将它写回内存。此时其它线程访问该变量,从内存中读到的是旧数据,而非第一个线程更新后的数据。 -
顺序性:
程序执行的顺序按照代码的先后顺序执行。
CPU为了提高程序整体的执行效率,可能会对代码进行优化,按照更高效的顺序执行代码。
CPU虽然并不保证完全按照代码顺序执行,但它会保证程序最终的执行结果和代码顺序执行时的结果一致。
例:int a = 1; a = 2; a = 3; 编译器对代码优化为int a = 3;
- 原子性:
-
volatile关键字:保证可见性和禁止代码优化,但不是原子操作
-
解决线程安全问题:原子操作(c++原子类)、和线程同步(锁)
原子操作:本质是总线锁
八、线程同步
1. 互斥锁
等待锁的时候,线程会休眠,不会消耗CPU,适合等待时间可能很长的场景
声明锁:
pthread_mutex_t mutex;
-
普通锁: PTHREAD_MUTEX_TIMED_NP
当一个线程加锁以后,其余请求锁的线程将形成一个等待队列,并在解锁后按优先级获得锁。
这种锁策略保证了资源分配的公平性。 -
嵌套锁:PTHREAD_MUTEX_RECURSIVE_NP
允许同一个线程对同一个锁成功获得多次,并通过多次unlock解锁。
如果是不同线程请求,则在加锁线程解锁时重新竞争。 -
适应锁:PTHREAD_MUTEX_ADAPTIVE_NP
解锁后,请求锁的线程重新竞争。
int pthread_mutex_init(); //初始化锁,也可定义时直接使用宏初始化如上
int pthread_mutex_lock(); //等待并加锁
int pthread_mutex_trylock(); //尝试加锁,不等待
int pthread_mutex_timedlock();//带超时机制的加锁
int pthread_mutex_unlock() //解锁
int pthread_mutex_destroy(); //销毁锁
2. 自旋锁
循环的加测锁是否可用,会消耗CPU,适合等待时间很短的场景
pthread_spinlock_t mutex; //声明锁
int pthread_spin_init(); //初始化锁
int pthread_spin_lock(); //等待并加锁
int pthread_spin_trylock(); //尝试加锁,不等待
int pthread_spin_unlock(); //解锁
int pthread_spin_destroy(); //销毁锁
4. 读写锁
读时共享,写时单独,适用于读的次数远大于写的场景
-
定义锁:
pthread_rwlock_t mutex; //声明锁 PTHREAD_RWLOCK_INITIALIZER //使用宏初始化锁 int pthread_rwlock_init(); //初始化锁 int pthread_rwlock_destroy(); //销毁锁
-
锁属性:
int pthread_rwlockattr_getpshared();//获取读写锁属性 int pthread_rwlockattr_setpshared();//设置读写锁属性 PTHREAD_PROXESS_PRIVATE(单个线程私有) PTHREAD_PROCESS_SHARED(多线程共享)
-
读锁:
int pthread_rwlock_rdlock(); //阻塞获取读锁 int pthread_rwlock_tryrdlock(); //尝试获取读锁,不阻塞 int pthread_rwlock_timedrdlock(); //获取读锁,带超时机制
-
写锁:
int pthread_rwlock_wrlock(); //阻塞获取写锁 int pthread_rwlock_trywrlock(); //尝试获取写锁,不阻塞 int pthread_rwlock_timedwrlock(); //获取写锁,带超时机制
注意:只有在不加锁时,才能获取到写锁。linux系统优先考虑获取读锁,获取写锁的线程需要等待所有读锁释放才能获得到锁
九、条件变量
pthread_cond_t cond; //声明条件变量
PTHREAD_COND_INITIALIZER; //使用宏初始化条件变量
int pthread_cond_init(); //初始化条件变量
int pthread_cond_destroy(); //销毁条件变量
int pthread_cond_wait(); //等待被唤醒进行加锁
int pthread_cond_timedwait();//等待被唤醒进行加锁,带超时机制
int pthread_cond_signal(); //唤醒至少一个等待中的线程
int pthread_cond_broadcast();//唤醒全部等待中的线程
int pthread_condattr_getpshared();//获取共享属性
int pthread_condattr_setpshared();//设置共享属性(单个线程私有、多线程共享)
十、信号量
多进程的信号量可以用在多线程中,而多线程的信号量只能用于多线程中,多线程的信号量使用比较简单
sem_t *sem; //声明信号量
int sem_init(); //初始化信号量
int sem_destroy();//销毁信号量
int sem_wait(sem_t *sem); //信号量的P操作
int sem_trywait(sem_t *sem);//信号量的P操作,不阻塞
int sem_timedwait(); //信号量的P操作,带超时机制
int sem_post(sem_t *sem); //信号量的V操作
int sem_getvalue(); //获取信号量的值
注意:以上几种同步机制都能形成等待队列,但是不是绝对公平的,当前线程的cpu时间片还未使用完,获取到锁的概率会很大
十一、生产者消费者模型
- 互斥锁+条件变量实现
- 信号量实现
十二、保证多线程程序的稳定性
- 主进程只用与监控和调度
- 程序的功能由子线程实现,把心跳写入全局变量
- 如果心跳超时,取消子线程再重新启动
十三、多线程实现异步通讯
主线程创建socket连接,一个子线程负责发送,另一个子线程负责接收