多线程(下)

一、信号量

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

多线程下注入 Spring Bean,我们需要注意一些问题。首先,Spring Bean 的依赖注入是线程安全的,因为 Spring 容器会保证 Bean 的实例唯一,并且在多线程环境中不会出现竞态条件。然而,如果我们在多线程环境下手动创建 Bean 实例并注入,就需要注意线程安全性。 为了在多线程下注入 Spring Bean,我们需要确保 Bean 的作用域是线程安全的。通常情况下,我们可以将 Bean 的作用域设置为 prototype,使得每个线程都拥有自己的 Bean 实例,避免线程间的竞态条件。 另外,我们需要注意在多线程环境下对 Bean 的操作是否会造成线程安全问题。比如在单例 Bean 中使用了非线程安全的对象或方法,就可能会导致线程安全问题。在这种情况下,我们需要使用同步机制来保证线程安全,或者考虑将 Bean 的作用域设置为 prototype。 在注入 Bean 的时候,我们还需要考虑是否需要进行依赖注入或者手动创建 Bean 实例。如果需要在多线程下注入 Bean,最好使用 Spring 容器进行依赖注入,这样可以保证线程安全性并且简化代码逻辑。 总的来说,在多线程下注入 Spring Bean,我们需要确保 Bean 的作用域是线程安全的,并且在操作 Bean 的过程中注意线程安全性,避免出现竞态条件。同时尽量使用 Spring 容器进行依赖注入,避免手动创建 Bean 实例造成线程安全问题。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值