《Linux从练气到飞升》No.27 Linux中的线程互斥

🕺作者: 主页

我的专栏
C语言从0到1
探秘C++
数据结构从0到1
探秘Linux
菜鸟刷题集

😘欢迎关注:👍点赞🙌收藏✍️留言

🏇码字不易,你的👍点赞🙌收藏❤️关注对我真的很重要,有问题可在评论区提出,感谢阅读!!!

文章目录

    • 前言
    • 1 进程线程间的互斥相关背景概念
    • 2 线程安全
    • 3 线程不安全
      • 3.1 线程不安全(看看猪跑
    • 4 互斥量mutex
      • 4.1 为什么可能无法获得争取结果?
      • 4.2 怎么解决?
    • 5 互斥锁
      • 5.1 什么是互斥锁
      • 5.2 逻辑梳理
      • 5.3 加锁逻辑
    • 6 互斥锁的接口
      • 6.1 初始化互斥锁
      • 6.2 销毁互斥锁
      • 6.3 互斥量的加锁和解锁
      • 6.4 互斥锁的使用
    • 7 死锁
      • 7.1 死锁的定义
      • 7.2 死锁产生的条件
      • 7.3 预防死锁
      • 7.4 避免死锁
      • 7.5 死锁预防和死锁避免之间的区别
      • 7.6 避免死锁的算法

前言

当谈到多线程编程时,线程互斥是一个至关重要的概念。在多线程环境下,确保共享资源的安全访问是至关重要的,而线程互斥正是为此而设计的。通过线程互斥,我们能够确保在任意给定时间内,只有一个线程能够访问共享资源,从而避免竞态条件和数据损坏。

在本篇博客中,我们将探讨线程互斥的重要性、实现线程互斥的方法以及在实际编程中如何应用线程互斥来确保多线程程序的正确性和稳定性。通过深入了解线程互斥,我们可以更好地理解多线程编程中的关键概念,提高程序的可靠性和性能。

希望本篇博客能够帮助你更好地理解线程互斥,并为你在多线程编程中遇到的挑战提供一些思路和解决方案。让我们一起深入探讨线程互斥,为构建高效、稳定的多线程程序打下坚实的基础。

1 进程线程间的互斥相关背景概念

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

2 线程安全

多个线程并发同一段代码时不会出现不同的结果,多个执行流访问临界资源不会导致程序产生二义性。

  • 执行流:可以理解为线程
  • 访问:指的是对临界资源进行操作
  • 临界资源:指的是多个线程都可以访问到的资源
  • 临界区:代码操作临界资源的代码区域称之为临界区
  • 二义性:相同的代码,结果不唯一

3 线程不安全

不安全、和上面就相反喽

  1. 假设一个场景:
    假设有一个CPU,两个线程,线程A和线程B,线程A和线程B都要对全局变量i(10)进行++操作
  2. 假设线程A先运行,但是线程A将i的值读取到寄存器之后,就被线程切换了。(操作系统会保存线程A的程序计数器和上下文信息)
  3. 假设B线程运行,正常继续++操作,那么i的值在内存中就被修改增加1了
  4. 此时线程A切换回来了,怎么计算?内存中i的值是多少?
  5. 结论:此时的i最终结果还是11,明明加了两次,但是却不符合逻辑,这就是不安全
  6. 怎么解决?这就需要用到互斥了,每次只允许一个线程进入修改,这样就不会有这种情况了

6f44ffc80d154eb998e5aaf484f98d17.png

3.1 线程不安全(看看猪跑

在我们想买票的时候,如果有两个人同时下单,它会发生什么呢?票是怎么发放的,会不会有两个人买到同一张票的情况?会不会有票数为负的情况?
我们通过代码来模拟一下,如果线程不安全时,抢票的情况
代码如下:
我们让4个线程来循环获取ticket,模拟抢票

#include<iostream>
#include<pthread.h>
#include<unistd.h>
using namespace std;
int ticket=1000;

void* get_ticket(void* arg)
{
    while(1)
    {
        if(ticket>0)
        {
            cout<<"i am "<<pthread_self()<<" get a ticket,no:"<<ticket<<endl;
            ticket--;
        }
        else 
        {
            break;
        }
    }
    return NULL;
}
int main()
{
    pthread_t tid[4];
    for(int i=0;i<4;i++)
    {
        int ret=pthread_create(&tid[i],NULL,get_ticket,NULL);
        if(ret!=0)
        {
            cout<<"线程创建失败!"<<endl;
        }

    }
    for(int i=0;i<4;i++)
    {
        pthread_join(tid[i],NULL);
    }

    cout<<"pthread_join end!"<<endl;
    return 0;
}

结果如下:
image.png
可以看到出现了负数的情况
image.png
甚至出现了两个线程抢到同一张票的情况

这就是所谓的线程不安全的情况

4 互斥量mutex

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

之前的抢票模拟,可以看到出现了负数的票,但是我们的条件中清楚的要求>0,这是为什么?

4.1 为什么可能无法获得争取结果?

  • if语句判断条件为真后,代码可以并发的切换到其他线程
  • –ticket操作本身就不是一个原子操作
取出ticket--部分的汇编代码
objdump -d a.out > test.objdump
152 40064b: 8b 05 e3 04 20 00 mov 0x2004e3(%rip),%eax # 600b34 <ticket>
153 400651: 83 e8 01 sub $0x1,%eax
154 400654: 89 05 da 04 20 00 mov %eax,0x2004da(%rip) # 600b34 <ticket>
  • --ticket操作本身就不是一个原子操作,而是对应三条汇编指令:
    • load:将共享变量ticket从内存加载到寄存器中
    • update:更新寄存器里面的值,执行-1操作
    • store:将新值,从寄存器写回共享变量ticket的内存地址

4.2 怎么解决?

要解决上面的问题需要做到三点

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

要做到这三点,只需要一把锁就行了,这把锁叫做互斥锁
image.png

5 互斥锁

5.1 什么是互斥锁

  • 互斥锁的底层就是一个互斥量,而互斥量的本质就是一个计数器,这个计数器的本质就只有两种情况 0 和 1
  • 1 表示当前临界资源可以被访问
  • 0 表示当前临界资源不可以被访问

5.2 逻辑梳理

拿之前的抢票的情况来说,如果加上这个锁,当一个线程想去访问临界资源,他得先获取互斥锁,如果此时互斥锁的值为1,则说明它可以访问,反之则不能,如果它正在访问临界资源,此时有第二个线程想来访问临界资源,发现互斥锁为0,它就不能进入,只能等待互斥锁为1时才能进入访问。这就保证当前的临界资源在同一时刻只能被一个执行流访问了。
但是需要注意的是,如果多个线程访问临界资源的时候是互斥访问的属性,一定要在多个线程中进行同一把锁的加锁操作,这样每个线程在访问临界资源之前都要获取这把锁,若锁的值为1就可以访问,反之则不能访问;如果给线程A加锁,但是不给线程B加锁,就会导致线程不安全的情况。
ebbd81970a3349d286674c8ba418f532.png

5.3 加锁逻辑

加锁的时候会提前在寄存器的计数器中保存的一个值 0,而不管内存的计数器中保存的值为多少,都会将寄存器中保存到值 0 和内存计数器中保存的值进行交换,然后对寄存器中的值进行判断是否为 1 ,如果为 1 ,则能加锁,如果不为 1 ,则不能加锁。
5b6f639cab594553a10749ecc2fb39c8.png
为了实现互斥锁操作,大多数体系结构都提供了swap或exchange指令
该指令的作用是把寄存器和内存单元的数据相交换
由于只有一条指令,保证了原子性
即使是多处理器平台,访问内存的总线周期也有先后
一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。
现在我们把lock和unlock的伪代码给一下。
image.png

6 互斥锁的接口

6.1 初始化互斥锁

初始化互斥量有两种方法:

  1. 静态分配

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER

  1. 动态分配
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrictattr);

返回值及参数说明:

  • 返回值:如果初始化成功则返回0 失败则返回错误码
  • 参数 mutex:需要初始化的互斥量
  • 参数 attr:初始化互斥量的属性一般设置为NULL即可

6.2 销毁互斥锁

image.png
需要注意的是:

  • 使用静态初始化的互斥量不需要销毁
  • 不要销毁一个已经加锁互斥量
  • 已经销毁的互斥量要确保后面不会有线程再尝试加锁
int pthread_mutex_destroy(pthread_mutex_t *mutex)

6.3 互斥量的加锁和解锁

  1. 阻塞加锁接口
int pthread_mutex_lock(pthread_mutex_t *mutex);
  • 如果互斥锁变量当中的计数器的值为1,调用该接口,则加锁成功,该接口调用完毕,函数返回
  • 如果互斥锁变量当中的计数器的值为0,调用该接口,则调用该接口的执行流阻塞在当前接口内部
  1. 非阻塞加锁接口
int pthread_mutex_trylock(pthread_mutex_t *mutex);
  • 不管有没有加锁成功,都会返回,所以需要对加锁返回的结果进行判断,判断是否加锁,如果加锁成功,则操作临界资源,反之则需要循环获取互斥锁,直到拿到互斥锁
  1. 带有超时时间的加锁接口
int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex,const struct timespec *restrict abstime);

struct timespec{
	time_t tv_sec;//秒
	long tv_nsec;//纳秒
};
  • 需要搭配循环来使用,并且判断返回值
  1. 解锁接口
int pthread_mutex_unlock(pthread_mutex_t *mutex);
  • 不管是阻塞加锁/非阻塞加锁/带有超时时间的加锁,加锁成功的互斥锁都可以使用该接口进行加锁。

image.png

6.4 互斥锁的使用

  1. 什么时候使用初始化互斥锁?

先初始化互斥锁,再创建线程

  1. 什么时候使用销毁互斥锁?

在所有使用互斥锁的线程全部退出之后就可以销毁互斥锁

  1. 什么时候使用加锁?

线程访问临界资源之前进行加锁操作

  1. 什么时候使用解锁?

线程所有退出的地方进行解锁

  1. 加锁之后不解锁会发生什么?

以之前的抢票为例

#include<iostream>
#include<pthread.h>
#include<unistd.h>
using namespace std;
int ticket=1000;

pthread_mutex_t g_lock; //全局变量的互斥锁

void* get_ticket(void* arg)
{
    while(1)//1位置加锁还是在2位置加锁
    {
        pthread_mutex_lock(&g_lock);
        //pos1
        if(ticket>0)
        {
            cout<<"i am "<<pthread_self()<<" get a ticket,no:"<<ticket<<endl; 
            //pos2
            ticket--;
        }
        else 
        {
            //将下面解锁的注释去掉就是正确的代码
            //pthread_mutex_unlock(&g_lock);
            break;
        }
        //pthread_mutex_unlock(&g_lock);
    }
    return NULL;
}
int main()
{
    pthread_mutex_init(&g_lock,NULL);//初始化互斥锁
    pthread_t tid[4];
    for(int i=0;i<4;i++)
    {
        int ret=pthread_create(&tid[i],NULL,get_ticket,NULL);
        if(ret!=0)
        {
            cout<<"线程创建失败!"<<endl;
        }

    }
    for(int i=0;i<4;i++)
    {
        pthread_join(tid[i],NULL);
    }

    cout<<"pthread_join end!"<<endl;
    pthread_mutex_destroy(&g_lock);
    return 0;
}

结果:
image.png
我们可以看到它只获取一张票就不再往下执行了,陷入了死锁中
这是因为有一个工作线程加锁之后没有进行解锁,其他线程再次去获取锁时,互斥锁中计数器中的值还是0,就要被阻塞等待,所以加锁之后一定要记得解锁
image.png

7 死锁

前面我们讲如果不进行解锁会造成死锁现象,但是死锁是什么?现在我们就来讲讲

7.1 死锁的定义

死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所占用不会释放的资源而处于一种永久等待状态。
就像下图
线程A获取到互斥锁1,线程B获取到互斥锁2时,线程A和线程B同时还想获取对方手里的锁(线程A还想获取互斥锁2,线程B还想获取互斥锁1),此时就会导致死锁
image.png
代码实现看一下

#include<iostream>
#include<unistd.h>
#include<pthread.h>
using namespace std;

pthread_mutex_t lock1;
pthread_mutex_t lock2;

void* ThreadNum1(void* args)
{
    (void*)args;
    pthread_mutex_lock(&lock1);
    sleep(3);
    pthread_mutex_lock(&lock2);
    return NULL;
}
void* ThreadNum2(void* args)
{
    (void*)args;
    pthread_mutex_lock(&lock2);
    sleep(3);
    pthread_mutex_lock(&lock1);
    return NULL;
}
int main()
{
    pthread_mutex_init(&lock1,NULL);
    pthread_mutex_init(&lock2,NULL);

    pthread_t tid;
    int ret = pthread_create(&tid,NULL,ThreadNum1,NULL);
    if(ret < 0)
    {
        cout<<"thread1 create failed"<<endl;
        return 0;
    }

    ret = pthread_create(&tid,NULL,ThreadNum2,NULL);
    if(ret < 0)
    {
        cout<<"thread2 create failed"<<endl;
        return 0;
    }

    while(1)
    {
        ;
    }
    pthread_mutex_destroy(&lock1);
    pthread_mutex_destroy(&lock2);
    return 0;
}

结果:
image.png
image.png
可以看到发生了死锁的现象

7.2 死锁产生的条件

死锁的生成有四个必要的条件:

  • 互斥条件:一个资源只能被一个执行流使用。(一个互斥锁只能被一个执行流在同一时刻拥有)
  • 请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放。(就像前面的例子那样,各自都是各自所需要的资源,但是都不放手)
  • 不剥夺条件:一个执行流已获得的资源在未使用完之前不能强行剥夺
  • 循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系。(互相等待,无限循环)

7.3 预防死锁

想要预防死锁只要破坏死锁4个条件中的一个即可

  • 破坏死锁的四个必要条件
  • 加锁顺序一致
  • 避免锁未释放的场景
  • 资源一次性分配

7.4 避免死锁

它和死锁预防的差别很小,可以把它理解为死锁预防的一种特例。
死锁避免策略在允许三个必要条件存在的条件下,来确保永远不会达到死锁点。
死锁避免方法有:

  • 若一个进程的请求会导致死锁,那么不启动该进程。
  • 若一个进程增加的资源请求会导致死锁,则不允许这个资源的分配。

相比死锁预防策略,死锁避免策略并发性更强。但是在使用中也有诸多限制:

  • 必须事先声明每个进程请求的最大资源
  • 分配的资源数量必须是固定的
  • 在占有资源时,进程不能够退出
  • 所讨论的进程的执行顺序必须没有任何同步要求的限制

7.5 死锁预防和死锁避免之间的区别

编号比较项预防死锁避免死锁
1概念预防死锁至少阻止了发生死锁的必要条件之一。避免死锁确保系统不会进入不安全状态
2资源请求预防死锁所有的资源都是一起请求的。资源请求是根据可用的安全路径完成的。
3所需信息预防死锁不需要关于现有资源、可用资源和资源请求的信息避免死锁需要关于现有资源、可用资源和资源请求的信息
4过程通过限制资源请求过程和资源处理来防止死锁。避免死锁会自动考虑请求并检查它是否对系统安全。
5抢占有时,抢占会更频繁地发生。避免死锁在死锁避免中没有抢占。
6资源分配策略用于防止死锁的资源分配策略是保守的。防止死锁的资源分配策略并不保守。
7未来的资源请求预防死锁不需要知道未来的进程资源请求。避免死锁需要了解未来的进程资源请求。
8优点预防死锁不涉及任何成本,因为它只需使条件之一为假,这样就不会发生死锁。由于此方法动态工作以分配资源,因此没有系统未充分利用。
9缺点死锁预防设备利用率低。避免死锁会使进程阻塞太久。
10使用示例假脱机和非阻塞同步算法。使用银行家和安全算法。

7.6 避免死锁的算法

  • 死锁检测算法:推荐博客《死锁的处理策略——检测和解除》
  • 银行家算法:推荐博客《银行家算法及其代码实现》

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

小DEMO:在vue中自定义range组件

1、组件样式 2、使用 import cSlider from /components/c-slider/c-slider.vue<div class"range"><cSlider v-model"cScale" change"cScaleChange" :min"1" :max"10"/> </div> 3、组件代码 <templa…

5+单基因+免疫浸润,这篇肿瘤预后文章你值得拥有

今天给同学们分享一篇生信文章“Systematic analysis of the role of SLC52A2 in multiple human cancers”&#xff0c;这篇文章发表在Cancer Cell Int期刊上&#xff0c;影响因子为5.8。 结果解读&#xff1a; 多种人类癌症中SLC52A2的mRNA表达 首先&#xff0c;作者使用GT…

virtualbox基本配置

全屏模式调出热键 右边的 ctrl home 键 安装增强功能 注意&#xff1a;virtualbox 自带那个安装增强功能点击后是没有效果的 1、启动虚拟机 2、设备 3、分配虚拟光驱 4、选择虚拟盘 5、选择对应iso文件&#xff0c;文件下载路径 6、双击对应文件安装&#xff0c;默认配置…

多区域OSPF配置

配置命令步骤&#xff1a; 1.使用router ospf 进程ID编号 启用OSPF路由 2.使用network 直连网络地址 反掩码 area 区域号 将其归于对应区域 注意&#xff1a; 1.进程ID编号可任意&#xff08;1-65535&#xff09; 2.反掩码用4个255相减得到 3.area 0 为主干区域 4.连接不…

虚拟机第一次如何打开

1、将别人的虚拟机拷贝到自己的电脑盘里&#xff1b; 2、打开VMware&#xff0c;选择“打开虚拟机”&#xff1b; 3、选择拷贝的虚拟机里的.vmx文件&#xff1b; 4、选择“播放虚拟机”&#xff1b; 5、如果出现一个选择框&#xff0c;选“我已复制改虚拟机”即可。

Python 中的 tqdm() 方法

tqdm&#xff08;阿拉伯语"taqaddum"的缩写&#xff0c;意为"进展"&#xff09;是Python中一个用于在循环中显示进度条的库。它提供了一种简单而又灵活的方式来监测代码执行的进度&#xff0c;特别是在处理大量数据或耗时较长的任务时非常有用。 1、安装 …

golang 解析oracle 数据文件头

package mainimport ("encoding/binary""fmt""io""os" ) // Powered by 黄林杰 15658655447 // Usered for parser oracle datafile header block 1 .... // oracle 数据文件头块解析 // KCBlockStruct represents the structure of t…

什么是美颜SDK?美颜SDK对比评测

美颜SDK在视频直播中发挥着越来越重要的作用。为了实现实时、高质量的美颜效果&#xff0c;各种视频直播美颜SDK应运而生。本文将对这些技术进行深入解析与比较。 一、技术原理解析 深度学习技术通过大量的训练数据学习人脸特征&#xff0c;从而实现更为自然的美颜效果。传统…

干货分享:如何提高商城系统网页访问速度?

如今&#xff0c;商城系统网页的访问速度对于用户体验和销售业绩至关重要。用户对于网页的加载速度要求越来越高&#xff0c;一旦加载速度过慢&#xff0c;用户可能会选择离开&#xff0c;甚至转向竞争对手的网站。因此&#xff0c;提高商城系统网页的打开速度是每个电商运营者…

2023年【N1叉车司机】找解析及N1叉车司机复审模拟考试

题库来源&#xff1a;安全生产模拟考试一点通公众号小程序 2023年N1叉车司机找解析为正在备考N1叉车司机操作证的学员准备的理论考试专题&#xff0c;每个月更新的N1叉车司机复审模拟考试祝您顺利通过N1叉车司机考试。 1、【多选题】《中华人民共和国特种设备安全法》第八十三…

ROS1 学习11 坐标系tf 管理系统 简介及demo示例

坐标系是我们非常熟悉的一个概念&#xff0c;也是机器人学中的重要基础&#xff0c;在一个完整的机器人系统中&#xff0c;会存在很多坐标系&#xff0c;这些坐标系之间的位置关系该如何管理&#xff1f; ROS给我们提供了一个坐标系的管理神器——TF。 比如在机械臂形态的机器…

ubuntu 20.04+ORB_SLAM3 安装配库教程

目录 安装ros(如果只是运行ORB-SLAM3&#xff0c;可以跳过安装)0. ros 安装教程1. 安装opencv2. 安装Pangolin3. 安装Eigen34.安装Python & libssl-dev5.安装boost库6.安装ceres库&#xff08;不必须&#xff09;7.安装Sophus库&#xff08;不必须&#xff09;8. 安装g20库…

多线程概述及创建

什么是线程&#xff1f; 线程(thread)是一个程序内部的一条执行路径。 我们之前启动程序执行后&#xff0c;main方法的执行其实就是一条单独的执行路径。 程序中如果只有一条执行路径&#xff0c;那么这个程序就是单线程的程序。 多线程是什么&#xff1f; 多线程是指从软…

Linux 读写权限的配置

文章目录 Linux文件权限详解 一、文件权限二、修改文件访问权限的方法三、UMASK值四、三种特殊权限suid、sgid、sticky&#xff08;sticky权限工作环境中相对常用&#xff09;五、ACL访问控制列表六、文件权限操作的常用命令 Linux文件权限详解 Linux系统中不仅是对用户与组根…

Cesium 实战 - 加载水经微图下载资源问题 - 地球南北极有蓝圈,南北极空洞

Cesium 实战 - 加载水经微图下载资源问题 - 地球南北极有蓝圈 错误展示排错过程解决问题完整代码在线示例 在某个项目中&#xff0c;客户提到一个问题&#xff0c;即地球南北极会有一个蓝色的洞&#xff0c;经复现&#xff0c;确实有这个问题。见下图。 经过排查&#xff0c;最…

高难度工业废水处理有哪些注意事项

高难度工业废水处理的注意事项包括&#xff1a; 预处理&#xff1a;在将废水输送至污水处理系统前&#xff0c;应进行预处理&#xff0c;以减轻处理设备的负荷&#xff0c;提高处理效率。预处理可以包括去除废水中的悬浮物、沉淀物以及防止化学物质的沉积等。针对不同废水选择…

nestJs(三) 数据库

真正的服务往往包括数据存储。 本篇将介绍如何建立 NestJs 的数据库连接、并使用数据库联表查询。这样就就是完整的后台服务了。 开发准备 下载并安装 Mysql创建 school 库 create database school;3.安装 nestjs/typeorm typeorm mysql2 npm install --save nestjs/typeor…

浅了解下:运营商大数据如何挖掘电销同行网站,APP,精准获客 ?

今天我们要讲的是运营商精准大数据营销。运营商精准大数据营销只是精准营销的一种&#xff0c;精准营销筛选包含了电话营销这个词。那么电话营销如何通过运营商大数据找到精准的客户&#xff1f;电销如何通过大数据找到准确的客户来源&#xff1f; 在全网时代&#xff0c;大数…

echarts:graph图表拖拽节点

需求&#xff1a;实现一个可视化编辑器&#xff0c;用户可以添加节点&#xff0c;并对节点进行拖拽编辑等 实现期间碰到很多问题&#xff0c;特意记录下来&#xff0c;留待将来碰到这些问题的同学&#xff0c;省去些解决问题的时间 问题1&#xff1a;节点的data如下&#xff0…

无需管理底层基础设施,亚马逊云科技向量数据库轻松创建ML增强的搜索体验和应用程序

当我们进入一家图书馆时&#xff0c;图书馆的入口处会有几台电脑供你检索相关的书籍&#xff0c;你可以检索你想要的书籍的名字例如&#xff1a;《百年孤独》、《悲惨世界》等等&#xff0c;你也可以检索作者例如&#xff1a;川端康成、鲁迅、加缪等等&#xff0c;当然你也可以…