关于线程本人力推苏丙榅老师(曾经担任黑马讲师)的视频讲解,B站与老师自己博客都有老师的讲解,内容通俗易懂非常适合学习,本人线程学习皆从老师处学习。
https://subingwen.cn/linux/thread-sync/
上一章节讲述了线程的创建与使用,本章节主要讲述线程同步;
为什么要线程同步?
举个栗子,相信年轻一的的小伙伴都有想过,假如我银行卡内有300块,我提到支付宝的时候同一时间在ATM里取300,那是不是就能赚他300? 很遗憾,银行可不会给你钻空子,此时就引出了线程同步的概念
线程同步的概念:
很多小伙伴认为线程同步是执行的同步,实则不然;所谓线程同步,其实是多线程情况下对于内存中的共享资源进行访问修改的时候,使这个共享资源按顺序被修改,所谓的同步并不是多个线程同时对内存进行访问,而是按照先后顺序依次进行的。
其实使用原子变量也可以达到以上效果,而且不用开锁解锁,更加节省资源,有兴趣的小伙伴可以去了解一下,这里不过多赘述
1.1 同步方式
对于多个线程访问共享资源出现数据混乱的问题,需要进行线程同步。常用的线程同步方式有四种:互斥锁、读写锁、条件变量、信号量。所谓的共享资源就是多个线程共同访问的变量,这些变量通常为全局数据区变量或者堆区变量,这些变量对应的共享资源也被称之为临界资源。
1.2互斥锁
创建的锁对象中保存了当前这把锁的状态信息:锁定还是打开,如果是锁定状态还记录了给这把锁加锁的线程信息(线程 ID)。一个互斥锁变量只能被一个线程锁定,被锁定之后其他线程再对互斥锁变量加锁就会被阻塞,直到这把互斥锁被解锁,被阻塞的线程才能被解除阻塞。一般情况下,每一个共享资源对应一个把互斥锁,锁的个数和线程的个数无关。
// 初始化互斥锁
// restrict: 是一个关键字, 用来修饰指针, 只有这个关键字修饰的指针可以访问指向的内存地址, 其他指针是不行的
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
const pthread_mutexattr_t *restrict attr);
// 释放互斥锁资源
int pthread_mutex_destroy(pthread_mutex_t *mutex);
参数:
mutex: 互斥锁变量的地址
attr: 互斥锁的属性,一般使用默认属性即可,这个参数指定为 NULL
加锁
int pthread_mutex_lock(pthread_mutex_t *mutex);//给代码段加锁
线程会判断当前锁的状态:
如果是上锁状态:线程会阻塞在此处
如果为未锁:线程加锁并记录锁状态,向下执行
尝试加锁
// 尝试加锁
int pthread_mutex_trylock(pthread_mutex_t *mutex);
线程会判断当前锁的状态:
如果是上锁状态:线程不会被阻塞,加锁失败直接返回错误号
如果为未锁:线程加锁并记录锁状态,向下执行
解锁
// 对互斥锁解锁
int pthread_mutex_unlock(pthread_mutex_t *mutex);
1.3读写锁
读写锁相当于高级的互斥锁,就像svip与vip一样。
创建:
pthread_rwlock_t rwlock;
之所以称其为读写锁,是因为这把锁既可以锁定读操作,也可以锁定写操作。为了方便理解,可以大致认为在这把锁中记录了这些信息:
1.1 锁的状态:锁定 / 打开
1.2 锁定的是什么操作:读操作 / 写操作,使用读写锁锁定了读操作,需要先解锁才能去锁定写操 作,反之亦然。
1.3 哪个线程将这把锁锁上了
特点:1.使用读写锁的读锁锁定了临界区,线程对临界区的访问是并行的,读锁是共享的。
2.使用读写锁的写锁锁定了临界区,线程对临界区的访问是串行的,写锁是独占的。
3.使用读写锁分别对两个临界区加了读锁和写锁,两个线程要同时访问者两个临界区,访问写锁临界区的线程继续运行,访问读锁临界区的线程阻塞,因为写锁比读锁的优先级高。
加读锁:
// 在程序中对读写锁加读锁, 锁定的是读操作
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
加写锁:
// 在程序中对读写锁加写锁, 锁定的是写操作
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
解锁:
// 解锁, 不管锁定了读还是写都可用解锁
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
1.4条件变量
一般情况下条件变量主要针对消费者模型,并且和互斥锁配合使用
- 严格意义上来说,条件变量的主要作用不是处理线程同步,而是进行线程的阻塞。如果在多线程程序中只使用条件变量无法实现线程的同步,必须要配合互斥锁来使用。
- 条件变量只有在满足指定条件下才会阻塞线程,如果条件不满足,多个线程可以同时进入临界区,同时读写临界资源,这种情况下还是会出现共享资源中数据的混乱。
pthread_cond_t cond;//创建条件变量
// 初始化
int pthread_cond_init(pthread_cond_t *restrict cond,
const pthread_condattr_t *restrict attr);
// 销毁释放资源
int pthread_cond_destroy(pthread_cond_t *cond);
参数
- cond为对象地址
- attr一般情况下置空,使用默认属性
同步
// 线程阻塞函数, 哪个线程调用这个函数, 哪个线程就会被阻塞
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
若条件变量在此处阻塞,会自动打开传入的互斥锁,以便其他的线程正常运行,理论上来说可以使全部的消费者线程阻塞在此处,当生产者线程抢到cpu时间片后正常加锁执行后调用解除阻塞函数;使消费者线程正常执行,此时阻塞在此处的消费者线程会抢夺此处的互斥锁,如何正常执行,如果没抢到锁,则继续阻塞;
解除阻塞:
// 唤醒阻塞在条件变量上的线程, 至少有一个被解除阻塞
int pthread_cond_signal(pthread_cond_t *cond);
// 唤醒阻塞在条件变量上的线程, 被阻塞的线程全部解除阻塞
int pthread_cond_broadcast(pthread_cond_t *cond);
此文章为作者学习时的记录,如有缺陷多多包涵