Linux操作系统学习(线程同步)

news2024/11/15 15:52:53

文章目录

  • 线程同步
    • 条件变量
    • 生产者与消费者模型
    • 信号量
    • 环形队列应用生产者消费者模型

线程同步

​ 现实生活中我们经常会遇到同一个资源多个人都想使用的问题,例如游乐园过山车排队,玩完的游客还想再玩,最好的办法就是玩完的游客想再玩就去重新排队

​ 线程同步其实就是一种等待机制,多个想要同时访问同一个对象的线程形成一个类似等待队列,等待前面的线程使用完毕后,下一个线程再使用。

线程同步的概念:

​ 在保证数据安全的前提下(加锁保护),让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题,叫做同步

为什么要线程同步:

​ 互斥是保证线程的安全。但当一个线程访问了临界资源后,释放了它的锁,同时立刻参与到了锁的竞争中,如果它又拿到了锁。那么其他线程就会由于长时间得不到锁访问不了临界资源而造成线程饥饿问题。

​ 同步能让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题,使多线程协同高效完成某些事情

同步就是在保证数据安全的前提下,让线程按照某种特定的顺序来访问临界资源。

同步与互斥的关系:互斥是保证数据的安全,同步在互斥的前提下,来提高线程之间的效率。

程序没有安全性的问题,就没有必要使用同步

条件变量

条件变量是类型为pthread_cond_t的变量,是利用线程间共享的全局变量进行同步的一种机制,主要有两个动作:

  • 线程对某个临界资源进行条件判断,为真则执行代码,为假则挂起等待,节省CPU资源避免空等(也可以反着来,主要是挂起等待节省资源)

  • 其他线程在执行某些动作后使条件成立,唤醒等待的线程

它可以用来保证:在某个线程没有满足某种条件完成之前,其他线程只能挂起等待。

条件变量一般用到4个接口:

  • int pthread_cond_init(pthread_cond_t *cv,const pthread_condattr_t *cattr);

    功能:初始化条件变量

    cv:要初始化的条件变量

    cattr:设置条件变量属性,一般置NULL交给OS默认设置即可

    返回值:成功返回0,失败返回错误码

  • int pthread_cond_destroy(pthread_cond_t *cond)

    功能:释放申请的条件变量

    返回值:成功返回0,失败返回错误码

    注意:条件变量所占的空间没有被销毁(静态区)

  • int pthread_cond_wait(pthread_cond_t *cv,pthread_mutex_t *mutex);

    功能:将调用此函数的线程在指定条件变量中挂起等待

    cv:要在哪个条件变量下挂起等待

    mutex:线程调用此函数时,会自动释放传入的锁

    返回值:成功返回0,失败返回错误码

  • int pthread_cond_signal(pthread_cond_t *cv);

    功能:唤醒等待中的线程

    cv:唤醒在哪个条件变量下等待的线程

    返回值:成功返回0,失败返回错误码

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1PsOO5fC-1677869705608)(G:\Typora\图片保存\image-20221227012439771.png)]

    broadcast是一次唤醒指定条件变量下多个线程

下面用一段代码验证条件变量的用法

代码逻辑:一共申请6个线程,其中一个线程负责发布命令,另外5个线程负责工作

#include <iostream>
#include <string>
#include <pthread.h>
#include <unistd.h>

pthread_mutex_t mtx;
pthread_cond_t cond;

//发布命令线程
void* master(void* args)
{
    std::string name = (char*)args;
    while(true)
    {
        //唤醒在条件变量下等待的一个线程
        std::cout << "begin run:" << std::endl;
        pthread_cond_signal(&cond);
        sleep(1);
    }
}
//工作线程[5]
void* threadrun(void* args)
{
    int num = *(int*)args;
    delete (int*)args;
    while(true)
    {
        pthread_cond_wait(&cond,&mtx);
        std::cout << "thread[" << num << "]running. . . " << std::endl;
    }
}
//每个线程再唤醒执行后经过while循环再次挂起等待

int main()
{
    //初始化条件变量
    pthread_mutex_init(&mtx,nullptr);
    pthread_cond_init(&cond,nullptr);

    pthread_t tid[5];
    pthread_t boss;
    
    pthread_create(&boss,nullptr,master,(void*)"boss");
    for(size_t i = 0;i < 5;i++)
    {
        int* num = new int(i);//用堆区变量去传递线程号
        pthread_create(tid+i,nullptr,threadrun,(void*)num);
    }

	//最后记得要等待线程以及释放锁和条件变量
    for(size_t i = 0;i < 5;i++)
        pthread_join(tid[i],nullptr);
    pthread_join(boss,nullptr);


    pthread_mutex_destroy(&mtx);
    pthread_cond_destroy(&cond);
    return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RjRtRlKO-1677869705609)(G:\Typora\图片保存\image-20221227014403285.png)]

通过输出结果有如下分析:

  1. 条件变量内部一定有一个等待队列,哪个线程调用wait,哪个线程就挂起等待并进入等待队列,唤醒顺序就是等待的顺序

    (线程执行的顺序是不一定的)

  2. wait函数一定是释放锁的,否则线程调用该函数是抱着锁挂起等待的,其他线程就无法访问临界区了

  3. signal函数唤醒的线程也一定会去争锁,争到才会继续执行;否则可能会出现一个带锁的线程访问临界区,和一个刚唤醒的线程继续访问临界区等方面的错误

  4. 在mutex已上锁的时候才能调用wait()

条件变量通常和互斥锁一起使用,互斥是保证线程的安全,条件变量防止互斥造成的饥饿问题


生产者与消费者模型

​ 条件变量使用“通知—唤醒”模型,例如网购商家会发快递,我们只需要等待快递到了给我们发送提示短信;运用在多线程中最经典的就是生产者—消费者模型

​ 生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题。

​ 生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理, 直接扔给阻塞队列, 消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。这个阻塞队列就是用来给生产者和消费者解耦的。

​ 与普通的队列区别在于,当队列为空时,从队列获取元素的操作将会被阻塞,直到队列中被放入了元素;当队列满时,往队列里存放元素的操作也会被阻塞,直到有元素被从队列中取出

​ 上面的说法放到现实中解释就是,消费者—超市—供货商这样的模型,消费者购买商品不需要找供货商,只需要去超市即可,而供货商也不用管消费者,只需要给超市提供商品。

​ 消费者购买商品的同时,供货商也在生产商品并向超市提供,若供货商出现问题,超市会有存品可以暂时共给消费者,并寻找新的供货商,此时超市就解决了消费者和供货商之间的耦合问题

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-q1SfW0ln-1677869705610)(G:\Typora\图片保存\image-20221226022303881.png)]

下面用代码验证一下模型

代码逻辑:

  • 设计一个类,对普通队列、条件变量、互斥量进行封装
  • Push对应生产者,利用条件变量对其限制,队列为满就挂起等待,待消费者消费数据后唤醒
  • Pop对应消费者,利用条件变量对其限制,队列为空就挂起等待,待生产者生产数据后唤醒
/************************BlockQueue.hpp************************/

#pragma once
#include <iostream>
#include <queue>
#include <unistd.h>
#include <pthread.h>
#include <time.h>
#include <cstdlib>

namespace dd
{

template<class T>
class BlockQueue
{
public:
    //初始化
    BlockQueue()
        :_capacity(10)
    {
        pthread_cond_init(&_empty,nullptr);
        pthread_cond_init(&_full,nullptr);
        pthread_mutex_init(&_mtx,nullptr);
    }
	//释放互斥量、条件变量
    ~BlockQueue()
    {
        pthread_cond_destroy(&_empty);
        pthread_cond_destroy(&_full);
        pthread_mutex_destroy(&_mtx);
    }
	//生产逻辑
    void Push(const T& key)
    {
        Lockqueue();

        while(full())
            ProducterWait();
        
        _bq.push(key);

        UnlockQueue();
        WakeupConsumer();

    }
    
	//消费逻辑
    void Pop(T* key)
    {
        Lockqueue();
        
        while(empty())
            ConsumerWit();
        
        *key = _bq.front();
        _bq.pop();

        UnlockQueue();
        WakeupProducter();
    }

	//队列为空则为真
    bool empty()
    {
        return _bq.empty();
    } 
    //队列为满则为真,这里最大容量设置的是10
    bool full()
    {
        return _bq.size() == _capacity-1;           //先判断 后push,所以要-1
    }
    
	//加锁
    void Lockqueue()
    {
        pthread_mutex_lock(&_mtx);
    }
    //解锁
    void UnlockQueue()
    {
        pthread_mutex_unlock(&_mtx);
    }
	//队列满时生产者挂起等待
    void ProducterWait()
    {
        pthread_cond_wait(&_full,&_mtx);
    }
    //生产者唤醒等待
    void WakeupProducter()
    {
        pthread_cond_signal(&_full);
    }
	//队列为空时消费者挂起等待
    void ConsumerWit()
    {
        pthread_cond_wait(&_empty,&_mtx);
    }
    //消费者唤醒等待
    void WakeupConsumer()
    {
        pthread_cond_signal(&_empty);
    }


private:
    std::queue<T> _bq;
    int _capacity;	//最大容量本示例设置的是10
    pthread_mutex_t _mtx;
    pthread_cond_t _empty;
    pthread_cond_t _full;
}; 

}


/************************************************************************************************/
#include "BlockQueue.hpp"
using namespace dd;

void* consumer(void* args)
{
    BlockQueue<int>* bq = (BlockQueue<int>*)args;
    while(true)
    {
        sleep(1);
        int data = 0;
        bq->Pop(&data);
        std::cout << "消费了一个数据:"<< data << std::endl;
    }
}

void* producter(void* args)
{
    BlockQueue<int>* bq = (BlockQueue<int>*)args;
    while(true)
    {
        int data = rand()%20+1;
        std::cout << "生产了一个数据:" << data << std::endl;
        bq->Push(data);
    }
}

int main()
{
    srand((long long)time(nullptr));
    BlockQueue<int>* bq = new BlockQueue<int>();

    pthread_t c,p;
    pthread_create(&c,nullptr,consumer,(void*)bq); 
    pthread_create(&p,nullptr,producter,(void*)bq); 

    pthread_join(c,nullptr);
    pthread_join(p,nullptr);
    
    return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-elm7NfMw-1677869705610)(G:\Typora\图片保存\image-20221228183625402.png)]

上述示例需要注意:

  • 虚假唤醒问题

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BrnfHmKV-1677869705610)(G:\Typora\图片保存\image-20221228184108194.png)]

    在wait时,必须把它放到while里,而不是if里,为了防止虚假唤醒

    ​ 例如上述示例中再增加一个线程,1号线程是生产者,2、3号线程是消费者,当2号线程获取走最后一个数据后,3号线程也想获取,但发现生产队列为NULL于是就挂起等待,随后1号线程生产了一个数据便唤醒3号,但是2号又先把数据获取走了,3号就属于虚假唤醒

    ​ 3号线程唤醒后获取竞争锁,竞争到了以后继续执行Pop,但是队列已经为NULL了,所以利用while再次判断可预防这种现象,这种现象常见于多核CPU多线程中。

上述示例代码,也可以将发送数据改为发送任务:

  • 把发送数据改为发送加减乘除的任务
  • 再申请一个类,负责确定任务完成任务的逻辑功能部分,同时也是阻塞队列的数据类型
  • 生产者只负责确定要算的数和算法
  • 消费者负责完成生产者发布的任务

综上,变更的部分有:新增的类、最后cpp执行部分

/************************BlockQueue.hpp************************/
#pragma once
#include <iostream>
#include <queue>
#include <unistd.h>
#include <pthread.h>
#include <time.h>
#include <cstdlib>

namespace dd
{

template<class T>
class BlockQueue
{
public:
    BlockQueue()
        :_capacity(10)
    {
        pthread_cond_init(&_empty,nullptr);
        pthread_cond_init(&_full,nullptr);
        pthread_mutex_init(&_mtx,nullptr);
    }

    ~BlockQueue()
    {
        pthread_cond_destroy(&_empty);
        pthread_cond_destroy(&_full);
        pthread_mutex_destroy(&_mtx);
    }

    void Push(const T& key)
    {
        Lockqueue();

        while(full())
            ProducterWait();
        
        _bq.push(key);

        UnlockQueue();
        WakeupConsumer();

    }

    void Pop(T* key)
    {
        Lockqueue();
        
        while(empty())
            ConsumerWit();
        
        *key = _bq.front();
        _bq.pop();

        UnlockQueue();
        WakeupProducter();
    }


    bool empty()
    {
        return _bq.empty();
    } 
    bool full()
    {
        return _bq.size() == _capacity-1;           //先判断 后push,所以要-1
    }

    void Lockqueue()
    {
        pthread_mutex_lock(&_mtx);
    }
    void UnlockQueue()
    {
        pthread_mutex_unlock(&_mtx);
    }

    void ProducterWait()
    {
        pthread_cond_wait(&_full,&_mtx);
    }
    void WakeupProducter()
    {
        pthread_cond_signal(&_full);
    }

    void ConsumerWit()
    {
        pthread_cond_wait(&_empty,&_mtx);
    }
    void WakeupConsumer()
    {
        pthread_cond_signal(&_empty);
    }


private:
    std::queue<T> _bq;
    int _capacity;
    pthread_mutex_t _mtx;
    pthread_cond_t _empty;
    pthread_cond_t _full;
}; 

}


/************************Task.hpp************************/
#pragma once
#include <iostream>
#include <pthread.h>
namespace dd
{

class Task
{
public:
    Task()
    {}
    Task(int x,int y,char op)
        :_x(x)
        ,_y(y)
        ,_op(op)
    {}

    int Run()
    {
        int ret = 0;
        switch(_op)
        {
            case '+':
                ret = _x + _y;
                break;
            case '-':
                ret = _x - _y;
                break;
            case '*':
                ret = _x * _y;
                break;
            case '/':
                ret = _x / _y;
                break;
            case '%':
                ret = _x % _y;
                break;
            default:
                break;
        }
        //std::cout << _x << _op << _y << " = " << ret << std::endl;
        std::cout << pthread_self() << ": " << _x << _op << _y << " = " << ret << std::endl;
    }
private:
    int _x;
    int _y;
    char _op;

};


}

/************************************************************************************************/
#include "BlockQueue.hpp"
#include "Task.hpp"
using namespace dd;

void* consumer(void* args)
{
    BlockQueue<Task>* bq = (BlockQueue<Task>*)args;
    while(true)
    {
        Task t;
        bq->Pop(&t);
        t.Run();
    }
}

void* producter(void* args)
{
    BlockQueue<Task>* bq = (BlockQueue<Task>*)args;
    std::string ops = "+-*/%";
    while(true)
    {
        int x = rand()%20+1;
        int y = rand()%20+1;
        char op = ops[rand()%5];
        Task t(x,y,op);
        bq->Push(t);
        sleep(1);
    }
}

int main()
{
    srand((long long)time(nullptr));
    BlockQueue<Task>* bq = new BlockQueue<Task>();

    pthread_t c1,c2,c3,p;
    pthread_create(&c1,nullptr,consumer,(void*)bq); 
    pthread_create(&p,nullptr,producter,(void*)bq); 
    pthread_create(&c2,nullptr,consumer,(void*)bq); 
    pthread_create(&c3,nullptr,consumer,(void*)bq); 

    pthread_join(c1,nullptr);
    pthread_join(c2,nullptr);
    pthread_join(c3,nullptr);
    pthread_join(p,nullptr);
    
    return 0;
}

​ 直接使用互斥量,除了生产者、消费者之间要竞争互斥量以外,消费者之间也需要竞争互斥量,但如果汇聚(链表)中没有数据,消费者之间竞争互斥锁是无意义的。有了条件变量机制以后,只有生产者完成生产,才会引起消费者之间的竞争,提高了程序效率。


信号量

信号量的本质是一把计数器,用来描述临界资源中资源数目的大小,达到无冲突的访问共享资源目的

(例如飞机售票,把票看成信号量,买票就是申请信号量,卖票就是释放信号量)

伪代码如下:

临界资源分成5个部分(count=5),count就被称作信号量

​ count–,一个执行流占有临界资源一部分的操作叫做P操作

​ count++,一个执行流结束使用临界资源的一部分叫做V操作

​ count == 0,表示没有资源可以分配,此时的线程或进程就会被挂起等待(内部数据结构会有类似等待队列的结构)

但信号量也属于临界资源,所以V、P操作都是原子性的

  • 信号变量的函数接口
#include <semaphore.h>
 
int sem_init(sem_t *sem, int pshared, unsigned int value);
  • 作用:初始化信号量

  • sem:要初始化的信号量,同互斥量、条件变量一样,要创建sem_t类型的 变量
    pshared:0表示线程间共享,大于0表示进程间共享

    value:信号量初始值,信号量个数

  • 返回值:成功返回0,失败返回-1,并设置errno来表示错误

 #include <semaphore.h>
  
 int sem_destroy(sem_t *sem);
  • 作用:销毁定义的信号量
  • sem:要销毁的信号量
  • 返回值:成功返回0,失败返回-1,并设置errno来表示错误
#include <semaphore.h>
 
int sem_wait(sem_t *sem);
  • 作用:等待信号量,将信号量的值减1,如果信号量为0,阻塞等待。V( )操作
  • sem:要等待的信号量
  • 返回值:成功返回0,失败返回-1,并设置errno来表示错误
#include <semaphore.h>

int sem_post(sem_t *sem);
  • 作用:表示资源使用完毕,将信号量做加1操作。P( )操作
  • sem:要发布的信号量
  • 返回值:成功返回0,失败返回-1,并设置errno来表示错误

环形队列应用生产者消费者模型

基本原理:

  • 生产者和消费者在一开始时是指向同一位置,代表队列为空,应该让消费者等待,生产者工作
  • 生产者和消费者当之后所在同一位置,代表队列为满,应该让消费者工作,生产者等待
  • 其余时候,生产者和消费者 一定不 指向同一位置

注意事项:

  • 在不是同一位置时,生产者必须在消费者前面
  • 在同一位置时,为空让生产者先走,为满让消费者先走,但是消费者不可以套圈
  • 消费者最关心队列中的数据,因此可以定义一个信号量关心队列已有数据个数
  • 生产最关心队列的空位置,因此可以定义一个信号量关心队列的空位置
  • 不能让它们同时执行,但是可以并发执行

代码示例:

/**********************************ring_queue.hpp***************************************/
#pragma once

#include <iostream>
#include <semaphore.h>
#include <pthread.h>
#include <unistd.h>
#include <vector>

namespace dd
{

template<class T>
class Ring_queue
{
public:
    //初始化
    Ring_queue()
        :_cap(10)
        ,_c_step(0)
        ,_p_step(0)
    {
        sem_init(&_blank_sem,0,10);	//位置信号量设置初始值为10
        sem_init(&_data_sem,0,0);	//数据信号量设置初始值为0
        _rq.reserve(10);
    }
    //释放信号量
    ~Ring_queue()
    {
        sem_destroy(&_blank_sem);
        sem_destroy(&_data_sem);
    }
	//生产
    void Push(const T& key)
    {
        //申请数据消费信号量,放入数据,释放位置信号量
        sem_wait(&_blank_sem);
        _rq[_p_step] = key;
        sem_post(&_data_sem);
        
		//更新位置
        _p_step++;
        _p_step %= _cap;
    }
	//消费
    void Pop(T* key)
    {
        //申请位置信号量,取出数据,释放数据消费信号量
        sem_wait(&_data_sem);
        *key = _rq[_c_step];
        sem_post(&_blank_sem);
		
        //更新位置
        _c_step++;
        _c_step %= _cap;
    }

private:
    int _cap;					//总容量
    std::vector<T> _rq;			//队列
    sem_t _blank_sem;			//位置信号量
    sem_t _data_sem;			//数据信号量
    int _c_step;				//消费者位置(下标)
    int _p_step;				//生产者位置(下标)
    
};   

}



/*************************************************************************/
#include "ring_queue.hpp"
#include <time.h>
using namespace dd;

void* consumer(void* args)
{
    Ring_queue<int>* rq = (Ring_queue<int>*)args;
    while(true)
    {
        int data = rand()%20 + 1;
        rq->Push(data);
        std::cout << "生产数据:" << data << std::endl;
        
    }

}

void* producter(void* args)
{
    Ring_queue<int>* rq = (Ring_queue<int>*)args;
    while(true)
    {
        sleep(1);
        int data;
        rq->Pop(&data);
        std::cout << "消费数据:" << data << std::endl;
    }
}


int main()
{
    srand((long long)time(nullptr));
    pthread_t c,p;
    Ring_queue<int>* rq = new Ring_queue<int>();

    pthread_create(&c,nullptr,consumer,(void*)rq);
    pthread_create(&p,nullptr,producter,(void*)rq);
	

    pthread_join(c,nullptr);
    pthread_join(p,nullptr);
    return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iqsOYh0J-1677869705611)(G:\Typora\图片保存\image-20230205201654712.png)]

  • 多生产多消费的区别

        void Push(const T& key)
        {
            sem_wait(&_blank_sem);
            
            pthread_mutex_lock(&p_mtx_);
            _rq[_p_step] = key;       
            _p_step++;
            _p_step %= _cap;
            pthread_mutex_unlock(&p_mtx_);
            
            sem_post(&_data_sem);
        }
    
        void Pop(T* key)
        {
            sem_wait(&_data_sem);
            
            pthread_mutex_lock(&c_mtx_);
            *key = _rq[_c_step];		
            _c_step++;
            _c_step %= _cap;
            pthread_mutex_unlock(&c_mtx_);
            
            sem_post(&_blank_sem);
        }
    
    • sem_wait是原子性的,但多生产多消费中的队列和下标是临界资源,它们不是原子的,所以需要加锁

    • 另外先进行信号量申请相对效率高,因为无论是生产还是消费本质是它们的信号量不为0,而不是先拿到锁

      最终,互斥锁加在信号量申请之后,避免争到锁但没有信号量的情况

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/391547.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

分享几种WordPress怎么实现相关文章功能

一淘模板&#xff08;56admin.com&#xff09;给大家介绍一下WordPress代码实现相关文章的几种方法&#xff0c;希望对大家有所帮助&#xff01; WordPress很多插件可以实现相关文章的功能&#xff0c;插件的优点是配置简单&#xff0c;但是可能会对网站的速度造成一些小的影响…

做数据分析有前景吗?

当然有前景的。 每个行业都有发展前景&#xff0c;只是看你自身的技能情况或者关系人脉、软实力方面是否到位&#xff0c;不同的行业要求不一样。作为数据分析领域而言&#xff0c;属于IT行业&#xff0c;看的是你的专业技能&#xff1b;只要你技能过硬&#xff0c;就能在行业…

蓝桥杯 时间显示

题目 输入输出样例 示例 1 输入 46800999输出 13:00:00示例 2 输入 1618708103123输出 01:08:23评测用例规模与约定 对于所有评测用例&#xff0c;给定的时间为不超过 10^{18}1018 的正整数。 运行限制 最大运行时间&#xff1a;1s最大运行内存: 512M 基础知识 时间的转换…

Go底层原理:一起来唠唠GMP调度(一)

目录前言一、进程、线程、Goroutine1、进程与线程2、Goroutine二、Go调度器设计思想1、线程模型1.1 内核级线程模型1.2 用户级线程模型1.3 混合型线程模型2、 被废弃的 G-M 调度器2.1 了解 G-M 调度如何工作3、如今高效的 GMP 模型3.1 GMP模型调度流程3.2 GMP调度设计策略3.3 G…

【Vue3】封装数字框组件

数量选择组件-基本结构 &#xff08;1&#xff09;准备基本结构 <script lang"ts" setup name"Numbox"> // </script> <template><div class"numbox"><div class"label">数量</div><div cla…

C语言-基础了解-19-C位域

C位域 一、C位域 如果程序的结构中包含多个开关量&#xff0c;只有 TRUE/FALSE 变量&#xff0c;如下&#xff1a; struct {unsigned int widthValidated;unsigned int heightValidated; } status;这种结构需要 8 字节的内存空间&#xff0c;但在实际上&#xff0c;在每个变…

引领云数仓创新浪潮 HashData闪耀PostgreSQL中国技术大会

3月3日-3月5日&#xff0c;第12届PostgreSQL中国技术大会在杭州举行。本次大会以“突破•进化•共赢——安全可靠&#xff0c;共建与机遇”为主题&#xff0c;以线上线上结合的方式&#xff0c;邀请了PG领域众多行业大咖、学术精英及技术专家&#xff0c;共同探讨数据库未来的发…

【C/C++ 数据结构】-八大排序之 归并排序其它排序

作者&#xff1a;学Java的冬瓜 博客主页&#xff1a;☀冬瓜的主页&#x1f319; 专栏&#xff1a;【C/C数据结构与算法】 分享&#xff1a;本王在此&#xff0c;狼狈为奸者&#xff0c;谋权篡位者&#xff0c;倒行逆施者&#xff0c;都得死&#xff01; ——岐王李茂贞《画江湖…

Linux 静态与动态编译、静态库与动态库

文章目录一、库的简介二、静态链接和动态链接1、静态链接2、动态链接3、GCC 下动态库与静态库三、静态库制作和使用四、动态库制作和使用总结一、库的简介 什么是库文件呢&#xff1f; 所谓库文件&#xff0c;大家可以将其等价为压缩包文件&#xff0c;该文件内部通常包含不止…

MyBatis源码分析(六)MetaObject工具类的使用与源码分析

文章目录一、MetaObject基本使用二、关键类源码分析1、MetaObject的构造方法2、PropertyTokenizer分词器3、BeanWrapper4、MetaClass5、DefaultReflectorFactory6、Reflector7、总结三、MetaObject的getValue源码分析写在后面一、MetaObject基本使用 public class User {priva…

OPT(奥普特)一键测量传感器SmartFlash高精度的四重保证

OPT&#xff08;奥普特&#xff09;一键测量传感器SmartFlash集成了机器视觉的边缘提取、自动匹配、自动对焦、自动学习及图像合成等人工智能技术&#xff0c;采用双远心光路及多角度照明系统设计&#xff0c;搭载高精度运动平台&#xff0c;并通过亚像素边缘提取算法处理图像&…

Mysql全解[中级篇]

目录存储引擎MySQL体系结构1). 连接层2). 服务层3). 引擎层4). 存储层存储引擎介绍存储引擎特点InnoDBMyISAMMemory文件区别及特点存储引擎选择索引无索引情况有索引情况特点索引结构二叉树红黑树B-TreeBTreeMySQL中优化之后的BTreeHash索引分类聚集索引&二级索引回表查询索…

双周赛99(贪心、数学、区间合并计算、换根DP)

文章目录双周赛99[6312. 最小和分割](https://leetcode.cn/problems/split-with-minimum-sum/)贪心[6311. 统计染色格子数](https://leetcode.cn/problems/count-total-number-of-colored-cells/)找规律[6313. 统计将重叠区间合并成组的方案数](https://leetcode.cn/problems/c…

规并排序(Swift版本)

Overview 概述 时间复杂度为 O(nlogn) ;适合大规模的数据排序 ;相比于冒泡排序、插入排序、选择排序这三种排序算法, 更加常用 ;用到了分治思想(即分而治之, 英文叫 “Divide and conquer”)&#xff0c;非常巧妙 ;英文名称: Merge Sort ; 分治思想, 在很多领域都有广泛的应用…

windows系统安装Linux虚拟机教程

虚拟机的安装首先要下载虚拟机的安装包&#xff0c;当前最新版本是VMware 16.2.1。软件我都已经给大家准备好了&#xff08;含序列号&#xff09;&#xff0c;大家在这里下载就好。虚拟机安装包下载完毕之后&#xff0c;将它安装到电脑里。这个安装过程很简单&#xff0c;一路下…

Linux操作系统学习(线程池)

文章目录线程池线程池原理代码示例单例模式饿汉模式懒汉模式饿汉懒汉对比其他的锁线程池 线程池原理 ​ 线程池是一种线程使用模式。在多线程应用中&#xff0c;若每有一个任务&#xff0c;线程就去调度相应的函数去创建&#xff0c;当任务过多时&#xff0c;每次都去调度且每…

CCF大数据专家委员会十周年纪念庆典纪实:拥抱数字时代,展望科技未来

山河远阔&#xff0c;奋进十年&#xff0c;作为国内大数据领域最权威的学术组织&#xff0c;CCF大数据专家委员会&#xff08;以下简称“大专委”&#xff09;不忘初心&#xff0c;凝心聚力&#xff0c;见证并推动了过去10年来大数据技术生态在中国的建立、发展和成熟。 2023年…

HBase安装

文章目录一、安装Zookeeper二、安装HBase三、启动Hbase步骤四、关闭进程顺序五、简单使用Hbase在开始安装HBase之前&#xff0c;请确保您已经安装了Java运行环境和Hadoop分布式文件系统。如果您还没有安装这些软件&#xff0c;请查看之前博文介绍安装。 HBase安装包&#xff1a…

谷歌广告投放步骤流程是什么?一文带你全方位了解实操细节

谷歌&#xff0c;大家都不陌生吧&#xff0c;一个人们很常用的搜索引擎。而谷歌还可以打广告&#xff0c;即谷歌广告&#xff0c;那这跟跨境电商有什么关心呢&#xff1f;东哥告诉大家&#xff0c;关系大了去了&#xff0c;毕竟如果用户搜索与我们相关的关键词&#xff0c;就有…

streaming systems 第二章

The What, Where, When, and How of Data Processing 第一章主要关注三个领域:术语&#xff0c;准确定义我使用重载术语时的意思&#xff0c;如“流”;批处理和流处理&#xff0c;比较两种类型系统的理论能力&#xff0c;并假设使流处理系统超越批处理系统只有两件事是必要的:…