【Linux】——线程安全

目录

关于线程进程的问题

可重入与线程安全

常见的线程安全的情况

常见的不可重入的情况

常见的可重入的情况

可重入与线程安全区别

可重入与线程安全联系

Linux线程互斥

进程线程间的互斥相关概念

互斥量mutex

互斥量mutex常用接口

互斥量改造抢票系统

 互斥量的原理

常见锁的概念

死锁

死锁的四个必要条件

避免死锁的方法 


关于线程进程的问题

可重入与线程安全

概念

  • 线程安全: 多个线程并发同一段代码时,不会出现不同的结果。常见对全局变量或者静态变量进行操作,并且没有锁保护的情况下,会出现线程安全问题。
  • 重入: 同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则是不可重入函数。

注意: 线程安全讨论的是线程执行代码时是否安全,重入讨论的是函数是否能被重新进入

常见的线程安全的情况

  • 每个线程对全局变量或者静态变量只有读取的权限,而没有写入的权限,一般来说这些线程是安全的。
  • 类或者接口对于线程来说都是原子操作。
  • 多个线程之间的切换不会导致该接口的执行结果存在二义性。

常见的不可重入的情况

  • 调用了malloc/free函数,因为malloc函数是用全局链表来管理堆的。
  • 调用了标准I/O库函数,标准I/O库的很多实现都以不可重入的方式使用全局数据结构。
  • 可重入函数体内使用了静态的数据结构。

常见的可重入的情况

  • 不使用全局变量或静态变量。
  • 不使用用malloc或者new开辟出的空间。
  • 不调用不可重入函数。
  • 不返回静态或全局数据,所有数据都有函数的调用者提供。
  • 使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据。

可重入与线程安全区别

  • 可重入函数是线程安全函数的一种。
  • 线程安全不一定是可重入的,而可重入函数则一定是线程安全的。
  • 如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数若锁还未释放则会产生死锁,因此是不可重入的。

可重入与线程安全联系

  • 函数是可重入的,那就是线程安全的。
  • 函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题。
  • 如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的。

Linux线程互斥

进程线程间的互斥相关概念

  • 临界资源: 多线程执行流共享的资源叫做临界资源。
  • 临界区: 每个线程内部,访问临界资源的代码,就叫做临界区。
  • 互斥: 任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用。
  • 原子性: 不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成。

临界资源和临界区

进程之间如果要进行通信我们需要先创建第三方资源,让不同的进程看到同一份资源,由于这份第三方资源可以由操作系统中的不同模块提供,于是进程间通信的方式有很多种。进程间通信中的第三方资源就叫做临界资源,访问第三方资源的代码就叫做临界区。

多线程的大部分资源都是共享的,线程之间进行通信不需要费那么大的劲去创建第三方资源。

比如下面的代码:

#include<stdio.h>
#include<unistd.h>
#include<pthread>int count = 0;void* threadRun(void* arg)
{while (1){count++;sleep(1);}pthread_exit((void*)0);
}int main()
{pthread_t tid;pthread_create(&tid, NULL, threadRun, NULL);while(1){printf("count: %d\n", count);sleep(1);}pthread_join(tid, NULL);return 0;
}

运行结果如下:

此时我们相当于实现了主线程和新线程之间的通信,其中全局变量count就叫做临界资源,因为它被多个执行流共享,而主线程中的printf和新线程中count++就叫做临界区,因为这些代码对临界资源进行了访问。

互斥和原子性 

在多线程情况下,如果这多个执行流都自顾自的对临界资源进行操作,那么此时就可能导致数据不一致的问题。解决该问题的方案就叫做互斥,互斥的作用就是,保证在任何时候有且只有一个执行流进入临界区对临界资源进行访问。

原子性指的是不可被分割的操作,该操作不会被任何调度机制打断,该操作只有两态,要么完成,要么未完成。

例如,下面我们模拟实现一个抢票系统,我们将记录票的剩余张数的变量定义为全局变量,主线程创建四个新线程,让这四个新线程进行抢票,当票被抢完后这四个线程自动退出。

//抢票系统测试互斥和原子性操作
//假设一共有tickets张票,共有t1、t2、t3、t4四个线程一起抢票
int tickets = 100;
void* Ticket(void* arg)
{char* name = (char*)arg;while (1){if (tickets > 0){usleep(10000);printf("[%s] get a ticket, left: %d\n", name, --tickets);}else{break;}}printf("%s quit!\n", name);pthread_exit((void*)0);
}
int main()
{pthread_t t1, t2, t3, t4;pthread_create(&t1, NULL, Ticket, (void*)"thread 1");pthread_create(&t2, NULL, Ticket, (void*)"thread 2");pthread_create(&t3, NULL, Ticket, (void*)"thread 3");pthread_create(&t4, NULL, Ticket, (void*)"thread 4");pthread_join(t1, NULL);pthread_join(t2, NULL);pthread_join(t3, NULL);pthread_join(t4, NULL);return 0;
}

运行结果如下:

【bug出现的原因】: 

  • if语句判断条件为真以后,代码可以并发的切换到其他线程。
  • usleep用于模拟漫长业务的过程,在这个漫长的业务过程中,可能有很多个线程会进入该代码段。
  • --ticket操作本身不是原子操作

原子操作的本质

为什么说--ticket不是一个原子操作,因为我们对一个变量进行--操作时,实际需要进行三个步骤,而真正的原子操作是不可被分割的操作,该操作不会被任何调度机制打断,该操作只有两态,要么完成,要么未完成。

--操作具体过程:

  1. load:将共享变量tickets从内存加载到寄存器中。
  2. updata:更新寄存器里面的值,执行-1操作。
  3. store:将新值从寄存器写回共享变量tickets的内存地址。

 

既然--操作实际需要三个步骤才能完成,那么就有可能当thread1刚把tickets的值读进CPU就被切走了,也就是从CPU上剥离下来,假设此时thread1读取到的值就是1000,而当thread1被切走时,寄存器中的100叫做thread1的上下文信息,因此需要被保存起来,之后thread1就被挂起了。

  

假设此时thread2被调度了,由于thread1只进行了--操作的第一步,因此thread2此时看到tickets的值还是100,而系统给thread2的时间片可能较多,导致thread2一次性执行了100次--才被切走,最终tickets由100减到了0。 

此时系统再把thread1恢复上来,恢复的本质就是继续执行thread1的代码,并且要将thread1曾经的硬件上下文信息恢复出来,此时寄存器当中的值是恢复出来的100,然后thread1继续执行--操作的第二步和第三步,最终将99写回了内存。

 在上述过程中,thread1抢了1张票,thread2抢了100张票,而此时剩余的票数却是99,也就相当于多出了100张票。

互斥量mutex

  • 大部分情况,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况变量归属单个线程,其他线程无法获得这种变量。
  • 但有时候,很多变量都需要在线程间共享,这样的变量成为共享变量,可以通过数据的共享,完成线程之间的交互。
  • 多个线程并发的操作共享变量,就会带来一些问题。

要解决上述抢票系统的问题,需要做到三点:

  • 代码必须有互斥行为:当代码进入临界区执行时,不允许其他线程进入该临界区。
  • 如果多个线程同时要求执行临界区的代码,并且此时临界区没有线程在执行,那么只能允许一个线程进入该临界区。
  • 如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区。

要做到这三点,本质上就是需要一把锁,Linux上提供的这把锁叫互斥量。

互斥量mutex常用接口

初始化互斥量 

初始化互斥量的函数叫做pthread_mutex_init,该函数的函数原型如下:

int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);

参数说明:

  • mutex:需要初始化的互斥量。
  • attr:初始化互斥量的属性,一般设置为NULL即可。

返回值说明:

  • 互斥量初始化成功返回0,失败返回错误码。

调用pthread_mutex_init函数初始化互斥量叫做动态分配,除此之外,我们还可以用下面这种方式初始化互斥量,该方式叫做静态分配:

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

销毁互斥量

销毁互斥量的函数叫做pthread_mutex_destroy,该函数的函数原型如下:

int pthread_mutex_destroy(pthread_mutex_t *mutex);

参数说明:

  • mutex:需要销毁的互斥量。

返回值说明:

  • 互斥量销毁成功返回0,失败返回错误码。

销毁互斥量需要注意:

  • 使用PTHREAD_MUTEX_INITIALIZER初始化的互斥量不需要销毁。
  • 不要销毁一个已经加锁的互斥量。
  • 已经销毁的互斥量,要确保后面不会有线程再尝试加锁。

互斥量加锁

互斥量加锁的函数叫做pthread_mutex_lock,该函数的函数原型如下:

int pthread_mutex_lock(pthread_mutex_t *mutex);

参数说明:

  • mutex:需要加锁的互斥量。

返回值说明:

  • 互斥量加锁成功返回0,失败返回错误码。

调用lock函数时有以下情况需要注意:

  • 互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功
  • 发起函数调用时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量,那么pthread_mutex_lock调用会陷入阻塞(执行流被挂起),等待互斥量解锁。

互斥量解锁

互斥量解锁的函数叫做pthread_mutex_unlock,该函数的函数原型如下:

int pthread_mutex_unlock(pthread_mutex_t *mutex);

参数说明:

  • mutex:需要解锁的互斥量。

返回值说明:

  • 互斥量解锁成功返回0,失败返回错误码。

互斥量改造抢票系统

为了解决抢票系统的bug,我们可以在上述的抢票系统中引入互斥量,每一个线程要进入临界区之前都必须先申请锁,只有申请到锁的线程才可以进入临界区对临界资源进行访问,并且当线程出临界区的时候需要释放锁,这样才能让其余要进入临界区的线程继续竞争锁。

//抢票系统测试互斥和原子性操作
//假设一共有tickets张票,共有t1、t2、t3、t4四个线程一起抢票
//加锁版本,保证线程安全
int tickets = 100;
pthread_mutex_t mutex;void* Ticket(void* arg)
{char* name = (char*)arg;while (1){pthread_mutex_lock(&mutex);if (tickets > 0){usleep(10000);printf("[%s] get a ticket, left: %d\n", name, --tickets);pthread_mutex_unlock(&mutex);}else{pthread_mutex_unlock(&mutex);break;}}printf("%s quit!\n", name);pthread_exit((void*)0);
}
int main()
{pthread_mutex_init(&mutex, NULL);pthread_t t1, t2, t3, t4;pthread_create(&t1, NULL, Ticket, (void*)"thread 1");pthread_create(&t2, NULL, Ticket, (void*)"thread 2");pthread_create(&t3, NULL, Ticket, (void*)"thread 3");pthread_create(&t4, NULL, Ticket, (void*)"thread 4");pthread_join(t1, NULL);pthread_join(t2, NULL);pthread_join(t3, NULL);pthread_join(t4, NULL);pthread_mutex_destroy(&mutex);return 0;
}

运行结果: 

【互斥锁相关注意事项】

  • 在大部分情况下,加锁本身都是有损于性能的事,它让多执行流由并行执行变为了串行执行,这几乎是不可避免的。
  • 我们应该在合适的位置进行加锁和解锁,这样能尽可能减少加锁带来的性能开销成本。
  • 进行临界资源的保护,是所有执行流都应该遵守的标准,这时程序员在编码时需要注意的。

 互斥量的原理

加锁后的原子性体现在哪里?

引入互斥量后,当一个线程申请到锁进入临界区时,在其他线程看来该线程只有两种状态,要么没有申请锁,要么锁已经释放了,因为只有这两种状态对其他线程才是有意义的。

例如,图中线程1进入临界区后,在线程2、3、4看来,线程1要么没有申请锁,要么线程1已经将锁释放了,因为只有这两种状态对线程2、3、4才是有意义的,当线程2、3、4检测到其他状态时也就被阻塞了。

此时对于线程2、3、4而言,它们就认为线程1的整个操作过程是原子的。 

临界区内的线程可能进行线程切换吗?

临界区内的线程完全可能进行线程切换,但即便该线程被切走,其他线程也无法进入临界区进行资源访问,因为此时该线程是拿着锁被切走的,锁没有被释放也就意味着其他线程无法申请到锁,也就无法进入临界区进行资源访问了。

其他想进入该临界区进行资源访问的线程,必须等该线程执行完临界区的代码并释放锁之后,才能申请锁,申请到锁之后才能进入临界区。

锁是否需要被保护?

我们说被多个执行流共享的资源叫做临界资源,访问临界资源的代码叫做临界区。所有的线程在进入临界区之前都必须竞争式的申请锁,因此锁也是被多个执行流共享的资源,也就是说锁本身就是临界资源。

既然锁是临界资源,那么锁就必须被保护起来,但锁本身就是用来保护临界资源的,那锁又由谁来保护的呢?

锁实际上是自己保护自己的,我们只需要保证申请锁的过程是原子的,那么锁就是安全的。

如何保证申请锁的过程是原子的?

  • 为了实现互斥锁操作,大多数体系结构都提供了swap或exchange指令,该指令的作用就是把寄存器和内存单元的数据相交换。
  • 由于只有一条指令,保证了原子性,即使是多处理器平台,访问内存的总线周期也有先后,一个处理器上的交换指令执行时,另一个处理器的交换指令只能等待总线周期。

操作系统的工作原理:

操作系统一旦启动成功后就是一个死循环。
时钟是计算机中的一个硬件,时钟每隔一段时间会向操作系统发起一个时钟中断,操作系统就会根据时钟中断去执行中断向量表。
中断向量表本质上就是一个函数表,比如刷磁盘的函数、检测网卡的函数以及刷新数据的函数等等。
计算机不断向操作系统发起时钟中断,操作系统就根据时钟中断,不断地去执行对应的代码。
CPU有多个,但总线只有一套。CPU和内存都是计算机中的硬件,这两个硬件之间要进行数据交互一定是用线连接起来的,其中我们把CPU和内存连接的线叫做系统总线,把内存和外设连接起来的线叫做IO总线。
系统总线只有一套,有的时候CPU访问内存是想从内存中读取指令,有的时候是想从内存读取数据,所以总线是被不同的操作种类共享的。计算机是通过总线周期来区分此时总线当中传输的是哪种资源的。


常见锁的概念

死锁

死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所占用不会释放的资源而处于的一种永久等待状态。

单执行流也有可能产生死锁,如果某一执行流连续申请了两次锁,那么此时该执行流就会被挂起。因为该执行流第一次申请锁的时候是申请成功的,但第二次申请锁时因为该锁已经被申请过了,于是申请失败导致被挂起直到该锁被释放时才会被唤醒,但是这个锁本来就在自己手上,自己现在处于被挂起的状态根本没有机会释放锁,所以该执行流将永远不会被唤醒,此时该执行流也就处于一种死锁的状态。

例如,在下面的代码中我们让主线程创建的新线程连续申请了两次锁。

//单执行流死锁
pthread_mutex_t mutex;
void* Routine(void* arg)
{pthread_mutex_lock(&mutex);pthread_mutex_lock(&mutex);pthread_exit((void*)0);
}
int main()
{pthread_t tid;pthread_mutex_init(&mutex, NULL);pthread_create(&tid, NULL, Routine, NULL);pthread_join(tid, NULL);pthread_mutex_destroy(&mutex);return 0;
}

运行结果:

ps命令查看该进程时可以看到,该进程当前的状态时Sl+,其中的l实际上就是lock的意思,表示该进程当前处于一种死锁的状态。

阻塞 

进程运行时是被CPU调度的,换句话说进程在调度时是需要用到CPU资源的,每个CPU都有一个运行等待队列(runqueue),CPU在运行时就是从该队列中获取进程进行调度的。

在运行等待队列中的进程本质上就是在等待CPU资源,实际上不止是等待CPU资源如此,等待其他资源也是如此,比如锁的资源、磁盘的资源、网卡的资源等等,它们都有各自对应的资源等待队列。

死锁的四个必要条件

  • 互斥条件:一个资源每次只能被一个执行流使用
  • 请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放
  • 不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺
  • 循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系

避免死锁的方法 

  • 破坏死锁的四个必要条件
  • 加锁顺序一致
  • 避免锁未释放的场景
  • 资源一次性分配
  • 避免死锁算法:如死锁检测法、银行家算法

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

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

相关文章

Zookeeper基础操作

搭建Zookeeper服务器 windows下部署 下载地址: https://mirrors.cloud.tencent.com/apache/zookeeper/zookeeper-3.7.1/ 修改配置文件 打开conf目录&#xff0c;将 zoo_sample.cfg复制一份&#xff0c;命名为 zoo.cfg打开 zoo.cfg&#xff0c;修改 dataDir路径&#xff0c…

leetcode 452. 用最少数量的箭引爆气球

2023.8.2 本题思路先将二维数组points按照第一个维度排序&#xff0c; 然后初始化射箭数为1&#xff0c;因为题中提示说了最少有一个气球。 在遍历这些气球&#xff0c;看是否有重叠&#xff0c;如果没有重叠区域&#xff0c;射箭数&#xff1b;如果有重叠区域&#xff0c;更新…

java快速生成数据库表文档(HTML、DOC、MD)

在企业级开发中、我们经常会有编写数据库表结构文档的时间付出&#xff0c;关于数据库表结构文档状态&#xff1a;要么没有、要么有、但都是手写、后期运维开发&#xff0c;需要手动进行维护到文档中&#xff0c;很是繁琐&#xff0c;这里推荐一个开源项目&#xff1a;screw gi…

Spring源码面试题

Spring源码面试题 谈谈你对Spring框架的理解? Spring 是一个开源的应用程序框架&#xff0c;它起源于 Rod Johnson 在其著名的 Spring Framework 专著中提出的一个轻量级框架的观念。下面是 Spring 的发展历史&#xff1a; 2002 年&#xff0c;Rod Johnson 发表了他的专著 …

RISC-V 指令集介绍

1. 背景介绍 指令集从本质上可以分为复杂指令集&#xff08;Complex Instruction Set Computer&#xff0c;CISC&#xff09;和精简指令集&#xff08;Reduced Instruction Set Computer&#xff0c;RISC&#xff09;两种。复杂指令集的特点是能够在一条指令内完成很多事情。 指…

云安全攻防(四)之 云原生技术

云原生技术 容器技术 容器与虚拟化 虚拟化&#xff08;Virtualization&#xff09;和容器&#xff08;Container&#xff09;都是系统虚拟化的实现技术&#xff0c;可实现系统资源的”一虚多“共享。容器技术可以理解成一种”轻量的虚拟化“方式&#xff0c;此处的”轻量“主…

安全加固服务器

根据以下的内容来加固一台Linux服务器的安全。 首先是限制连续密码错误的登录次数&#xff0c;由于RHEL8之后都不再使用pam_tally.so和pam_tally2.so&#xff0c;而是pam_faillock.so 首先进入/usr/lib64/security/中查看有什么模块&#xff0c;确认有pam_faillock.so 因为只…

UG\NX 二次开发 相切面、相邻面的选择控件

文章作者&#xff1a;里海 来源网站&#xff1a;https://blog.csdn.net/WangPaiFeiXingYuan 简介&#xff1a; 有群友问“UFUN多选功能过滤面不能选择相切面或相邻面之类的吗&#xff1f;” 这个用Block UI的"面收集器"就可以&#xff0c;ufun函数是不行的。 效果&am…

Debeizum 增量快照

在Debeizum1.6版本发布之后&#xff0c;成功推出了Incremental Snapshot&#xff08;增量快照&#xff09;的功能&#xff0c;同时取代了原有的实验性的Parallel Snapshot&#xff08;并行快照&#xff09;。在本篇博客中&#xff0c;我将介绍全新快照方式的原理&#xff0c;以…

HTML+CSS+JavaScript:实现B站评论发布效果

一、需求 1、用户输入内容&#xff0c;输入框左下角实时显示输入字数 2、为避免用户输入时在内容左右两端误按多余的空格&#xff0c;在发送评论时&#xff0c;检测用户输入的内容左右两端是否带有空格&#xff0c;若有空格&#xff0c;发布时自动取消左右两端的空格 3、若用…

浏览器的同源策略 - 跨域问题

1.什么是跨域 跨域问题的实质是浏览器的同源策略造成的。浏览器同源策略是浏览器为 JavaScript 施加的限制。简单点说就是非同源会出现如下等限制&#xff1a; 无法访问其他源下的网页的 Cookies&#xff0c;Storage等&#xff1b;无法访问其他源下的DOM对象和 JS 对象;无法使…

java环境搭建 Ubuntu Linux

jdk的安装和配置环境变量 使用apt sudo apt install default-jdk若是安装成功了在终端输入java -version来查看是否安装成功 使用官网下载的jdk包 直接在百度上搜索jdk&#xff0c;选择图片这个 网址:jdk下载网址 若是arm就选择带有arm的&#xff0c;反之选择x64的&#…

vscode 通过mongoose 连接mongodb atlas

了解mongodb 的项目结构 1.代表集群名称 > 2.代表数据库名称>3.代表每个 collection名称 三者范围为从大到小的关系 &#xff08;一对多&#xff09;。每个集群有不同的连接地址、用户信息&#xff08;Database Access&#xff09;、ip配置信息&#xff08;Network Acce…

51单片机(普中HC6800-EM3 V3.0)实验例程软件分析 实验一 点亮第一个LED

目录 前言 一、原理图及知识点介绍 1.1、LED原理图 1.2、MCU51原理图 二、代码分析 知识点一&#xff1a;#include "reg52.h" //此文件中定义了单片机的一些特殊功能寄存器 知识点二&#xff1a;你知道sfr P0 0x80;是怎么来的呢为什么要赋值0x80&#xff…

vxworks文件系统分析

参考https://www.freebuf.com/articles/endpoint/335030.html 测试固件 https://service.tp-link.com.cn/detail_download_7989.html 固件提取 binwalk解压固件&#xff0c;在第一部分即为要分析的二进制文件&#xff0c;可以拖进ida分析 设置为arm小端字节序&#xff0c;点…

数组中出现次数超过一半的数字——剑指 Offer 39

文章目录 题目描述法一 哈希表法二 摩尔投票 题目描述 法一 哈希表 使用哈希映射&#xff08;HashMap&#xff09;来存储每个元素以及出现的次数。对于哈希映射中的每个键值对&#xff0c;键表示一个元素&#xff0c;值表示该元素出现的次数。 class Solution { public:int maj…

程序的编译和调试

gcc编译器 gcc(GNU Compiler Collection)是GNU推出的多平台编译器&#xff0c;可将C、C源程序编译连接成可执行文件&#xff0c;支持以下后缀&#xff1a; .c c语言源代码 .h 程序所包含的头文件 .i 已经预处理过的C源代码文件 .s 汇编语言源代码文件 .o 编译后的目标文件…

解决SVN或GIT忽略提交文件的问题

背景 使用IDEA 的SVN插件提交文件是总是会提交一些不需要提交的文件; 我们可以通过一些简单设置忽略这些文件。 git 在项目根目录新建文本文件&#xff0c;修改后缀为.gitignore 文件中添加内容 *.iml .project .gradle/ .idea/ target/ build/ .vscode/ .settings/ .facto…

Elasticsearch:语义搜索 - Semantic Search in python

当 OpenAI 于 2022 年 11 月发布 ChatGPT 时&#xff0c;引发了人们对人工智能和机器学习的新一波兴趣。 尽管必要的技术创新已经出现了近十年&#xff0c;而且基本原理的历史甚至更早&#xff0c;但这种巨大的转变引发了各种发展的“寒武纪大爆炸”&#xff0c;特别是在大型语…

SAP数据库表维护视图生成器的使用

在SAP中&#xff0c;经常需要自定义数据库表。而且可能需要人工维护数据库表中的数据&#xff0c;可以通过SM30进行维护数据&#xff1b;但是SM30事务的权限太大&#xff0c;不适宜将SM30直接分配&#xff1b;因此&#xff0c;可以通过给维护表分配事务代码&#xff0c;来达到控…