1. 线程互斥
- 互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用。
1.1 临界资源、临界区、原子性
- 临界资源:被多个执行流同时访问的共享资源就叫做临界资源。
- 临界区:每个线程内部,访问临界资源的代码,就叫做临界区。
- 原子性:不会被任何调度机制打断的操作,该操作只有两态,要么别做,要么做完。
1.2互斥量mutex
- 大部分情况,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量归属单个线程,其他线程无法获得这种变量。
- 但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完成线程之间的交互。
- 多个线程并发的操作共享变量,会带来一些问题。
下面写个多个线程操作共享变量来抢票的售票系统代码:
1.为什么可能无法获得争取结果?
if 语句
判断条件为真以后,代码可以并发的切换到其他线程。usleep
这个模拟漫长业务的过程中,可能有很多个线程会进入该代码段。ticket--
操作本身就不是一个原子操作。
要解决以上问题,需要做到三点:
- 代码必须要有互斥行为:当代码进入临界区执行时,不允许其他线程进入该临界区。
- 如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只能允许一个线程进入该临界区。
- 如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区。
要做到这三点,本质上就是需要一把锁。Linux上提供的这把锁叫互斥量(mutex)。
1.3互斥量的接口
初始化互斥量:
销毁互斥量:
销毁互斥量需要注意:
- 不要销毁一个已经加锁的互斥量。
- 已经销毁的互斥量,要确保后面不会有线程再尝试加锁。
互斥量加锁和解锁:
注意:在特定线程/进程拥有锁的期间,有新的线程来申请锁,pthread_ mutex_lock调用会陷入阻塞(执行流被挂起),等待互斥量解锁。unlock之后对线程进程唤醒操作。此外,加锁的粒度越小越好。
- 锁的申请是将lock由1变为0;
- 锁的销毁时将lock由0变为1。
改进上面的抢票系统:
1.4互斥量(锁)实现原理
每个线程的寄存器是私有的,在修改数据的时候用的不是拷贝而是xchgb交换,将寄存器的值和内存的值互换。这保证了锁的原子性。因为其他线程申请的话,内存的值为0,申请不到锁。lock:0表示被占, 1表示可以被申请。
- 整个过程为1的mutex只有一份。
- exchange一条汇编完成了寄存器和内存数据的交换。
2. 可重入函数&&线程安全
- 线程安全:多个线程并发同一段代码时,不会出现不同的结果。常见对全局变量或者静态变量进行操作,并且没有锁保护的情况下,会出现该问题。
- 重入:同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则,是不可重入函数。
2.1 常见的线程不安全的情况
- 不保护共享变量的函数
- 函数状态随着被调用,状态发生变化的函数
- 返回指向静态变量指针的函数
- 调用线程不安全函数的函数
3. 死锁
死锁是指在一组进程中的各个进程均占有不会释放的资源,但因为互相申请被其他进程所占用而不会释放的资源,而处于的一种永久等待状态。
3.1 死锁四个必要条件
- 互斥条件:一个资源每次只能被一个执行流使用。
- 请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放。
- 不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺。
- 循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系。
3.2 避免死锁的方法
- 破坏死锁的四个必要条件
- 加锁顺序一致
- 避免锁未释放的
- 资源一次性分配
4.线程同步
同步概念:在保证数据安全(一般使用加锁方式
)的情况下,让线程能够按照某种特定的顺序访问临界资源,就叫做同步。
- 为什么要存在同步?
- 使多线程同步高效的完成某些事情。
同步实现的事情:当有资源的时候,可以直接获取资源,没有资源的时候,线程进行等待,等待另外的线程生产一个资源,当生产完成的时候,通知等待的线程。
4.1条件变量
- 当一个线程互斥地访问某个变量时,它可能发现在其它线程改变状态之前,它什么也做不了。
- 例如一个线程访问队列时,发现队列为空,它只能等待,只到其它线程将一个节点添加到队列中,这种情况就需要用到条件变量。
竞态条件:因为时序问题,而导致程序异常,我们称之为竞态条件。在线程场景下,这种问题也不难理解。
条件变量的本质:PCB等待队列+两个接口(等待接口+唤醒接口)
4.2条件变量函数
- 定义条件变量
pthread_cond_t 条件变量类型
- 初始化条件变量
int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);
参数:
cond:传入条件变量的地址
attr:条件变量的属性,一般设置为NULL,采用默认属性
- 销毁(释放动态初始化的条件变量所占用的内存)
int pthread_cond_destroy(pthread_cond_t *cond)
- 等待条件满足(将调用该等待接口的执行流放入PCB等待队列当中)
int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);
参数:
cond:传入条件变量的地址
restrict mutex:传入互斥锁变量的地址
- 唤醒等待(通知PCB等待当中的执行流来访问临界资源)
int pthread_cond_signal(pthread_cond_t *cond);
参数:
cond:传入条件变量的地址
//唤醒至少一个PCB等待队列当中的线程
4.3 为什么会有互斥锁?
- 同步并没有保证互斥,意味着不同的执行流可以在同一时刻去访问临界资源,所以需要条件变量中的互斥锁来保证互斥,各执行流在访问临界资源的时候,只有一个执行流可以访问。
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
pthread_mutex_t lock;
pthread_cond_t cond;
void *task_t2(void *arg)
{
const char *name = (char*)arg;
while(1){
pthread_cond_wait(&cond, &lock);
printf("get cond : %s 活动...\n", name);
}
}
void *task_t1(void *arg)
{
const char *name = (char*)arg;
while(1){
sleep(rand()%3+1);
pthread_cond_signal(&cond);
printf("%s signal done...\n", name);
}
}
int main()
{
pthread_mutex_init(&lock, NULL);
pthread_cond_init(&cond, NULL);
pthread_t t1,t2,t3,t4,t5;
pthread_create(&t1, NULL, task_t1, "thread1");
pthread_create(&t2, NULL, task_t2, "thread2");
pthread_create(&t3, NULL, task_t2, "thread3");
pthread_create(&t4, NULL, task_t2, "thread4");
pthread_create(&t5, NULL, task_t2, "thread5");
pthread_join(t1, NULL);
pthread_join(t2, NULL);
pthread_join(t3, NULL);
pthread_join(t4, NULL);
pthread_join(t5, NULL);
pthread_mutex_destroy(&lock);
pthread_cond_destroy(&cond);
return 0;
}
转载:https://blog.csdn.net/weixin_50843868/article/details/115874565
查看评论