线程有一个很大的问题就是线程安全问题。
由于多个线程都可以看到进程的全局变量,那么在多个线程都可以读取和修改它,这样就会存在线程安全的问题。所以说,虽然线程间的通信变简单了,但是与此同时也太来了问题(双刃剑)。
先举一个例子看一下线程不安全的情况会产生什么现象
- #include <iostream>
- #include <pthread.h>
- #include <unistd.h>
- using namespace std;
-
- int ticket = 666;//票数总量
- void* fuc(void* arg)
- {
- long long num = (long long)arg;
- while(1)
- {
- if(ticket > 0)
- {
- usleep(1000);
- cout<<"i am thread "<<num<<",i got ticket num is "<<ticket<<endl;
- ticket--;
- }
- else
- break;
- }
- return NULL;
- }
- int main()
- {
-
- pthread_t tid[4];
- for(long long i=0;i < 4;i++)//创建四个线程去抢票
- pthread_create(tid+i,nullptr,fuc,(void*)i);
-
- for(int i=0;i < 4;i++)//线程回收
- pthread_join(tid[i],nullptr);
- return 0;
- }
运行结果:
圈红的地方可以看出多个线程对同一资源进行修改时在不加保护的情况下会出现意想不到的错误。
两个概念
1、临界资源
线程可以看到的同一块资源。上面的代码中ticket就是临界资源
2、临界区
指访问某一共享资源的代码片段,对应上面的程序就是if判断的那一部分对ticket操作的代码。
在未加锁的情况下,临界区的代码是非原子的。
即ticket--需要经历三个过程
在这三个过程的任意一个过程中都有可能会切出去。假设线程1正在加载数据时切出去了,切出去前ticket的大小为100。另一个线程过来也加载数据看到的ticket也是100。结果二者都抢的是100这张票就会出错。
确保同时仅有一个线程可以访问某项共享资源,保证对任意共享资源的原子访问。
注意:至多只有一个线程可以锁定互斥量,如果不这样。可能会导致阻塞线程或者直接出错。
定义一把锁,互斥锁的类型是pthread_mutex_t类型。
初始化一把锁
第一个参数传入定义锁的地址,第二个参数设置锁的属性,设置为空表示为默认的属性。
PTHREAD_MUTEX_INITIALIZER
对于静态分配的互斥量来说,可以用PTHREAD_MUTEX_INITIALIZER来初始化。
static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
对于静态初始化的互斥量无需调用pthread_mutex_destroy()函数进行销毁,而且这把锁的属性是默认值。
销毁一把锁
当不再需要经由自动或动态分配的互斥量时,应使用 pthread_mutex_destroy()将其销毁。只有当互斥量处于未锁定状态,且后续也无任何线程企图锁定它时,将其销毁才是安全的。
加锁
解锁
对上面的代码做出以下更改
- while(1)
- {
- pthread_mutex_lock(&lock)
- if(ticket > 0)
- {
- usleep(1000);
- cout<<"i am thread "<<num<<",i got ticket num is "<<ticket<<endl;
- ticket--;
- pthread_mutex_unlock(&lock)
- }
- else
- {
- pthread_mutex_unlock(&lock)
- break;
- }
- }
注意所有线程看到同一把锁,所也是临界资源。
锁的实现是原子的,保证自身不会出现线程安全的问题。
死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所站用不会释放的资源而处于的一种永久等待状态。
通俗一点就是进程1有锁1,进程2有锁2,进程1想要锁2,进程2想要锁1,但二者都不是放自己本身的锁,就会出现死锁。
死锁的必要条件
1、互斥条件:一个资源只能被一个执行流使用
2、请求与保持条件:一个执行流对以获得的资源保持不放
3、不可剥夺条件:不能强行剥夺某个线程的资源
4、循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系
在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题,叫做同步。
就好比当某个线程需要等待另一个线程对数据修改完毕后才能在执行自己的线程该执行的,如果提前来到线程里发现条件不满足只能只能干等着。例如一个线程负责往队列里面塞数据,一个线程负责往队列里面取数据,当队列为空的时候取数据的线程就没必要执行,而是等待队列里面有数据后再执行。
条件等待是线程间同步的一种手段,如果只有一个线程,条件不满足,一直等下去都不会满足,所以必须要有一个线程通过某些操作,改变共享变量,使原先不满足的条件变得满足,并且友好的通知等待在条件变量上的线程。
定义条件变量
初始化条件变量
基本用法与pthread_mutex_init类似。
销毁条件变量
等待条件满足
pthread_cond_wait()函数需要传入一个互斥量,也就是说总有一个条件变量和互斥量相关。
通常情况下,当前线程执行pthread_cond_wait时,处于临界区访问共享资源,存在一个mutex与该临界区相关联,也就是以下的形式。
- pthread_mutex_lock(&lock);
- pthread_cond_wait(&cond,&lock);
- pthread_mutex_unlock(&lock);
pthread_cond_wait会执行如下的三个操作
之所以要先解锁互斥量
1、可以让其他线程也进入临界区(通俗来说就是不要站着茅坑不拉屎)
2、避免死锁的发生(如果一个线程占有着这把锁,但又不放手,其他线程也想要这把锁就会被阻塞,如果这个请求这把锁资源的线程其中的一个任务是唤醒上一个线程,则会产生死锁,例如生产者消费者模型)
之所以要重新锁定
因为此时的被唤醒的线程仍处于临界区,所以需要互斥锁来保证线程安全。
唤醒等待
对cond所指定的条件变量发送通知。当pthread_cond_wait()收到通知后就不再阻塞。
pthread_cond_broadcast是一种广播方式的唤醒,会根据加入等待队列中的先后顺序依次唤醒他们,就是如果有多个线程pthread_cond_wait在同一条件变量下,会有一个等待队列,根据谁先调用pthread_cond_wait,谁就在队列的前面。而 pthread_cond_signal只是从队列中唤醒一个。
细节:条件变量的判断条件要用while而不是if
每个条件变量都有与之相关的判断条件,涉及一个或多个共享变量。当线程被唤醒时,一定要再次检查等待的条件是否满足,如果满足执行后面的代码,如果不满足就重新进入等待。
因为存在几种可能
1、其他线程可能会率先醒来。可能会使原本满足的条件不在满足。
2、可能发生虚假唤醒的可能。即使没有在此条件变量上发通知,在此条件变量上等待的线程也有可能被唤醒。
同步的实际例子
- #include <iostream>
- #include <pthread.h>
- #include <unistd.h>
- using namespace std;
-
- int ticket = 666;
- pthread_mutex_t lock;
- pthread_cond_t cond;
- void* GetTickets(void* arg)//获取票
- {
- long long num = (long long)arg;
- while(1)
- {
- pthread_mutex_lock(&lock);
- if(ticket > 0)
- {
- usleep(1000);
- cout<<"i am thread "<<num<<",i got ticket num is "<<ticket<<endl;
- ticket--;
- pthread_mutex_unlock(&lock);
- }
- else
- {
- pthread_mutex_unlock(&lock);
- pthread_cond_signal(&cond);//当没票的时候发通知添加票
- }
- }
- return NULL;
- }
- void* AddTickets(void* arg)
- {
- while(1)
- {
- pthread_mutex_lock(&lock);
- while(ticket > 0)//循环检测条件是否满足
- pthread_cond_wait(&cond,&lock);
- ticket += 100;
- pthread_mutex_unlock(&lock);
- }
- return nullptr;
- }
- int main()
- {
- pthread_mutex_init(&lock,nullptr);//初始化
- pthread_cond_init(&cond,nullptr);
- pthread_t tid[4];
- for(long long i=0;i < 4;i++)//创建四个线程去抢票
- pthread_create(tid+i,nullptr,GetTickets,(void*)i);
-
- pthread_t tid_t;
- pthread_create(&tid_t,nullptr,AddTickets,nullptr);//创建一个线程去添加票
-
- for(int i=0;i < 4;i++)
- pthread_join(tid[i],nullptr);
- pthread_join(tid_t,nullptr);
-
- pthread_mutex_destroy(&lock);
- pthread_cond_destroy(&cond);
- return 0;
- }
没票的时候就会去通知添加票的线程去增加票。