一、信号量
1.回忆下互斥锁+条件变量实现生产者与消费者模型的代码
我们在判断资源是否可用的时候是程序员使用while循环来进行自己判断的,那么我们不想每次在访问资源的时候手动进行判断,那么我们就需要用到信号量了。
2.信号量的原理
资源计数器+PCB等待队列
资源计数器:
执行流获取信号量,获取成功,信号量计数器减1操作,获取失败,执行流放入到PCB等待队列中,执行流释放信号量成功之后,计数器加1操作。
生产者与消费者原理图:
3.信号量的接口
初始化接口:
int sem_init(sem_t *sem, int pshared, unsigned int value);
sem:信号量,sem_t是信号量的类型
pshared:该信号量是用于进程间还是线程间,0用于线程间,全局变量;非0,用于进程间,将信号量所用到的资源在共享内从中开辟。
value:资源的个数,初始化信号量计数器的。
等待接口:
int sem_wait(sem_t *sem);函数有可能会阻塞的
1.对资源计数器进行减1操作
2.判断资源计数器的值是否小于0,是,则阻塞等待,将执行流放到PCB等待队列当中;不是,则接口返回
释放接口(唤醒接口):
int sem_post(sem_t *sem);
1.会对资源计数器的值进行加1操作
2.判断资源计数器的是否小于等于0,是,通知PCB等待队列;否,不用通知PCB等待队列,因为没有线程在等待。
注:信号量中的程序计数器负数的绝对值就表示PCB等待队列中等待的线程数
销毁接口:
int sem_destroy(sem_t *sem);
问:先拿信号量还是先加锁?
答:先获取信号量,再保证互斥(互斥锁,信号量)!!
原因:如果先拿锁,再判断信号量的话,如果信号量小于0,则该线程会阻塞等待,而且唯一的锁也被它拿走了,则其它的线程(生产者,消费者)都没法拿锁,就会一直等待。
信号量的代码:基于环形队列的生产消费模型
1 #include <stdio.h>
2 #include <unistd.h>
3 #include <vector>
4 #include <pthread.h>
5 #include <semaphore.h>
6
7 using namespace std;
8 /*
9 * 定义线程安全的队列
10 * 环形队列(用数组模拟)
11 * 线程安全:
12 * 同步:信号量
13 * 互斥: 信号量
14 * */
15
16 #define CAPACITY 1
17
18 class Rinqueue{
19 public:
20 Rinqueue():vec_(CAPACITY){
21 capacity_ = CAPACITY;
22 sem_init(&sem_lock_, 0, 1);
23
24 sem_init(&sem_cons_, 0, 0);
25 sem_init(&sem_prod_, 0, CAPACITY);
26
27 pos_write_ = 0;
28 pos_read_ = 0;
29
30 }
31
32 ~Rinqueue(){
33 sem_destroy(&sem_lock_);
34 sem_destroy(&sem_cons_);
35 sem_destroy(&sem_prod_);
36 }
37
38 void Push(int data){
39 sem_wait(&sem_prod_);
40
41 sem_wait(&sem_lock_);
W> 42 printf("i am product %p, i product %d\n", pthread_self(), data);
43 vec_[pos_write_] = data;
44 pos_write_ = (pos_write_ + 1) % capacity_;
45 sem_post(&sem_lock_);
46
47 sem_post(&sem_cons_);
48 }
49
50 void Pop(){
51 sem_wait(&sem_cons_);
52
53 sem_wait(&sem_lock_);
54 int data = vec_[pos_read_];
55 pos_read_ = (pos_read_ + 1) % capacity_;
W> 56 printf("i am thread %p, i consume %d\n", pthread_self(), data);
57 sem_post(&sem_lock_);
58
59 sem_post(&sem_prod_);
60 }
61 private:
62 vector<int> vec_;
63 //数组的容量大小
64 size_t capacity_;
65
66 //保证互斥的信号量
67 sem_t sem_lock_;
68
69 //消费者的信号量
70 sem_t sem_cons_;
71 //生产者的信号量
72 sem_t sem_prod_;
73
74
75 int pos_write_;
76 int pos_read_;
77 };
78
79 /*
80 * 创建两种角色的线程
81 * 1.生产者
82 * 2.消费者
83 * */
84
85 #define THREADCOUNT 1
86
87 void* cons_strat(void* arg){
88 Rinqueue* rq = (Rinqueue*)arg;
89 while(1){
90 rq->Pop();
91 sleep(2);
92 }
93 }
94
95 int g_data = 0;
96
97 void* prod_strat(void* arg){
98 Rinqueue* rq = (Rinqueue*)arg;
99 while(1){
100 rq->Push(g_data++);
101 sleep(1);
102 }
103 }
104
105 int main(){
106 Rinqueue* rq = new Rinqueue();
107
108 pthread_t cons[THREADCOUNT], prod[THREADCOUNT];
109 for(int i = 0; i < THREADCOUNT; i++){
110 int ret = pthread_create(&cons[i], NULL, cons_strat, (void*)rq);
111 if(ret < 0){
112 perror("pthread_create");
113 return 0;
114 }
115
116 ret = pthread_create(&prod[i], NULL, prod_strat, (void*)rq);
117 if(ret < 0){
118 perror("pthread_create");
119 return 0;
120 }
121 }
122
123 for(int i = 0; i < THREADCOUNT; i++){
124 pthread_join(cons[i], NULL);
125 pthread_join(prod[i], NULL);
126 }
127 return 0;
128 }
129
二、线程池
1.概念和应用场景
一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利用,还能防止过分调度。可用线程数量应该取决于可用的并发处理器、处理器内核、内存、网络sockets等的数量。* 线程池的应用场景:* 1. 需要大量的线程来完成任务,且完成任务的时间比较短。 WEB 服务器完成网页请求这样的任务,使用线程池技术是非常合适的。因为单个任务小,而任务数量巨大,你可以想象一个热门网站的点击次数。 但对于长时间的任务,比如一个Telnet 连接请求,线程池的优点就不明显了。因为 Telnet 会话时间比线程的创建时间大多了。* 2. 对性能要求苛刻的应用,比如要求服务器迅速响应客户请求。* 3. 接受突发性的大量请求,但不至于使服务器因此产生大量线程的应用。突发性大量客户请求,在没有线程池情况下,将产生大量线程,虽然理论上大部分操作系统线程数目最大值不是问题,短时间内产生大量线程可能使内存到达极限,出现错误 .
2.线程池的原理:
线程安全的队列+一堆的线程
- 线程安全:互斥+同步
- 队列:先进先出
- 元素:待处理的数据+处理数据的方法
2.3原理图:
3.线程池要完成的事
- 创建固定数量线程池,循环从任务队列中获取任务对象
- 获取到任务对象之后,执行任务对象中的任务接口
4.线程池的代码
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <queue>
using namespace std;
typedef void (*Handler)(int data);
class QueueData{
public:
QueueData(){
}
QueueData(int data, Handler handler){
data_ = data;
handler_ = handler;
}
void run(){
handler_(data_);
}
private:
int data_;
Handler handler_;
};
class ThreadPool{
public:
ThreadPool(int capa, int thread_count){
capacity_ = capa;
pthread_mutex_init(&lock_, NULL);
pthread_cond_init(&cons_cond_, NULL);
thread_count_ = thread_count;
pthread_cond_init(&prod_cond_, NULL);
flag_exit_ = 0;
}
~ThreadPool(){
pthread_mutex_destroy(&lock_);
pthread_cond_destroy(&cons_cond_);
pthread_cond_destroy(&prod_cond_);
}
int OnInit(){
int cnt = 0;
for(int i = 0; i < thread_count_; i++){
pthread_t tid;
int ret = pthread_create(&tid, NULL, ThreadPollStart, (void*)this);
if(ret < 0){
cnt++;
}
}
return thread_count_ -= cnt;
}
void Push(QueueData qd){
pthread_mutex_lock(&lock_);
while(que_.size() >= capacity_){
if(flag_exit_){
pthread_mutex_unlock(&lock_);
return;
}
pthread_cond_wait(&prod_cond_, &lock_);
}
que_.push(qd);
pthread_mutex_unlock(&lock_);
pthread_cond_signal(&cons_cond_);
}
void Pop(QueueData* qd){
*qd = que_.front();
que_.pop();
}
static void* ThreadPollStart(void* arg){
pthread_detach(pthread_self());
ThreadPool* tp = (ThreadPool*)arg;
while(1){
//pos1 - no
pthread_mutex_lock(&tp->lock_);
while(tp->que_.empty()){
//pos2 - yes
if(tp->flag_exit_){
tp->thread_count_--;
pthread_mutex_unlock(&tp->lock_);
pthread_exit(NULL);
}
pthread_cond_wait(&tp->cons_cond_, &tp->lock_);
}
QueueData qd;
tp->Pop(&qd);
pthread_mutex_unlock(&tp->lock_);
pthread_cond_signal(&tp->prod_cond_);
qd.run();
}
return NULL;
}
void ThreadPoolExit(){
flag_exit_ = 1;
while(thread_count_ > 0){
pthread_cond_signal(&cons_cond_);
}
}
private:
queue<QueueData> que_;
size_t capacity_;
pthread_mutex_t lock_;
pthread_cond_t cons_cond_;
pthread_cond_t prod_cond_;
int thread_count_;
int flag_exit_;
};
void DealData(int data){
printf("data = %d\n", data);
}
int main(){
ThreadPool* tp = new ThreadPool(10, 5);
if(tp == NULL){
printf("create threadpool failed\n");
return 0;
}
if(tp->OnInit() <= 0){
printf("create thread failed\n");
return 0;
}
for(int i = 0; i < 10000; i++){
QueueData qd(i, DealData);
tp->Push(qd);
}
tp->ThreadPoolExit();
delete tp;
return 0;
}
三、读写锁
1.作用:
大量读,少量写的情况,允许多个线程并行读,多个线程互斥写
2.读写锁的三种状态:
- 以读模式加锁的状态
- 以写模式加锁的状态(互斥锁)
- 不加锁的状态
3.读写锁的接口
初始化接口
int pthread_rwlock_init(pthread_rwlock_t *rwlock,const pthread_rwlockattr_t *attr);
pthread_rwlock_t:读写锁的类型
rwlock:传递读写锁
attr:NULL,默认属性
销毁接口
pthread_rwlock_destroy(pthread_rwlock_t*rwlock);
加锁
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);阻塞
以读模式加锁,允许多个线程并行以读模式获取读写锁
引用计数:用来记录当前读写锁有多少个线程以读模式获取了读写锁
1.每当有现成以读模式进行加锁,引用计数器++;
2.每当读模式的线程释放读写锁,引用计数器--;
int pthread_rwlock_tryrdlock(pthread_rwlock_t*rwlock);非阻塞接口
int pthread_rwlock_wrlock(pthread_rwlock_t*rwlock);阻塞接口(相当于互斥锁)
int pthread_rwlock_unlock(pthread_rwlock_t*rwlock);解锁接口
注意:假设有四个线程,A线程读模式,B线程读模式,C线程写模式,D线程读模式,线程按A->B->C->D的顺序去拿锁,那么
A,B可以立刻拿到锁,D必须等C获取并释放锁之后才能拿到锁,不可以直接去拿锁(尽管读模式可以多个线程拿锁)。
四、乐观锁&悲观锁
悲观锁:针对某个线程访问临界区修改数据的时候,都会认为可能有其它线程并行修改的情况发生,所以在线程修改数据之前就进行加锁,让多个线程互斥访问。悲观锁有:互斥锁,读写锁,自旋锁等等
乐观锁:针对某个线程访问临界区资源的时候,乐观的认为只有该线程在修改,大概率不会存在并行的情况。所以修改数据不加锁,但是,在修改完毕,进行更新的时候,进行判断。
例如:版本号控制,CAS(compare and swap)无锁编程。
五、自旋锁
自旋锁(busy-waiting类型)和互斥锁(sleep-waiting类型)的区别:
- 1.自旋锁加锁时,加不到锁,线程不会切换(时间片没有到的情况下,时间片到了,也会线程切换),会持续的尝试拿锁,直到拿到自旋锁。(忙等类型的锁)
- 2.互斥锁加锁时,加不到锁,线程会切换(时间片没有到也会切换),进入睡眠状态,当其他线程释放互斥锁(解锁)之后,被唤醒。再切回来,进行抢锁。(睡等类型的锁)
- 3.自旋锁的特点:因为自旋锁不会引起调用者睡眠,所以自旋锁的效率远高于互斥锁。
- 4.自旋锁的缺点:自旋锁一直占用着CPU,它在未获得锁的情况下,一直运行(自旋),所以占用着CPU,如果不能在很短的时间内获得锁,这无疑会使CPU效率降低
- 5.使用于临界区代码较短时(直白的说:临界区代码执行短)的情况,使用自旋锁效率比较高,因为线程不用来回切换。
- 6.当临界区当中执行时间较长,自旋锁就不适用了,因为拿不到锁就会占用CPU一直抢占锁。
自旋锁的接口:
int pthread_spin_init(pthread_spinlock_t*lock,int pshared);
int pthread_spin_destroy(pthread_spinlock_t *lock);
int pthread_spin_lock(pthread_spinlock_t *lock);
int pthread_spin_trylock(pthread_spinlock_t*lock);
int pthread_spin_unlock(pthread_spinlock_t *lock);
六、设计模式
1.什么是设计模式?
设计模式是软件开发人员在软件开发过程中面临的一般问题的解决方案,这些解决方案是众多软件开发人员经过相当长一段时间的实验和错误总结出来的。
2.设计模式的分类
创建型模型:这些设计模式提供了一种在创建对象的同时隐藏创建逻辑的方式,而不是使用new运算符直接实例化对象,这使得程序在判断针对某个给定实例需要创建哪些对象时更加灵活。
例如:工程模式,单例模式
结构型模式:这些设计模式关注类和对象的组合。继承的概念被用来组合接口和定义组合对象获得新功能方式。
例如:适配器模式,桥接模式
行为模式:这些设计模式特别关注对象之间的通信
例如:命令模式,观察者模式
3.单例模式
- 单例模式只能有一个实例。(在整个软件当中就只有一个实例对象)
- 单例类必须自己创建自己的唯一实例
- 单例类必须给所有其它对象提供这一实例
意图:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
主要解决:一个全局使用的类频繁的创建与销毁。
何时使用:当你想控制实例数目,节省系统资源的时候。
如何解决:判断系统是否以及有这个单例,如果有则返回,如果没有则创建。
关键代码:构造函数是私有的。
4.单例模式的两种形式(懒汉&饿汉)
饿汉模式:在程序启动的时候就创建唯一的一个实例对象,饿汉模式不需要加锁
懒汉模式:当你第一次使用时才创建唯一的一个实例对象,从而实现延迟加载的效果。懒汉模式在第一次使用单例对象时才完成初始化工作,因此可能存在多线程竞态的环境,如果不加锁会导致重复构造或者构造不完全问题。
实例代码:
注意线程安全:双层if判断,加锁的位置
1 #include<iostream>
2 #include<pthread.h>
3 using namespace std;
4
5 /*
6 *懒汉模式的单例类
7 *
8 * */
9
10 class Sigleton{
11 private:
12 Sigleton(){};
13 static Sigleton* st;
14 static pthread_mutex_t lock_;
15 public:
16 static Sigleton* GetInstance();
17 };
18
19 pthread_mutex_t Sigleton::lock_;
20 Sigleton* Sigleton::st = NULL;
21 Sigleton* Sigleton::GetInstance(){
22 if(st == NULL){
23 pthread_mutex_lock(&Sigleton::lock_);
24 if(st == NULL){
25 st = new Sigleton;
26 }
27 pthread_mutex_unlock(&Sigleton::lock_);
28 }
29 return st;
30 }
31
32 int main(){
33 Sigleton* st = Sigleton::GetInstance();
34 Sigleton* st1 = Sigleton::GetInstance();
35 if(st == st1){
36 cout << "st == st1" << endl;
37 }
38 return 0;
39 }
40 /*
41 *饿汉模式的单例类
42 *
43 * */
44 #if 0
45 class Sigleton
46 {
47 private:
48 Sigleton()
49 {}
50 static Sigleton*st;
51
52 public:
53 static Sigleton*GetInstance();
54 void Print()
55 {
56 cout<<"hhh"<<endl;
57 }
58 };
59
60 Sigleton* Sigleton::st = new Sigleton();
61 Sigleton* Sigleton::GetInstance()
62 {
63 return st;
64 }
65
66 int main()
67 {
68 Sigleton*st = Sigleton::GetInstance();
69 Sigleton*st1 = Sigleton::GetInstance();
70
71 if(st==st1)
72 {
73 cout<<"st==st1"<<endl;
74 }
75
76 st->Print();
77 return 0;
78 }
79 #endif