文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

Linux知识点 -- Linux多线程(三)

2023-08-30 15:48

关注

Linux知识点 – Linux多线程(三)


一、线程同步

1.概念理解

持有锁的线程会频繁进入临界区申请临界资源,造成其他进程饥饿的问题;
这本身是没有错的,但是不合理;
线程同步:就是线程按照一定的顺序,进行临界资源的访问;主要就是为了解决访问临界资源和理性的问题;在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题;

2.条件变量

注:pthread库返回值都是成功返回0,失败返回错误码;

3.使用条件变量进行线程同步

按照一定顺序控制线程:

#include #include #include #include #include using namespace std;#define TNUM 4//共四个线程typedef void (*func_t) (const string &name, pthread_mutex_t* pmtx, pthread_cond_t* pcond);//定义函数指针class ThreadData{public:    ThreadData(const string& name, func_t func, pthread_mutex_t* pmtx, pthread_cond_t* pcond)        : _name(name)        , _func(func)        , _pmtx(pmtx)        , _pcond(pcond)    {}public:    string _name;//线程名    func_t _func;//线程回调的函数    pthread_mutex_t* _pmtx;//锁    pthread_cond_t* _pcond;//条件变量};void func1(const string &name, pthread_mutex_t* pmtx, pthread_cond_t* pcond){    while(true)    {        pthread_cond_wait(pcond, pmtx);//默认该线程执行的时候,wait代码被执行,当前线程会立即被阻塞            //阻塞就是将当前进程放进一个队列中去等待,并且再等待条件满足后被唤醒        cout << name << "running -- 播放" << endl;    }}void func2(const string &name, pthread_mutex_t* pmtx, pthread_cond_t* pcond){    while(true)    {        pthread_cond_wait(pcond, pmtx);        cout << name << "running -- 下载" << endl;    }}void func3(const string &name, pthread_mutex_t* pmtx, pthread_cond_t* pcond){    while(true)    {        pthread_cond_wait(pcond, pmtx);        cout << name << "running -- 刷新" << endl;    }}void func4(const string &name, pthread_mutex_t* pmtx, pthread_cond_t* pcond){    while(true)    {        pthread_cond_wait(pcond, pmtx);        cout << name << "running -- 扫描" << endl;    }}//每一个线程都进入Entry接口,在entry接口内调用自己的函数void* Entry(void* args){    ThreadData* td = (ThreadData*)args;//td在每一个线程自己私有的栈空间中保存    td->_func(td->_name, td->_pmtx, td->_pcond);//这是一个函数,调用完就返回这里    delete td;//需要在td使用完后进行销毁    return nullptr;}int main(){    pthread_mutex_t mtx;    //锁    pthread_cond_t cond;    //条件变量    pthread_mutex_init(&mtx, nullptr);    pthread_cond_init(&cond, nullptr);    pthread_t tids[TNUM];    func_t funcs[TNUM] = {func1, func2, func3, func4};    for(int i = 0; i < TNUM; i++)    {        string name = "Thread ";        name += to_string(i + 1);        ThreadData* td = new ThreadData(name, funcs[i], &mtx, &cond);        pthread_create(tids + i, nullptr, Entry, (void*)td);    }    sleep(5);//主线程sleep,新线程创建出来都在wait    while(true)    {        cout << "resume thread run code ..." << endl;        pthread_cond_signal(&cond);//唤醒在指定条件变量下等待的线程,不用指定线程,因为wait的时候线程已经在队列中排队了        sleep(1);    }    for(int i = 0; i < TNUM; i++)    {        pthread_join(tids[i], nullptr);        cout << "thread: " << tids[i] << "quit" << endl;    }    pthread_mutex_destroy(&mtx);    pthread_cond_destroy(&cond);    return 0;}

上面的代码创建了局部的锁和条件变量,创建了四个新线程,将线程名、回调的函数地址、锁和条件变量的地址都放进了一个类对象中;
在创建线程的函数中,每个线程都调用的是一个Entry入口函数,在Entry接口内调用自己的函数;
在线程执行的函数中,默认该线程执行的时候,wait代码被执行,当前线程会立即被阻塞;阻塞就是将当前进程放进一个队列中去等待,并且再等待条件满足后被唤醒;
当主线程执行到pthread_cond_signal函数时,会唤醒在指定条件变量下等待的线程;不用指定线程,因为wait的时候线程已经在队列中排队了;

运行结果:
在这里插入图片描述
主线程在等待了5s后,开始调用新线程执行任务,并且新线程是按照一定的顺序被唤醒的;

如果使用pthread_cond_broadcast接口一次唤醒一批线程:

int main(){    pthread_mutex_t mtx;    //锁    pthread_cond_t cond;    //条件变量    pthread_mutex_init(&mtx, nullptr);    pthread_cond_init(&cond, nullptr);    pthread_t tids[TNUM];    func_t funcs[TNUM] = {func1, func2, func3, func4};    for(int i = 0; i < TNUM; i++)    {        string name = "Thread ";        name += to_string(i + 1);        ThreadData* td = new ThreadData(name, funcs[i], &mtx, &cond);        pthread_create(tids + i, nullptr, Entry, (void*)td);    }    sleep(5);//主线程sleep,新线程创建出来都在wait    while(true)    {        cout << "resume thread run code ..." << endl;        //pthread_cond_signal(&cond);//唤醒在指定条件变量下等待的线程,不用指定线程,因为wait的时候线程已经在队列中排队了        pthread_cond_broadcast(&cond);//一次唤醒一批线程        sleep(1);    }    for(int i = 0; i < TNUM; i++)    {        pthread_join(tids[i], nullptr);        cout << "thread: " << tids[i] << "quit" << endl;    }    pthread_mutex_destroy(&mtx);    pthread_cond_destroy(&cond);    return 0;}

运行结果:
在这里插入图片描述
等待队列中所有的线程被一次全部唤醒;

回调函数临界区加入加锁和解锁:
wait一定要在加锁和解锁之间进行;
加入了quit标志位,任务执行完后线程退出;
在这里插入图片描述

#include #include #include #include #include using namespace std;#define TNUM 4//共四个线程typedef void (*func_t) (const string &name, pthread_mutex_t* pmtx, pthread_cond_t* pcond);//定义函数指针volatile bool quit = false;//加入quit标志位class ThreadData{public:    ThreadData(const string& name, func_t func, pthread_mutex_t* pmtx, pthread_cond_t* pcond)        : _name(name)        , _func(func)        , _pmtx(pmtx)        , _pcond(pcond)    {}public:    string _name;//线程名    func_t _func;//线程回调的函数    pthread_mutex_t* _pmtx;//锁    pthread_cond_t* _pcond;//条件变量};void func1(const string &name, pthread_mutex_t* pmtx, pthread_cond_t* pcond){    while(!quit)//加入退出判断    {        //wait一定要在加锁和解锁之间进行        pthread_mutex_lock(pmtx);        //if(临界资源未就绪) 等待        pthread_cond_wait(pcond, pmtx);//默认该线程执行的时候,wait代码被执行,当前线程会立即被阻塞            //阻塞就是将当前进程放进一个队列中去等待,并且再等待条件满足后被唤醒        cout << name << "running -- 播放" << endl;        pthread_mutex_unlock(pmtx);    }}void func2(const string &name, pthread_mutex_t* pmtx, pthread_cond_t* pcond){    while(!quit)    {        pthread_mutex_lock(pmtx);        pthread_cond_wait(pcond, pmtx);        cout << name << "running -- 下载" << endl;        pthread_mutex_unlock(pmtx);    }}void func3(const string &name, pthread_mutex_t* pmtx, pthread_cond_t* pcond){    while(!quit)    {        pthread_mutex_lock(pmtx);        pthread_cond_wait(pcond, pmtx);        cout << name << "running -- 刷新" << endl;        pthread_mutex_unlock(pmtx);    }}void func4(const string &name, pthread_mutex_t* pmtx, pthread_cond_t* pcond){    while(!quit)    {        pthread_mutex_lock(pmtx);        pthread_cond_wait(pcond, pmtx);        cout << name << "running -- 扫描" << endl;        pthread_mutex_unlock(pmtx);    }}//每一个线程都进入Entry接口,在entry接口内调用自己的函数void* Entry(void* args){    ThreadData* td = (ThreadData*)args;//td在每一个线程自己私有的栈空间中保存    td->_func(td->_name, td->_pmtx, td->_pcond);//这是一个函数,调用完就返回这里    delete td;//需要在td使用完后进行销毁    return nullptr;}int main(){    pthread_mutex_t mtx;    //锁    pthread_cond_t cond;    //条件变量    pthread_mutex_init(&mtx, nullptr);    pthread_cond_init(&cond, nullptr);    pthread_t tids[TNUM];    func_t funcs[TNUM] = {func1, func2, func3, func4};    for(int i = 0; i < TNUM; i++)    {        string name = "Thread ";        name += to_string(i + 1);        ThreadData* td = new ThreadData(name, funcs[i], &mtx, &cond);        pthread_create(tids + i, nullptr, Entry, (void*)td);    }    sleep(5);//主线程sleep,新线程创建出来都在wait    int cnt = 10;    while(cnt)    {        cout << "resume thread run code ..."  << cnt-- << endl;        pthread_cond_signal(&cond);//唤醒在指定条件变量下等待的线程,不用指定线程,因为wait的时候线程已经在队列中排队了        //pthread_cond_broadcast(&cond);//一次唤醒一批线程        sleep(1);    }    cout << "control done" << endl;    quit = true;    pthread_cond_broadcast(&cond);//再唤醒一下线程,让其检测quit信号    for(int i = 0; i < TNUM; i++)    {        pthread_join(tids[i], nullptr);        cout << "thread: " << tids[i] << "quit" << endl;    }    pthread_mutex_destroy(&mtx);    pthread_cond_destroy(&cond);    return 0;}

运行结果:
在这里插入图片描述

二、生产者消费者模型

1.概念

生产者消费者模型就是一种多线程运作的模型,就像超市一样,生产者生产了商品运送到超市售卖,而消费者从超市里购买商品;
在这里插入图片描述
其中,生产者和消费者都是给线程进行了角色化,不同的线程执行不同的职能,超市则是一个数据的缓冲区,商品就是数据;

这个模型能够让生产者和消费者线程之间实现解耦,提高效率;
当生产者生产了商品,就能够给消费者同步信息,唤醒消费者线程;
当消费者消费之后,就能给生产者同步信息,唤醒生产者线程,继续生产;
可以让生产者和消费者线程互相同步;
在逻辑层面上解耦消费者和生产者,能够提高效率
重点是给线程赋予了角色;
需要消除生产中的状态,避免数据不一致;

2.基于BlockingQueue的生产者消费者模型

3.单生产者单消费者模型

BlockQueue.hpp:

#include #include #include #include using namespace std;const int gDefaultCap = 5;template <class T>class BlockQueue{private:    bool isQueueEmpty()    {        return _bq.size() == 0;    }    bool isQueueFull()    {        return _bq.size() == _capacity;    }public:    BlockQueue(int capacity = gDefaultCap)        : _capacity(capacity)    {        pthread_mutex_init(&_mtx, nullptr);        pthread_cond_init(&_Empty, nullptr);        pthread_cond_init(&_Full, nullptr);    }    void push(const T &in) // 生产者放数据    {        pthread_mutex_lock(&_mtx);        // 1.先检测当前的临界资源是否满足访问条件        // pthread_cond_wait是在临界区中的,此时进程是持有锁的,如果去等待了,锁怎么办?        // pthread_cond_wait第二个参数是一个锁,当此进程成功挂起后,传入的锁,会被自动释放        // 当此进程被唤醒时,从哪里阻塞的,就从那里唤醒,被唤醒的时候,此进程还是在临界区内部的        // 当被唤醒的时候,pthread_cond_wait会帮助此线程获取锁        // pthread_cond_wait:只要是一个函数,就有可能调用失败,也有可能存在伪唤醒的情况        // 因此条件变量的使用规范:使用while循环持续进行条件检测        // 这样在访问临界资源时,就能100%确定资源是就绪的        while (isQueueFull())        {            pthread_cond_wait(&_Full, &_mtx);        }        // 2.访问临界资源        _bq.push(in);        // 加入控制策略:当队列中数据量过半后,才唤醒消费者线程        if (_bq.size() >= _capacity / 2)        {            pthread_cond_signal(&_Empty); // 生产者放了数据后,就唤起消费者线程,通知其消费        }        pthread_mutex_unlock(&_mtx);        // 发信号在解锁之前和之后都是可以的    }    void pop(T *out)    {        pthread_mutex_lock(&_mtx);        while (isQueueEmpty())        {            pthread_cond_wait(&_Empty, &_mtx);        }        *out = _bq.front();        _bq.pop();        pthread_mutex_unlock(&_mtx);        pthread_cond_signal(&_Full); // 消费者取走数据后,就唤起生产者线程,通知其生产    }    ~BlockQueue()    {        pthread_mutex_destroy(&_mtx);        pthread_cond_destroy(&_Empty);        pthread_cond_destroy(&_Full);    }private:    queue<T> _bq;          // 阻塞队列    int _capacity;         // 容量上限    pthread_mutex_t _mtx;  // 通过互斥锁保证队列安全    pthread_cond_t _Empty; // 同来表示bq 是否为空的条件    pthread_cond_t _Full;  // 同来表示bq 是否为满的条件};

ConPod.cc:

#include"BlockQueue.hpp"using namespace std;void* consumer(void* args){    BlockQueue<int>* bqueue = (BlockQueue<int>*)args;    while(true)    {        int a;        bqueue->pop(&a);        cout << "消费一个数据:" << a << endl;        sleep(1);    }    return nullptr;}void* productor(void* args){    BlockQueue<int>* bqueue = (BlockQueue<int>*)args;    int a = 1;    while(true)    {        bqueue->push(a++);        cout << "生产一个数据:" << a << endl;    }    return nullptr;}int main(){    BlockQueue<int>* bqueue = new BlockQueue<int>();    pthread_t c, p;    pthread_create(&c, nullptr, consumer, bqueue);    pthread_create(&p, nullptr, productor, bqueue);    pthread_join(c, nullptr);    pthread_join(p, nullptr);    delete bqueue;    return 0;}

运行结果:在这里插入图片描述

4.多生产者多消费者模型

Task.hpp:

#pragma once#include #include typedef std::function<int(int, int)> func_t;class Task{public:    Task(){}    Task(int x, int y, func_t func):x_(x), y_(y), func_(func)    {}    int operator ()()    {        return func_(x_, y_);    }public:    int x_;    int y_;    func_t func_;};

封装一个Task类,队列中存储这个类,类中能够调用回调函数;

BlockQueue.hpp:(同上)

ConPod.cc:

#include"BlockQueue.hpp"#include"Task.hpp"#include using namespace std;int myAdd(int x, int y){    return x + y;}void* consumer(void* args){    BlockQueue<Task>* bqueue = (BlockQueue<Task>*)args;    while(true)    {        //获取任务        Task t;        bqueue->pop(&t);        //完成任务        cout << pthread_self() << "consumer: " << t.x_ << "+" << t.y_ << "=" << t() <<  endl;        sleep(1);    }    return nullptr;}void* productor(void* args){    BlockQueue<Task>* bqueue = (BlockQueue<Task>*)args;    int a = 1;    while(true)    {        //制作任务        int x = rand() % 10 + 1;        usleep(rand()%1000);        int y = rand() % 5 + 1;        Task t(x, y, myAdd);        //生产任务        bqueue->push(t);        cout << pthread_self() << "productor: " << t.x_ << "+" << t.y_ << "=?" << endl;        sleep(1);    }    return nullptr;}int main(){    srand((uint64_t)time(nullptr) ^ getpid());    BlockQueue<Task>* bqueue = new BlockQueue<Task>();    pthread_t c[2], p[2];    pthread_create(c, nullptr, consumer, bqueue);    pthread_create(c + 1, nullptr, consumer, bqueue);    pthread_create(p, nullptr, consumer, bqueue);    pthread_create(p + 1, nullptr, productor, bqueue);    pthread_join(c[0], nullptr);    pthread_join(c[1], nullptr);    pthread_join(p[0], nullptr);    pthread_join(p[1], nullptr);    delete bqueue;    return 0;}

结果:
在这里插入图片描述

5.锁的封装

lockGuard.hpp

#pragma once#include #include class Mutex{public:    Mutex(pthread_mutex_t *mtx)        : _pmtx(mtx)    {    }    void lock()    {        pthread_mutex_lock(_pmtx);    }    void unlock()    {        pthread_mutex_unlock(_pmtx);    }    ~Mutex()    {}private:    pthread_mutex_t *_pmtx;};class lockGuard{public:    lockGuard(pthread_mutex_t *mtx)        : _mtx(mtx)    {        _mtx.lock();    }    ~lockGuard()    {        _mtx.unlock();    }private:    Mutex _mtx;};

BlockQueue.hpp:

#pragma once#include #include #include #include #include"lockGuard.hpp"using namespace std;const int gDefaultCap = 5;template <class T>class BlockQueue{private:    bool isQueueEmpty()    {        return _bq.size() == 0;    }    bool isQueueFull()    {        return _bq.size() == _capacity;    }public:    BlockQueue(int capacity = gDefaultCap)        : _capacity(capacity)    {        pthread_mutex_init(&_mtx, nullptr);        pthread_cond_init(&_Empty, nullptr);        pthread_cond_init(&_Full, nullptr);    }    void push(const T &in) // 生产者放数据    {        lockGuard lockguard(&_mtx);//自动调用构造函数,加锁        while (isQueueFull())        {            pthread_cond_wait(&_Full, &_mtx);        }                _bq.push(in);        if (_bq.size() >= _capacity / 2)        {            pthread_cond_signal(&_Empty);}        //自动调用析构函数,解锁    }    void pop(T *out)    {        lockGuard lockguard(&_mtx);//自动调用构造函数,加锁        while (isQueueEmpty())        {            pthread_cond_wait(&_Empty, &_mtx);        }        *out = _bq.front();        _bq.pop();        pthread_cond_signal(&_Full);         //自动调用析构函数,解锁    }    ~BlockQueue()    {        pthread_mutex_destroy(&_mtx);        pthread_cond_destroy(&_Empty);        pthread_cond_destroy(&_Full);    }private:    queue<T> _bq;          // 阻塞队列    int _capacity;         // 容量上限    pthread_mutex_t _mtx;  // 通过互斥锁保证队列安全    pthread_cond_t _Empty; // 同来表示bq 是否为空的条件    pthread_cond_t _Full;  // 同来表示bq 是否为满的条件};

构造lockGuard对象的时候,就已经加锁完成了;
析构的时候,自动解锁;

三、POSIX信号量

1.信号量的概念与使用

共享资源:任何一个时刻只有一个执行流在进行访问,共享资源是被当作整体使用的,执行流之间都是互斥的;
如果一个共享资源不被当做一个整体,而让不同的执行流访问不同的区域,就可以多执行流并发访问了,不同执行流只有在访问同一个区域的时候才需要进行互斥;
当前共享资源中还有多少份资源,特定的执行流使用可以是否可以得到一个共享资源,这些都可以通过信号量来实现;

2.信号量的使用场景

(1)有共享资源;
(2)共享资源可以被局部性访问;
(3)需要对局部性资源的数量进行描述;

3.信号量接口

4.基于环形队列的生产消费模型

5.基于环形队列的生产消费模型实现

sem.hpp
信号量的封装,初始化对象时,就调用构造进行信号量的初始化;
对象销毁时,就自动调用析构,销毁信号量 ;

#ifndef _SEM_HPP_#define _SEM_HPP_#include#includeclass Sem{public:    Sem(int val)    {        sem_init(&_sem, 0, val);    }    void p()    {        sem_wait(&_sem);    }    void v()    {        sem_post(&_sem);    }    ~Sem()    {        sem_destroy(&_sem);    }private:    sem_t _sem;};#endif

ringQueue.hpp

#ifndef _RING_QUEUE_HPP_#define _RING_QUEUE_HPP_#include #include #include #include #include #include #include #include "sem.hpp"const int g_default_num = 5;using namespace std;template <class T>class RingQueue{public:    RingQueue(int default_num = g_default_num)        : _ring_queue(default_num)        , _num(default_num)        , _c_step(0)        , _p_step(0)        , _space_sem(default_num)        , _data_sem(0)    {        pthread_mutex_init(&_clock, nullptr);        pthread_mutex_init(&_plock, nullptr);    }    ~RingQueue()    {        pthread_mutex_destroy(&_clock);        pthread_mutex_destroy(&_plock);    }    // 生产者:空间资源,生产者们的临界资源是下标    // 加锁和申请信号量的先后:信号量一定是安全的,具有原子性,    // 资源是要配发给线程的,资源配发的越快,运行效率越高,因此先申请信号量,再加锁    // 加锁的粒度越小越好    void push(const T &in)    {        // 先申请空间信号量        _space_sem.p();        // 多生产进程访问时,当一个生产者线程访问一个下标时,加锁,其他线程来访问时就需要等待        pthread_mutex_lock(&_plock);        // 成功竞争到锁的线程继续执行下面操作        // 放入数据        _ring_queue[_p_step++] = in;        _p_step %= _num;        // 生产完后,解锁        pthread_mutex_unlock(&_plock);        // 释放数据信号量        _data_sem.v();    }    void pop(T *out)    {        _data_sem.p();        pthread_mutex_lock(&_clock);        *out = _ring_queue[_c_step++];        _c_step %= _num;        pthread_mutex_unlock(&_clock);        _space_sem.v();    }private:    vector<T> _ring_queue;    int _num;    int _c_step;            // 消费下标    int _p_step;            // 生产下标    Sem _space_sem;         // 空间信号量    Sem _data_sem;          // 数据信号量    pthread_mutex_t _clock; // 多消费者进程的锁    pthread_mutex_t _plock; // 多生产者进程的锁};#endif

ConPod.cc

#include"ringQueue.hpp"void* consumer(void* args){    RingQueue<int>* rq = (RingQueue<int>*)args;    while(true)    {        sleep(1);        int x = 0;        //从环形队列中获取任务或数据        rq->pop(&x);        //进行一定的处理        cout << "消费:" << x << "[" << pthread_self() << "]" << endl;    }}void* procudtor(void* args){    RingQueue<int>* rq = (RingQueue<int>*)args;    while(true)    {        //构建数据或任务对象        int x = rand() % 100 + 1;        //放入环形队列        rq->push(x);        cout << "生产:" << x << "[" << pthread_self() << "]" << endl;    }}int main(){    srand((uint64_t)time(nullptr) ^ getpid());    RingQueue<int>* rq = new RingQueue<int>();    pthread_t c[3], p[2];    pthread_create(c, nullptr, consumer, (void*)rq);    pthread_create(c + 1, nullptr, consumer, (void*)rq);    pthread_create(c + 2, nullptr, consumer, (void*)rq);        pthread_create(p, nullptr, procudtor, (void*)rq);    pthread_create(p + 1, nullptr, procudtor, (void*)rq);    for(int i = 0; i < 3; i++)    {        pthread_join(c[i], nullptr);    }    for(int i = 0; i < 2; i++)    {        pthread_join(p[i], nullptr);    }    return 0;}

运行结果:
在这里插入图片描述

6.信号量的意义

信号量的本质是一个计数器,它的意义在于可以不用进入临界区,就可以得知资源的情况,甚至可以减少临界区内部的判断;
申请锁和释放锁的过程,本质在于我们并不清楚临界资源的情况;
信号量要预设临界资源的情况,而且在pv变化过程中,我们在外部就能够知晓临界资源的情况;

来源地址:https://blog.csdn.net/kissland96166/article/details/132416026

阅读原文内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-人工智能
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯