线程安全:
- 多个线程同时对操作临界资源而不会出现数据二义性
- 在线程中对临界资源进行了非原子操作
可重入/不可重入 :多个执行流同时进入函数中运行而不会出现问题
如何实现线程安全:
- 同步:临界资源的合理访问
- 互斥:临界资源的同一时间访问
互斥的实现:
互斥锁:0/1计数器
1表示可以加锁,加锁后-1
操作完了之后要解锁,解锁后 +1
0表示不可以加锁,不能加锁则等待
互斥锁的操作步骤:
互斥锁的实现:简单的黄牛抢票
- 定义 互斥锁变量: pthread_mutex_t
- 初始化互斥锁变量:pthread_mutex_init
- 加锁 : pthread_mutex_lock
- 解锁 pthread_mutex_unlock
- 销毁互斥锁 pthread_mutex_destroy
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<pthread.h>
#include<errno.h>
int ticket=10000;
pthread_mutex_t mutex;
void* thr_start(void* arg){
while(1){
//加锁一定要在访问临界资源之前
//int pthread_mutex_lock(pthread_mutex_t* mutex);
//阻塞加锁,加不上就阻塞
//int pthread_mutex_trylock(pthread_mutex_t* mutex);
//尝试加锁,加不上锁直接报错返回
//int pyhread_mutex_timelock(pthread_mutex_t* mutex);
//限时阻塞加锁
pthread_mutex_lock(&mutex);
if(ticket>0){
sleep(1);
printf("yellow bull---%d---get a ticket is %d \n",(int)arg,ticket);
ticket--;
}else{
//在任何有可能的地方要解锁
pthread_mutex_unlock(&mutex);
pthread_exit(NULL);
}
pthread_mutex_unlock(&mutex);
}
return NULL;
}
int main(){
pthread_t tid[4];
int i=0,ret;
//int pthread_mutex_init(pthread_t thread,pthread_mutexattr_t* attr);
//thread:互斥锁变量
//attr:属性,通常置NULL
pthread_mutex_init(&mutex,NULL);
for(i=0;i<4;++i){
int ret=pthread_create(&tid[i],NULL,thr_start,(void*)i);
if(ret!=0){
printf("yellow %d is not exist\n",i);
return -1;
}
}
for(i=0;i<4;++i){
//此时不关心返回值 置NULL就可以
pthread_join(tid[i],NULL);
}
//int pthread_mutex_destroy(pthread_mutex_t* mutex );
//mutex : 互斥锁变量
pthread_mutex_destroy(&mutex);
return 0;
}
同步的实现:
临界资源访问的合理性-------生产出来才能使用----等待+唤醒。没有资源一直死等,生产资源出来后唤醒等待
条件变量:
1,定义条件变量:pthread_cond_t;
2,初始化条件变量:pthread_cond_init;
3,等待/唤醒: pthread_cond_wait/pthread_cond_signal
4,销毁条件变量: pthread_cond_destroy
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<pthread.h>
#include<errno.h>
pthread_cond_t boss;
pthread_cond_t customer;
pthread_mutex_t mutex;
int have_noodle=1;
void* thr_boss(void* arg){
while(1){
pthread_mutex_lock(&mutex);
//若面没有卖出去,则等待
while(have_noodle==1){
//int pthread_cond_timedwait(pthread_cond_t* cond,
//pthread_mutex_t* mutex,struct timespec* abstime);
//cond :条件变量
//mutex:互斥锁
//abstime:显示等待时长,
//int pthread_cond_wait(pthread_cond_t* cond,pthread_mutex_t* mutex);
//死等 原子操作不可被打断
//pthread_cond_wait 中集合了先解锁--唤醒---唤醒后加锁
//若有面先解锁然后一直等待被人唤醒
//若被唤醒则唤醒后加锁
pthread_cond_wait(&boss,&mutex);
}
printf("noodle has been complete\n");
have_noodle+=1;
//面做好了,唤醒顾客
pthread_cond_signal(&customer);
//解锁
pthread_mutex_unlock(&mutex);
}
return NULL;
}
void* thr_customer(void* arg){
while(1){
//第一件事情先加锁
pthread_mutex_lock(&mutex);
while(have_noodle==0){
pthread_cond_wait(&customer,&mutex);
}
//接下来吃面
printf("noodle is delicious\n");
have_noodle-=1;
//你吃完了就要唤醒老板再做一碗
pthread_cond_signal(&boss);
//解锁
pthread_mutex_unlock(&mutex);
}
return NULL;
}
int main(){
pthread_t tid1,tid2;
pthread_cond_init(&boss,NULL);
pthread_cond_init(&customer,NULL);
pthread_mutex_init(&mutex,NULL);
int i=0,ret;
for(i=0;i<2;++i){
ret=pthread_create(&tid1,NULL,thr_boss,NULL);
if(ret!=0){
printf("boss error\n");
return -1;
}
}
for(i=0;i<2;++i){
ret=pthread_create(&tid2,NULL,thr_customer,NULL);
if(ret!=0){
printf("customer error\n");
return -1;
}
}
pthread_join(tid1,NULL);
pthread_join(tid2,NULL);
pthread_cond_destroy(&boss);
pthread_cond_destroy(&customer);
pthread_mutex_destroy(&mutex);
return 0;
}
信号量:计数器+等待队列+等待+唤醒
功能:实现线程/进程间同步与互斥
计数器就是判断条件----当计数器只有0/1的时候就实现了互斥
等待队列+等待+唤醒实现同步的功能
system V信号量:信号量原语:p(-1+阻塞) / v(+1+唤醒)操作
prosix信号量:
定义:sem_t 信号量变量
初始化:sem_init
数据操作前资源计数判断:sem wait
计数>0则计数-1,直接返回,往下操作
计数<=0则阻塞等待
生产数据后+1 sem_post
销毁: sem_destroy
1 #include<iostream>
2 #include<pthread.h>
3 #include<semaphore.h>
4 #include<vector>
5 class RingQueue{
6 public:
7 //对信号量初始化
8 RingQueue(int cap=10):_queue(10),_capacity(cap),_write_step(0),_read_step(0){
9 //int sem_init(sem_t* sem,int pshared,unsigned int value);
10 //sem:信号量变量
11 //pshared:选项标志,决定信号量用于线程间还是进程间的同步与互斥
12 // 0:线程间
13 // !0:进程间
14 //value: 信号量初始计数
15 sem_init(&_sem_data,0,0);
16 sem_init(&_sem_idle,0,cap);
17 sem_init(&_sem_lock,0,1);
18 }
19 //销毁信号量
20 ~RingQueue(){
21 //int sem_destroy(sem_t* sem);
22 sem_destroy(&_sem_data);
23 sem_destroy(&_sem_idle);
24 sem_destroy(&_sem_lock);
25 }
26 //将用户传进来的数据放到环形队列当中
27 bool QueuePush(int data){
28 //首先再放入数据时需要判断是否满足条件
29 ProductorWait();
30 //队列操作进行上锁
31 QueueLock();
32 //将数据放入队列当中
33 _queue[_write_step]=data;
34 //因为是循环队列,所以
35 _write_step=(_write_step+1) % _capacity;
36 //在对队列进行解锁
37 QueueUnLock();
38 //然后唤醒消费者
39 ConsumerWakeUp();
40 return true;
41 }
42 bool QueuePop(int* data){
43 //消费者等待
44 ConsumerWait();
45 //上锁
46 QueueLock();
47 //读取数据
48 *data=_queue[_read_step];
49 //修改当前下标
50 _read_step=(_read_step+1) % _capacity;
51 QueueUnLock();
52 ProductorWakeUp();
53 return true;
54 }
55 private:
56 void QueueLock(){
57 //通过计数判断是否有资源可用,如果有计数-1+等待
58 //int sem_wait(sem_t* sem);
59 //若计数<=0则阻塞
60 //int sem_trywait(sem_t* sem);
61 //若
62 //计数<=则报错返回
63 //int sem_timewait(sem_t* sem,struct timespec* abs_timeout);
64 //若计数<=0则限时等待,超时则报错返回
65 //通过用信号量计数的方式实现互斥锁 原子操作
66 sem_wait(&_sem_lock);
67
68 }
69 void QueueUnLock(){
70 //计数+1,并唤醒等待
71 //int sem_post(sem_t* sem);
72 //计数+1,唤醒互斥锁信号
73 sem_post(&_sem_lock);
74
75 }
76 void ProductorWait(){
77 //当空闲数据<=0时,阻塞等待
78 sem_wait(&_sem_idle);
79 }
80 void ProductorWakeUp(){
81 //当空闲位置+1时唤醒生产者
82 sem_post(&_sem_idle);
83 }
84 void ConsumerWait(){
85 //当数据<=0时阻塞等待
86 sem_wait(&_sem_data);
87 }
88 void ConsumerWakeUp(){
89 //当有数据产生时唤醒消费者
90 sem_post(&_sem_data);
91 }
92 private:
93 std::vector<int> _queue;
94 int _capacity;
95 int _write_step;
96 int _read_step;
97 sem_t _sem_data;//数据资源计数
98 sem_t _sem_idle;//空闲空间计数
99 sem_t _sem_lock;//实现互斥
100
101 };
102 //生产者线程入口函数
103 void* thr_productor(void* arg){
104 int i=0;
105 RingQueue* q=(RingQueue*)arg;
106 while(1){
107 q->QueuePush(i);
108 std::cout<<"productor thread put data is "<<i<<std::endl;
109 i++;
110 }
111 return NULL;
112 }
113 //消费者入口函数
114 void* thr_consumer(void* arg){
115 RingQueue* q=(RingQueue*)arg;
116 while(1){
117 int data;
118 q->QueuePop(&data);
119 std::cout<<"consumer thread read data is "<<data<<std::endl;
120 }
121 return NULL;
122 }
123 int main(){
124 pthread_t pro[4],con[4];
125 RingQueue q;
126 int ret,i;
127 for(i=0;i<4;++i){
128 ret=pthread_create(&pro[i],NULL,thr_productor,(void*)&q);
129 if(ret!=0){
130 std::cout<<"thread is error"<<std::endl;
131 return -1;
132 }
133 }
134 for(i=0;i<4;++i){
135 ret=pthread_create(&con[i],NULL,thr_consumer,(void*)&q);
136 if(ret!=0){
137 std::cout<<"thread is error"<<std::endl;
138 return -1;
139 }
140 }
141 for(i=0;i<4;++i){
142 pthread_join(pro[i],NULL);
143 }
144
145 for(i=0;i<4;++i){
146 pthread_join(con[i],NULL);
147 }
148 return 0;
149 }
线程安全的单例模式
饿汉方式
template <typename T>
class Singleton {
static T data;
public:
static T* GetInstance() {
return &data;
}
};
懒汉方式
非安全版本
template <typename T>
class Singleton {
static T* inst;
public:
static T* GetInstance() {
if (inst == NULL) {
inst = new T();
}
return inst;
}
};
安全版本
/ 懒汉模式, 线程安全
template <typename T>
class Singleton {
volatile static T* inst; // 需要设置 volatile 关键字, 否则可能被编译器优化.
static std::mutex lock;
public:
static T* GetInstance() {
if (inst == NULL) { // 双重判定空指针, 降低锁冲突的概率, 提高性能.
lock.lock(); // 使用互斥锁, 保证多线程情况下也只调用一次 new.
if (inst == NULL) {
inst = new T();
}
lock.unlock();
}
return inst;
}
};
STL中的容器是否是线程安全的?
不是.
原因是, STL 的设计初衷是将性能挖掘到极致, 而一旦涉及到加锁保证线程安全, 会对性能造成巨大的影响.
而且对于不同的容器, 加锁方式的不同, 性能可能也不同(例如hash表的锁表和锁桶).
因此 STL 默认不是线程安全. 如果需要在多线程环境下使用, 往往需要调用者自行保证线程安全.
其他常见的锁
- 悲观锁:在每次取数据时,总是担心数据会被其他线程修改,所以会在取数据前先加锁(读锁,写锁,行锁等),当其他线程想要访问数据时,被阻塞挂起。
- 乐观锁:每次取数据时候,总是乐观的认为数据不会被其他线程修改,因此不上锁。但是在更新数据前,会判断其他数据在更新前有没有对数据进行修改。主要采用两种方式:版本号机制和CAS操作。
- CAS操作:当需要更新数据时,判断当前内存值和之前取得的值是否相等。如果相等则用新值更新。若不等则失败,失败则重试,一般是一个自旋的过程,即不断重试