Linux--线程互斥(加锁)

目录

0.对原生线程封装的代码

1.为什么需要线程互斥

 2.加锁

2.1.认识加锁和它的接口

2.2用一下接口

2.2.1 解决抢票问题

2.2.2设置局部锁

2.3从原理角度理解锁

2.4锁是如何实现的****


0.对原生线程封装的代码

方便后续对锁的理解:Thread.hpp

详情请看:Linux--线程ID&&封装管理原生线程-CSDN博客

#pragma once
#include <iostream>
#include <string>
#include <pthread.h>namespace ThreadMoudle
{// 线程要执行的方法,后面我们随时调整typedef void (*func_t)(const std::string &name); // 函数指针类型//执行函数时,名字带出来,方便打印测试结果class Thread{public://成员方法调用_func执行任务void Excute(){std::cout << _name << " is running" << std::endl;_isrunning = true;//开始回调了,就表示线程跑起来了_func(_name);_isrunning = false;}public://构造Thread(const std::string &name, func_t func):_name(name), _func(func){std::cout << "create " << name << " done" << std::endl;}// 线程的固定历程,新线程都会执行该方法!static void *ThreadRoutine(void *args) {//为了匹配类型,加static属于类而不属于对象,就没有this指针了//this指针从creat函数传递过来Thread *self = static_cast<Thread*>(args); // 获得了当前对象self->Excute();//直接调用成员方法return nullptr;//简单的演示,没有设置返回值}//线程启动bool Start(){//使用标准库中的方法创建进程int n = ::pthread_create(&_tid, nullptr, ThreadRoutine, this);if(n != 0) return false;return true;}//表示状态std::string Status(){if(_isrunning) return "running";else return "sleep";}void Stop(){//表示有线程在running才需要stopif(_isrunning){::pthread_cancel(_tid);//取消_isrunning = false;//状态变为停止std::cout << _name << " Stop" << std::endl;}}void Join(){//线程退出后等待回收。if(!_isrunning){::pthread_join(_tid, nullptr);std::cout << _name << " Joined" << std::endl;}}//知道是哪个线程std::string Name(){return _name;}~Thread(){}private:std::string _name;//线程名字pthread_t _tid;//IDbool _isrunning;//是否在运行func_t _func; // 线程要执行的回调函数(任务)};
} 

1.为什么需要线程互斥

  1. 数据一致性和完整性
    在并发环境中,多个线程可能会同时尝试读取或修改同一个数据项。如果没有适当的互斥机制,这种并发访问可能导致数据损坏或不一致性。例如,一个线程可能在另一个线程完成更新之前读取了部分更新的数据,从而导致数据错误。

  2. 避免竞争条件
    竞争条件是指两个或多个线程在尝试执行一系列操作时,由于执行顺序的不可预测性而导致的错误输出。这通常发生在多个线程试图同时更新共享资源时。通过实现互斥,可以确保在任何给定时间内只有一个线程能够访问该资源,从而避免竞争条件。

  3. 保护临界区
    临界区是指访问共享资源的那部分代码,这些代码的执行需要互斥保护以防止数据竞争。通过互斥机制,可以确保在任何给定时间内,只有一个线程能够执行临界区代码,从而保护共享资源免受并发访问的干扰。

  4. 提高程序的稳定性和可靠性
    通过实施线程互斥,可以减少并发程序中的错误和异常。这有助于确保程序即使在高负载或异常情况下也能稳定运行,从而提高整体的系统稳定性和可靠性。

  5. 简化并发编程
    虽然互斥可能会增加程序的复杂性(因为需要管理锁和其他同步机制),但它也简化了并发编程的某些方面。通过提供明确的同步点,互斥使得开发人员能够更容易地理解和控制线程之间的交互,从而编写出更加健壮和可靠的并发程序。

见一见多线程访问的问题 --抢票的代码:

                我们发现会有抢到负数的情况。

#include <iostream>
#include <vector>
#include <cstdio>
#include <unistd.h>
#include "Thread.hpp"using namespace ThreadMoudle;int tickets = 10000;void route(const std::string &name)
{while(true){if(tickets > 0){// 抢票过程usleep(1000); // 1ms -> 抢票花费的时间printf("who: %s, get a ticket: %d\n", name.c_str(), tickets);tickets--;}else{break;}}
}int main()
{Thread t1("thread-1", route);Thread t2("thread-2", route);Thread t3("thread-3", route);Thread t4("thread-4", route);t1.Start();t2.Start();t3.Start();t4.Start();t1.Join();t2.Join();t3.Join();t4.Join();
}


这是为什么?

        首先判断的过程就是一种计算,属于逻辑运算,由CPU来做。tickets变量是在内存中的变量, 要把数据移动到CPU的eax寄存器中用于逻辑计算

        现实情况是,每个线程都要这样执行上面的逻辑,但cpu的寄存器只有一套,但是寄存器中的数据有多套。(由于ticket的读取和修改操作不是原子的(即,它们被分成了两步:读取和写入)有一种情况,当一个线程正走到以上逻辑的第二步时,正准备判断,此时这个线程被切换了,一旦被切换了当前线程的数据都会被带走,回来的时候,会恢复!

        当票数为1时,a线程会做判断,符合逻辑进入if,走到usleep语句;此时b线程也进来来,a将寄存器中的数据带走,此时b线程见到的票数也是1,b线程也符合逻辑,进入if,也会走到usleep;同样的c和d线程都会做以上线程的动作,都会进入if。当a过了usleep时间,会执行--操作(1.重读数据2.--数据3.写回数据),此时票数为0了,同样的b,c,d线程也会做--,因为它们已经进入了if中。最后就导致票数为-2的情况了。


如何解决这样的问题? 

        加锁!


 2.加锁

2.1.认识加锁和它的接口

1.设置锁的接口:pthread_mutex_init

  • mutex 指向要初始化的互斥锁对象的指针。
  • attr 是指向互斥锁属性的指针,如果设置为 NULL,则使用默认属性。互斥锁属性允许你定制互斥锁的行为,比如设置互斥锁的类型为递归锁(recursive mutex),但在大多数情况下,使用默认属性就足够了。

        如果 pthread_mutex_init 函数调用成功,它会返回 0。如果发生错误,它会返回一个非零的错误码。

phread_mutex_t是互斥锁类型:任何时刻只允许一个线程进行资源访问。

        如果你是定义的全局的或者静态的(即它在编译时就已经分配了内存空间)你也可以考虑使用PTHREAD_MUTEX_INITIALIZER宏来静态初始化它,这样可以避免在运行时调用pthread_mutex_init,在这种情况下不需要destroy,进程结束后会自动释放。但是,请注意,静态初始化通常只适用于静态分配的互斥锁,并且不能用于动态分配的互斥锁。

        但是,如果你使用的是动态分配的互斥锁(例如,通过malloccalloc分配的),或者出于某种原因你希望在运行时控制互斥锁的初始化(例如,基于某些条件决定是否初始化),那么你就必须调用pthread_mutex_init

2. 同时使用接口:pthread_mutex_destroy来释放锁

#include <pthread.h>  int pthread_mutex_destroy(pthread_mutex_t *mutex);
  • mutex 指向要销毁的互斥锁对象的指针。

        如果 pthread_mutex_destroy 函数调用成功,它会返回 0。如果发生错误(例如,如果传入的互斥锁未初始化),它会返回一个非零的错误码。


3.加锁的接口:pthread_mutex_lock

        用于对互斥锁(mutex)进行加锁操作。当一个线程调用 pthread_mutex_lock 并成功获取锁时,它就可以安全地访问或修改被该锁保护的共享数据,而不用担心其他线程同时访问这些数据。

  • mutex 指向要锁定的互斥锁对象的指针。

        如果 pthread_mutex_lock 调用成功,它会返回 0,表示线程已经成功获取了锁。如果调用失败(例如,因为另一个线程已经持有了该锁,并且调用了线程在等待锁时被中断或取消了),它会返回一个错误码。

4.解锁的接口:pthread_mutex_unlock

        当一个线程完成对共享数据的访问或修改后,它应该调用 pthread_mutex_unlock 来释放锁,以便其他线程可以获取该锁并访问共享数据。

  • mutex 指向要解锁的互斥锁对象的指针。

如果 pthread_mutex_unlock 调用成功,它会返回 0,表示锁已经被成功释放。如果调用失败(例如,因为传入的互斥锁未初始化或当前线程并未持有该锁),它会返回一个错误码。


2.2用一下接口

2.2.1 解决抢票问题

        从临界区的角度来看,上面的问题涉及到多线程程序中对共享资源(即ticket变量)的访问冲突。临界区是指访问共享资源并执行某些操作的代码段,这些操作在并发执行时可能会相互干扰,导致数据竞争或不一致。

        每个线程都有一个循环,该循环试图检查ticket变量的值,如果大于0,则减少它并打印。这个检查和减少的操作组合起来就构成了一个临界区,因为多个线程可能同时试图执行这个操作。

        没有同步机制(如互斥锁)的情况下,临界区内的操作可能会以不可预测的方式交错执行,导致数据竞争(不一致性)。

        为了解决这个问题,我们需要确保在任何给定时间,只有一个线程可以进入临界区并执行其中的操作。这可以通过使用互斥锁(mutex)来实现。(由并行操作转为串行操作)

        所谓对临界资源进行保护,本质是对临界区进行保护!我们对所有资源进行访问吗,本质是通过代码进行访问!保护资源,本质就是想办法把访问资源的代码保护起来!

        1.加锁是将并行改为串行的一种方法,所以一定要搞清楚临界区在哪里,枷锁的范围(代码行数),粒度一定要尽量的小(串行跨度过长是会导致多线程情况下的效率降低)

这一步就完成了加锁和解锁

int tickets = 10000;
//设置锁
pthread_mutex_t gmutex=PTHREAD_MUTEX_INITIALIZER;
void route(const std::string &name)
{while(true){//加锁pthread_mutex_lock(&gmutex);if(tickets > 0){// 抢票过程usleep(1000); // 1ms -> 抢票花费的时间printf("who: %s, get a ticket: %d\n", name.c_str(), tickets);tickets--;//执行完抢票逻辑后解锁pthread_mutex_unlock(&gmutex);}else{pthread_mutex_unlock(&gmutex);break;}}
}

运行结果:减到1后停止抢票,完成了抢票逻辑的加锁和解锁

        2.至此任何线程,要进行抢票,都得先申请锁,不应该有例外

        3.所有线程申请锁,前提是所有线程都得看到这把锁,锁本身也是共享资源---加锁的过程必须是原子的!(不能是我在申请锁的时候,别人也在申请)

        4.原子性:指的是一个操作(或一组操作)在执行过程中不可分割,即要么全部执行成功,要么全部不执行,中间不会被其他操作或事件打断。(对于任何一个线程在抢票的时候,要么抢到了,要么没抢到,不存在正在抢票的状态)

        5.如果线程申请锁失败了,我的线程就要被阻塞。

        6.如果线程申请锁成功了,继续向后运行。(申请成功的锁先进行串行操作,完成之后,其它线程才从阻塞态被唤醒,执行该操作,这样并行就变成串行了)

        7.如果一个线程申请成功了,执行临界区的代码了,在执行临界区代码期间,该线程可以被切换走吗?可以切换,但是其它线程无法进入临界区,因为该线程并没有释放锁!!!该线程可以放心的执行完毕临界区的代码,没有人能打扰!

        结论:所以对于其它线程来说,要么我没申请锁,要么我释放了锁,对其他线程才有意义!我访问临界区,对于其它线程是原子的!


2.2.2设置局部锁

我们使用ThreadData接收参数,包括锁的接收,这样每一个线程都能看到同一个锁了

#pragma once
#include <iostream>
#include <string>
#include <pthread.h>namespace ThreadMoudle
{//传递的参数class ThreadData{public:ThreadData(const std::string &name, pthread_mutex_t *lock):_name(name), _lock(lock){}public:std::string _name;pthread_mutex_t *_lock;};// 线程要执行的方法,后面我们随时调整typedef void (*func_t)(ThreadData *td); // 函数指针类型class Thread{public:void Excute(){std::cout << _name << " is running" << std::endl;_isrunning = true;_func(_td);_isrunning = false;}public:Thread(const std::string &name, func_t func, ThreadData *td):_name(name), _func(func), _td(td){std::cout << "create " << name << " done" << std::endl;}static void *ThreadRoutine(void *args) // 新线程都会执行该方法!{Thread *self = static_cast<Thread*>(args); // 获得了当前对象self->Excute();return nullptr;}bool Start(){int n = ::pthread_create(&_tid, nullptr, ThreadRoutine, this);if(n != 0) return false;return true;}std::string Status(){if(_isrunning) return "running";else return "sleep";}void Stop(){if(_isrunning){::pthread_cancel(_tid);_isrunning = false;std::cout << _name << " Stop" << std::endl;}}void Join(){::pthread_join(_tid, nullptr);std::cout << _name << " Joined" << std::endl;delete _td;}std::string Name(){return _name;}~Thread(){}private:std::string _name;pthread_t _tid;bool _isrunning;func_t _func; // 线程要执行的回调函数ThreadData *_td;//线程参数};
} 

        这样,每个线程都获取了局部锁的地址,在每个线程在执行抢票逻辑的时候,将锁的地址传给加锁函数,就能实现加锁了。

#include <iostream>
#include <vector>
#include <cstdio>
#include <unistd.h>
#include "Thread.hpp"using namespace ThreadMoudle;int tickets = 10000;
void route(ThreadData *td)
{while (true){pthread_mutex_lock(td->_lock);if (tickets > 0){// 抢票过程usleep(1000); // 1ms -> 抢票花费的时间printf("who: %s, get a ticket: %d\n", td->_name.c_str(), tickets);tickets--;pthread_mutex_unlock(td->_lock);}else{pthread_mutex_unlock(td->_lock);break;}}
}
static int threadnum =4;int main()
{//创建局部锁pthread_mutex_t mutex;pthread_mutex_init(&mutex, nullptr);std::vector<Thread> threads;for(int i = 0; i < threadnum; i++){std::string name = "thread-" + std::to_string(i+1);//创建锁后,把锁的地址给到td对象,再将td给到ThreadThreadData *td = new ThreadData(name, &mutex);threads.emplace_back(name, route, td);}for(auto &thread : threads){thread.Start();}for(auto &thread : threads){thread.Join();}//释放锁pthread_mutex_destroy(&mutex);}

进一步对锁进行封装:

#pragma once#include <pthread.h>class LockGuard
{
public:LockGuard(pthread_mutex_t *mutex):_mutex(mutex){pthread_mutex_lock(_mutex);}~LockGuard(){pthread_mutex_unlock(_mutex);}
private:pthread_mutex_t *_mutex;
};

route函数就可以这样写:由于while区域是一个代码块,进入时调用构造,当条件判断结束时:自动调用析构

void route(ThreadData *td)
{while (true){LockGuard lockguard(td->_lock); // RAII风格的锁if (tickets > 0){// 抢票过程usleep(1000); // 1ms -> 抢票花费的时间printf("who: %s, get a ticket: %d\n", td->_name.c_str(), tickets);tickets--;}else{break;}}
}


2.3从原理角度理解锁

如何理解申请锁成功,允许你进入临界区。

        申请锁成功,pthread_mutex_lock函数会返回。

如何理解申请锁失败,不允许你进入临界区。

        申请锁失败,pthread_mutex_lock函数不返回,线程就阻塞了。(锁没有就绪)

        pthread_mutex_lock函数和线程都属于pthread库,函数内部实现时就是一个判断,没有申请成功那么就将线程设置成阻塞状态。如果有线程pthread_mutex_unlock了,那么被阻塞的线程在pthread_mutex_lock内部就会被重新唤醒,重新申请锁,申请成功走上面逻辑,申请失败继续阻塞等待。


2.4锁是如何实现的****

        经过上面的例子,大家已经意识到单纯的 i++ 或者 ++i 都不是原子的,有可能会有数据一致性问题
        为了实现互斥锁操作,大多数体系结构都提供了swap或exchange指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性,即使是多处理器平台,访问内存的 总线周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。 现在我们把lock和unlock的伪代码改一下

        al可看成一个寄存器,把0movb到al中,xchgb将内存中的变量与寄存器中的做了直接的交换,不需要中间变量。(比如说lock一开始的数据是1表示锁没有被申请,0表示锁被申请了)

        1.CPU的寄存器只有一套,被所有的线程共享。但是寄存器中的数据,属于执行流上下文,属于执行流私有的数据!

        2.CUP在执行代码的时候,一定要有对应的执行载体 --线程&&进程。

        3.数据在内存中,是被所有线程所共享的

        据1,2,3可知:把数据从内存移动到寄存器,本质是吧数据从共享,变成线程私有!!!

        线程执行判断,如果al中的内容>0,则申请锁成功然后返回,否则挂起等待,等待完成被唤醒,goto lock重新申请锁。

        基于以上代码我们来理解一下加锁:

        线程 A执行第一行代码,此时%al寄存器中为0;执行第二条代码此时内存中lock中的数据与%al继续交换,所有%al中值为1,lock的值为0;当线程A执行第三行代码的时候被切换走,因此线程A会保存上下文,带走%al中的数据,此时线程A处在第三行。

        此时线程B走第一行和第二行代码,由于内存中lock的值为0,交换之后%al的值还是0。所以当线程B执行到第3行代码的时候只能跳到第6行,线程B被挂起等待,线程B下次被唤醒的时候将执行第7行代码,goto lock重新申请锁。

        线程B被挂起,线程A被切回,继续从第三行开始执行,进入if,调用接口pthread_mutex_lock,return 0表示加锁成功,进入临界区。所以此时线程A称之为:申请锁成功。在这个过程中加锁就是执行第二行代码:xchgb,只有一条汇编代码,交换不是拷贝,只有一个“1”,持有1的,就表示持有锁!当线程下次想申请锁的时候就要把%al清空了。

        解锁就是把“1”还回去,内存中的lock由0变为1.

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

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

相关文章

mybatis-plus LambdaQueryWrapper条件构造器使用apply查询含有逗号‘,‘分隔的字段

mybatis-plus LambdaQueryWrapper apply自定义SQL条件使用 真实业务开发使用场景 数据库:postgreSQL 开发场景:SpringBoot2.5.7 数据库中数据如下 需求 查找, bingBdNumber字段为422875,要求数据库中bind_bd_number这一栏中只要有422875的就返回. 根据上图查422875需要返回…

【python】导入serial模块,读写串口数据(嵌入式软件自动化测试)

目录 环境准备安装pyserial硬件连接 基本函数使用获取串口设备列表初始化串口对象发送数据读取数据 环境准备 安装pyserial 1、python 3.6.7 2、pyserial模块封装了python对串口的访问 pip install serialpip install pyserial如果代码报错卸载掉serial 硬件连接 1、将串口…

Android 10.0 SystemUI下拉状态栏固定展开QsPanel不收缩功能实现

1. 前言 在10.0的系统ROM产品定制化开发中,在systemUi的原生下拉状态栏中,首次下拉展开quickQsPanel,第二次展开就显示 QsPanel,在产品开发中,需要下拉状态栏固定展开QsPanel,不需要二次展开,接下来分析下相关功能的实现,如图: 2.SystemUI下拉状态栏固定展开QsPanel不收…

Python和C++行人轨迹预推算和空间机器人多传感融合双图算法模型

&#x1f3af;要点 &#x1f3af;双图神经网络模型&#xff1a;最大后验推理和线性纠错码解码器 | &#x1f3af;重复结构和过约束问题超图推理模型 | &#x1f3af;无向图模型变量概率计算、和积消息传播图结构计算、隐马尔可夫模型图结构计算、矩阵图结构计算、图结构学习 |…

基于Java技术的致远汽车租赁系统

你好呀&#xff0c;我是计算机学姐码农小野&#xff01;如果有相关需求&#xff0c;可以私信联系我。 开发语言&#xff1a;Java 数据库&#xff1a;MySQL 技术&#xff1a;B/S模式、Java技术 工具&#xff1a;Visual Studio、MySQL数据库开发工具 系统展示 首页 用户注册…

现场可重构CPLD芯片应用案例—蓝牙音箱

我司英尚微提供的高性能数模混合现场可重构IC、通用可配置的模数混合芯片内部集成丰富的模拟资源和数字资源&#xff0c;可轻松替代电路中的各种标准器件&#xff0c;并按照客户要求组合成最优小型ASIC&#xff0c;缩短开发周期&#xff0c;降低成本。下面介绍LS98002现场可重构…

object-C 解答算法:移动零(leetCode-283)

移动零(leetCode-283) 题目如下图:(也可以到leetCode上看完整题目,题号283) 解题思路: 本质就是把非0的元素往前移动,接下来要考虑的是怎么移动,每次移动多少? 这里需要用到双指针,i 记录每次遍历的元素值, j 记录“非0元素值”需要移动到的位置; 当所有“非0元素值”都移…

链表面试练习习题(Java)

1. 思路&#xff1a; 创建两个链表&#xff0c;一个用来记录小于x的结点&#xff0c;一个用来记录大于等于x的结点&#xff0c;然后遍历完原链表后&#xff0c;将小于x的链表和大于等于x的链表进行拼接即可 public class Partition { public ListNode partition(ListNode pH…

如何将PDF转换成可以直接编辑的CAD图纸?

PDF图纸是为了让用户更好的阅览CAD文件&#xff0c;但是&#xff0c;当我们想要对其进行编辑的时候&#xff0c;PDF图纸就是一个麻烦了。那么PDF转换成CAD后可以编辑吗&#xff1f;如何将PDF转换成可以直接编辑的CAD图纸呢&#xff1f;本篇给你答案。 1、启动迅捷CAD编辑器&…

linux进行redis的安装并使用RDB进行数据迁移

现在有两台电脑&#xff0c;分别是A&#xff0c;B&#xff0c;现在我要把A电脑上的redis的数据迁移到B电脑上&#xff0c;B电脑上是没有安装redis的 1.找到A电脑的redis的版本 1.先启动A电脑的redis&#xff0c;一般来说&#xff0c;都是直接在linux的控制台输入&#xff1a;re…

Spring Cloud Loadbalancer 的使用

一、默认负载均衡策略 Spring Cloud LoadBalancer 默认的负载均衡策略是轮询。 轮询效果示例 我们需要示例一个请求分发到不同的模块上&#xff0c;所以我们需要创建多模块项目。 新建 Spring Boot &#xff08;3.0.2&#xff09;的 Maven 项目&#xff08;JDK 17&#xff09…

万界星空科技MES系统生产计划管理的功能

MES系统&#xff08;Manufacturing Execution System&#xff0c;制造执行系统&#xff09;的生产计划管理功能是其核心功能之一&#xff0c;旨在将企业的生产计划转化为实际的生产操作&#xff0c;并通过实时监控和调整来确保生产活动的顺利进行。以下是MES系统生产计划管理功…

STM32智能环境监测系统教程

目录 引言环境准备智能环境监测系统基础代码实现&#xff1a;实现智能环境监测系统 4.1 数据采集模块 4.2 数据处理与控制模块 4.3 通信与网络系统实现 4.4 用户界面与数据可视化应用场景&#xff1a;环境监测与管理问题解决方案与优化收尾与总结 1. 引言 智能环境监测系统通…

基于STM32智能电子锁设计

1.简介 随着时代的高速发展&#xff0c;家居安全也成为人们日常生活中的一个安全问题。目前传统的门锁使用的是机械密码&#xff0c;在安全性方面表现不佳。这些缺点可以通过改用智能电子密码锁来弥补。智能电子锁是一种使用了现代电子技术的高科技产品&#xff0c;它的出现解决…

ActiveMQ配置延迟投递和定时投递教程

配置activemq.xml中的<broker>标签添加schedulerSupport"true" schedulerSupport"true"更改完成重启生效 四大属性解释 Property nametypedescriptionAMQ_SCHEDULED_DELAYlong延迟投递的时间AMQ_SCHEDULED_PERIODlong重复投递的时间间隔AMQ_SCHEDU…

期权末日双买跨式策略-这才是末日轮稳定赚钱的方法吗?!

今天带你了解期权末日双买跨式策略-这才是末日轮稳定赚钱的方法吗&#xff1f;&#xff01;期权末日双买跨式策略是一种在期权到期日前预期市场会出现大幅波动时使用的策略。 期权双买跨式策略适合期权末日轮是因为它能利用临近到期日时市场潜在的大幅波动来获利。末日轮期权&…

AI数字人+数字孪生IOC智慧运营平台:提升业务场景智慧化运维水平

在人工智能时代&#xff0c;“AI数字人数字孪生IOC智慧运营平台”&#xff0c;不仅能够提升数字孪生系统的人机交互体验&#xff0c;还能实现高效的运维管理&#xff0c;可以有效推动多领域场景数字化转型和智能化升级。 案例分享 深圳新一代产业园NEXT PARK交流中心 深圳新一…

【中项第三版】系统集成项目管理工程师 | 第 5 章 软件工程① | 5.1 - 5.3

前言 第5章对应的内容选择题和案例分析都会进行考查&#xff0c;这一章节属于技术的内容&#xff0c;学习要以教材为准。 目录 5.1 软件工程定义 5.2 软件需求 5.2.1 需求的层次 5.2.2 质量功能部署 5.2.3 需求获取 5.2.4 需求分析 5.2.5 需求规格说明书 5.2.6 需求变…

使用C#实现无人超市管理系统——数据结构课设(代码+PPT+说明书)

说明&#xff1a;这是自己做的课程设计作业&#xff0c;得分情况98/100 如果想要获取私信我 本项目采用线性表中的链表来进行本次系统程序的设计。链表分为两条线&#xff0c;分别是存储用户信息和商品信息&#xff0c;并且都设为公共属性&#xff0c;方便对用户信息和商品信息…

C#数字医学影像系统(RIS/PACS)源码,Oracle数据库,C/S架构,运行稳定

数字医学影像系统&#xff08;RIS/PACS&#xff09;源码&#xff0c;三甲以下的医院都能满足。PACS 系统全套成品源码。 开发技术&#xff1a;C/S架构&#xff0c;C#开发语言&#xff0c;数据库服务器采用Oracle数据库。 医学影像存储与传输系统&#xff0c;融合了医学信息化…