信号量(信号量操作 基于信号量实现的生产者消费者模型)

news2024/11/24 15:44:51

  本篇文章重点对信号量的概念,信号量的申请、初始化、释放、销毁等操作进行讲解。同时举例把信号量应用到生产者消费者模型来理解。希望本篇文章会对你有所帮助。

目录

一、信号量概念

1、1 什么是信号量

1、2 为什么要有信号量

1、3 信号量的PV操作

二、信号量的相关接口

2、1 sem_t

2、2 sem_init

2、3 sem_wait

2、4 sem_post

2、5 sem_destory

三、基于信号量的生产者消费者模型

3、1 信号量控制环形队列

3、1、1 空间资源和数据资源

3、1、2 保护原理(二元信号量)

3、2 demo代码

3、2、1 单生产与单消费

 3、2、2 多生产多消费


🙋‍♂️ 作者:@Ggggggtm 🙋‍♂️

👀 专栏:Linux从入门到精通  👀

💥 标题:信号量💥

 ❣️ 寄语:与其忙着诉苦,不如低头赶路,奋路前行,终将遇到一番好风景 ❣️

 

一、信号量概念

1、1 什么是信号量

  我们之前学了互斥锁和体哦阿健变量可以实现线程的互斥与同步。那么还有其他方法吗?信号量也可以做到!

  信号量(Semaphore)是操作系统中一种用于实现线程间同步与互斥的机制。它本质就是一个计数器,用于控制多个线程对共享资源的访问信号量可以被视为一个简单的整数变量,并且可以进行原子操作,包括等待(wait)和释放(signal)

1、2 为什么要有信号量

  信号量(Semaphore)是一种多线程同步的机制,用于解决并发环境中的资源竞争问题。在并发编程中,多个线程可能同时访问共享资源,如果不对资源进行合理的管理,就会导致数据不一致或错误的结果。

  我们在学习互斥锁时,一个线程在操作临界资源的时候,必须临界资源是满足条件的!可是公共资源是否满足生产或者消费条件,我们无法直接得知。因为你要检测,本质也是在访问临界资源。所以只能先加锁,再检测,再操作,再解锁。只要我们对资源进行整体加锁,就默认了我们对这个资源整体使用。但是,有时候会是一份临界资源同时访问不同的区域。这时互斥锁并不能很好的满足对临界资源的充分利用。在这种情况下就可以引入信号量来很好的解决。具体如下图:

  现在我们有一个共享资源,不当做一个整体,而让不同的执行流访问不同的区域的话,那么不就可以继续并发了。

1、3 信号量的PV操作

  这里会有一个疑问,我们怎么知道临界资源内部一共有少个区域资源呢?我们又怎么知道内部一定还有资源呢?实际上,一般都是外部就会提供有多少资源。同时,信号量一定会保证內部是否还有资源

  信号量本质是一个计数器。一个线程在申请信号量,本质就是在对信号量的 -- 操作(对剩余资源数量的减减操作)。只要拥有信号量,就在未来一定能够拥有临界资源的一部分。申请信号量的本质:对临界资源中特定小块资源的预订机制。信号量因此保证了只要你申请成功,就代表一定还有资源。如果申请失败,就会进入等待。这不就是我们在访问真正的临界资源之前,我们其实就可以提前知道临界资源的使用情况!!!就不用再进行复杂的加锁、判断、解锁等操作了

  此时发现,线程要访问临界资源中的某一区域,就得先申请信号量。所有人必须的先看到统一信号量。信号量本身必须是公共资源。

  对信号量的操作就是申请和释放操作。信号量本质就是一个计数器,也就是在对信号量进行++和-- 操作。申请资源,可以看成对sem--,同时必须保证操作的原子性,我们也称之为 P 操作。释放资源,可以看成sem++,也必须保证操作的原子性,称之为 V 操作 。信号量核心操作:PV操作

二、信号量的相关接口

2、1 sem_t

  sem_t 是在 POSIX 系统中用来实现信号量机制的类型。它是一个不透明的数据结构,用于控制多个进程或线程对共享资源的访问。

  sem_t 提供了三个主要的函数接口:

  1. sem_init:用于初始化一个信号量。该函数接受三个参数,分别是指向 sem_t 对象的指针、信号量的共享标志和初始值。共享标志指定信号量的共享方式,根据具体需求可以选择在进程间共享(设置为0)或者在同一进程内的线程间共享(设置为非0)。初始值表示信号量的初始计数值。

  2. sem_wait:该函数使调用线程等待信号量。如果信号量的计数值大于0,则将计数值减一,并立即返回。如果计数值为0,则线程将阻塞,直到信号量的计数值大于0。

  3. sem_post:该函数用于释放信号量。它将信号量的计数值加一,并唤醒因等待该信号量而阻塞的线程。

  4. sem_destroy:该函数是用于销毁一个已经初始化的信号量的函数,在使用完信号量后,通过调用该函数可以释放相关资源。

  下面我们来看这几个函数的详细解释。

2、2 sem_init

  sem_init函数是用于初始化一个信号量的函数,它在程序中创建一个新的信号量,并为其分配必要的资源。函数原型如下:

int sem_init(sem_t *sem, int pshared, unsigned int value);

参数: sem_init函数有三个参数:

  • sem:一个指向sem_t类型的指针,用于存储初始化后的信号量对象。
  • pshared:表示信号量的共享方式。
    • 如果pshared的值为0,表示信号量只能在调用它的进程内的线程之间共享。
    • 如果pshared的值为非零,表示信号量可以在多个进程之间共享。
  • value:表示信号量的初始值(初始临界资源內部有多少个小块资源)。

返回值: sem_init函数的返回值是一个整数,用于表示函数调用是否成功。

  • 如果返回值为0,表示函数调用成功。
  • 如果返回值为-1,表示函数调用失败,此时可以通过查看全局变量errno获取错误码。

  以下是一个示例代码,演示了如何使用sem_init函数来初始化一个信号量:

#include <stdio.h>
#include <semaphore.h>

int main() {
    sem_t mySem;
    
    // 初始化一个非共享的信号量,初始值为1
    int ret = sem_init(&mySem, 0, 1);
    if (ret == -1) {
        perror("Failed to initialize semaphore");
        return 1;
    }

    // 进行其他操作...

    // 销毁信号量
    sem_destroy(&mySem);

    return 0;
}

2、3 sem_wait

  sem_wait函数用于对信号量进行等待操作,同时会将信号量减1。以实现对临界资源的互斥访问。函数原型如下:

int sem_wait(sem_t *sem);

参数: sem_wait函数只有一个参数:

  • sem:一个指向已经初始化的信号量对象。

返回值: sem_wait函数的返回值是一个整数,用于表示函数调用是否成功。

  • 如果返回值为0,表示函数调用成功,信号量的值被成功减一。
  • 如果返回值为-1,表示函数调用失败,此时可以通过查看全局变量errno获取错误码。常见的错误码包括EINTR(被信号中断)和EDEADLK(死锁)等。

2、4 sem_post

  sem_post函数用于对信号量进行发布操作,以增加信号量的值(对信号量加1)。它通常与sem_wait函数一起使用,用于在对共享资源的访问结束后释放信号量,以便其他线程可以获取到该资源。函数原型如下:

int sem_post(sem_t *sem);

参数: sem_post函数只有一个参数:

  • sem:一个指向已经初始化的信号量对象。

返回值: sem_post函数的返回值是一个整数,用于表示函数调用是否成功。

  • 如果返回值为0,表示函数调用成功,信号量的值被成功增加。
  • 如果返回值为-1,表示函数调用失败,此时可以通过查看全局变量errno获取错误码。常见的错误码包括EINVAL(信号量未初始化)和EOVERFLOW(信号量值达到上限)等。

  需要注意的是,sem_post函数并不处理过度发布的情况,即如果信号量的值已经达到了其上限,再调用sem_post函数也无法将其继续增加。因此,在使用信号量时,必须正确地控制信号量的值以避免出现竞态条件或死锁等问题。

2、5 sem_destory

  sem_destory函数用于销毁一个已经初始化的信号量对象,并释放相关的资源。当不再需要使用信号量时,应该调用sem_destroy函数进行清理操作。函数原型如下:

int sem_destroy(sem_t *sem);

参数: sem_destroy函数只有一个参数:

  • sem:一个指向已经初始化的信号量对象。

返回值: sem_destroy函数的返回值是一个整数,用于表示函数调用是否成功。

  • 如果返回值为0,表示函数调用成功,信号量对象被成功销毁并释放了相关的资源。
  • 如果返回值为-1,表示函数调用失败,此时可以通过查看全局变量errno获取错误码。常见的错误码包括EINVAL(信号量未初始化)和EBUSY(仍有线程在等待该信号量)等。

三、基于信号量的生产者消费者模型

3、1 信号量控制环形队列

  我们之前学习了​ 生产者消费者问题(条件变量 & 互斥锁)。之前学习的时由阻塞队列来实现的。通过互斥锁与条件变量很好的维护了生产者与消费者之前的同步与互斥关系。那么我么那接下来看看用信号量来维护生产者和消费者之间的同步与互斥关系的环形队列。

3、1、1 空间资源和数据资源

  我们先看下图:

  生产者就是要生产数据放进环形队列中去。那么生产者所需要的就是申请环形队列空间资源。这个空间的大小我们可以自定义,比如环形队列由10个空间资源

  消费者就是去环形队列拿数据。消费者所需要的就是申请数据资源。也就是看环形队列中是否还有数据资源。 

3、1、2 保护原理(二元信号量)

  我们发现:生产和消费在队列为空的时候或者满的时,可能访问同一个位置。那我们必须保证生产者生产的数据个数最多不能超过环形队列的容量,其次消费者在空的时候不能再拿数据

  那我们就可以用两个信号量来很好的维护这两个角色的需求。生产者对应空间资源信号量,消费者对应数据资源信号量。当申请对应的信号量资源失败时,也会进入阻塞式等待。直到有信号量资源才会继续执行。

  当只有单生产单消费时,我们只需要维护好生产与消费的同步与互斥关系。多生产与多消费时,还需维护生产与生产、消费与消费的互斥关系。下面我们直接看代码。

3、2 demo代码

3、2、1 单生产与单消费

ringQueue.hpp

#include<iostream>
#include<semaphore.h>
#include<unistd.h>
#include<vector>
#include<assert.h>
#include<pthread.h>
#include<time.h>
#include<string.h>


using namespace std;

#include"Task.hpp"
#include"LogTest.hpp"

static const int g_cap=5;

template<class T>
class RingQueue
{
private:
    void P(sem_t& sem)
    {
        int n = sem_wait(&sem);
        assert(n == 0);
        (void)n;
    }

    void V(sem_t& sem)
    {
        int n = sem_post(&sem);
        assert(n == 0);
        (void)n;
    }

public:
    RingQueue(int cap = g_cap)
        :_cap(cap)
    {
        int n = sem_init(&_spaceSem, 0, _cap);
        assert(n == 0);
        n = sem_init(&_dataSem, 0, 0);
        assert(n == 0);

        _queue.resize(_cap);
        _productorStep = _consumerStep = 0;
    }

    // 生产者
    void push(const T& in)
    {
        P(_spaceSem);
        _queue[_productorStep++] = in;
        _productorStep %= _cap;
        V(_dataSem);
    }

    // 消费者
    void pop(T* out)
    {
        P(_dataSem);
        *out = _queue[_consumerStep++];
        _consumerStep %= _cap;
        V(_spaceSem);
    }

    ~RingQueue()
    {
        sem_destroy(&_spaceSem);
        sem_destroy(&_dataSem);
    }
private:
    sem_t _spaceSem; // 生产者——空间资源
    sem_t _dataSem;  // 消费者——数据资源
    vector<T> _queue;
    int _cap;
    int _productorStep;
    int _consumerStep;
};

testMain.cpp

#include "ringQueue.hpp"

int myAdd(int x, int y)
{
    return x + y;
}

void* ProductorRoutine(void *arg)
{
    RingQueue<Task> *rq = (RingQueue<Task>*) arg;
    while(true)
    {
        int x = rand() % 10 +1;
        int y = rand() % 100 + 1;
        Task t(x, y, myAdd);
        rq->push(t);
        LogMessage(1,"%s:%d + %d = ?","生产者申请了一个空间", x, y);
        //sleep(1);
    }
}

void* ConsumerRoutine(void* arg)
{
    RingQueue<Task> *rq = (RingQueue<Task>*)arg;
    while(true)
    {
        Task t;
        rq->pop(&t);
        LogMessage(1,"%s:%d + %d = %d","消费者消费了一个数据", t.x_, t.y_, t());
        sleep(1);
    }
}
int main()
{
    srand((unsigned int)time(nullptr) ^ 0x666888);
    RingQueue<Task> *rq = new RingQueue<Task>();

    pthread_t c, p;
    pthread_create(&p, nullptr, ProductorRoutine, rq);
    pthread_create(&c, nullptr, ConsumerRoutine, rq);

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

  通过上述代码我们发现:信号量有点类似于互斥锁+条件变量的结合,不还是在竞争资源串行访问吗?实际上刚开始我们并不知道是先生产,还是先消费。如果先消费,则需等待。如果满的情况下,生产也需要等待。其他情况下大部分时间都是在并发执行的生产和消费可同时进行。

 3、2、2 多生产多消费

  当多生产和多消费时,我们还需维护生产与生产、消费与消费的互斥关系。这时候就需要互斥锁来维护了。代码如下:

ringQueue.hpp

#pragma once

#include<iostream>
#include<semaphore.h>
#include<unistd.h>
#include<vector>
#include<assert.h>
#include<pthread.h>
#include<time.h>
#include<string.h>


using namespace std;

#include"Task.hpp"
#include"LogTest.hpp"

static const int g_cap=5;

template<class T>
class RingQueue
{
private:
    void P(sem_t& sem)
    {
        int n = sem_wait(&sem);
        assert(n == 0);
        (void)n;
    }

    void V(sem_t& sem)
    {
        int n = sem_post(&sem);
        assert(n == 0);
        (void)n;
    }

public:
    RingQueue(int cap = g_cap)
        :_cap(cap)
    {
        int n = sem_init(&_spaceSem, 0, _cap);
        assert(n == 0);
        n = sem_init(&_dataSem, 0, 0);
        assert(n == 0);

        _queue.resize(_cap);
        _productorStep = _consumerStep = 0;
    }

    // 生产者
    void push(const T& in)
    {
        P(_spaceSem);
        pthread_mutex_lock(&_pmutex);  
        _queue[_productorStep++] = in;
        _productorStep %= _cap;
        pthread_mutex_unlock(&_pmutex);
        V(_dataSem);
    }

    // 消费者
    void pop(T* out)
    {
        P(_dataSem);
        pthread_mutex_lock(&_cmutex);
        *out = _queue[_consumerStep++];
        _consumerStep %= _cap;
        pthread_mutex_unlock(&_cmutex);
        V(_spaceSem);
    }

    ~RingQueue()
    {
        sem_destroy(&_spaceSem);
        sem_destroy(&_dataSem);
    }
private:
    sem_t _spaceSem; // 生产者——空间资源
    sem_t _dataSem;  // 消费者——数据资源
    vector<T> _queue;
    int _cap;
    int _productorStep;
    int _consumerStep;

    pthread_mutex_t _pmutex;
    pthread_mutex_t _cmutex;
};

testMain.cpp

#include "ringQueue.hpp"

int myAdd(int x, int y)
{
    return x + y;
}

void* ProductorRoutine(void *arg)
{
    RingQueue<Task> *rq = (RingQueue<Task>*) arg;
    while(true)
    {
        int x = rand() % 10 +1;
        int y = rand() % 100 + 1;
        Task t(x, y, myAdd);
        rq->push(t);
        LogMessage(1,"%s:%d + %d = ?","生产者申请了一个空间", x, y);
        //sleep(1);
    }
}

void* ConsumerRoutine(void* arg)
{
    RingQueue<Task> *rq = (RingQueue<Task>*)arg;
    while(true)
    {
        Task t;
        rq->pop(&t);
        LogMessage(1,"%s:%d + %d = %d","消费者消费了一个数据", t.x_, t.y_, t());
        sleep(1);
    }
}
int main()
{
    srand((unsigned int)time(nullptr) ^ 0x666888);
    RingQueue<Task> *rq = new RingQueue<Task>();

    pthread_t p[4], c[8];
    for(int i = 0; i < 4; i++) pthread_create(p+i, nullptr, ProductorRoutine, rq);
    for(int i = 0; i < 8; i++) pthread_create(c+i, nullptr, ConsumerRoutine, rq);

    for(int i = 0; i < 4; i++) pthread_join(p[i], nullptr);
    for(int i = 0; i < 8; i++) pthread_join(c[i], nullptr);
    delete rq;
    return 0;
}

  这里又有一个小细节:我们在插入或者删除时,是先加锁再申请信号量呢,还是先申请信号量再加锁呢? 首先申请信号量的操作时原子的,这个问题不用担心。关键在于插入和删除的过程。其实先加锁再申请信号量是肯定可行的,就是先申请信号量再加锁可以吗?答案是可以的!申请信号量的本质就是在对资源的预定。只要你申请信号量成功,就一定有资源可用。当我们先把信号量申请完,就是把资源先分配给了不同线程。反而会更好一点。后续各个线程不用竞争信号资源了,只需竞争锁资源就可以了。

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

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

相关文章

Element登录+注册

目录 一、ElementUI 1.1 定义 1.2 特点 1.3 完成用户注册登录界面搭建 1.3.1 创建一个Vue项目 1.3.2 在src目录下创建views目录 1.3.3 下载js依赖 ​编辑 1.3.4 创建用户登录注册组件 1.3.5 配置路由 1.3.6 修改项目端口并启动项目 二、数据交互 2.1 数据导入 2.2…

NLP BigModel

NLP 基础 建议看 [CS224N 2023]打基础 【NLP入门】1. n元语法模型 / 循环神经网络 【NLP入门】3. Word2Vec / GloVe Language Model&#xff1a;语言模型的马尔可夫假设&#xff08;每个词出现的概率仅依赖前面出现的词&#xff09;&#xff0c;是一个自回归模型&#xff08;…

sql注入(其他)

1.宽字节注入 组成汉字把转义的字符改为汉字 源代码 php 做转义 把语句改为gbk 2.http头部注入 3.补充

ElasticSearch - 索引库和文档相关命令操作

目录 一、ElasticSearch 索引库操作 1.1、mapping 属性 1.2、索引库相关操作 1.2.1、创建索引库 1.2.2、增加和删除索引库 1.2.3、修改索引库 1.3、文档操作 1.3.1、添加文档 1.3.2、文档的查询和删除 1.3.3、修改文档 1.全量修改&#xff1a;会先删除旧文档&#xf…

Windows 上下载并提取 Wikipedia

下载资源 很久以前看过了 Wikipedia 是支持 dump 的&#xff0c;不得不说真是造福人类的壮举。我其实也用不到这个&#xff0c;但是看见不少人是用来做 NLP 语料训练的。不过最近我也想尝试一些新的东西&#xff08;我就是单纯想要这个文本数据&#xff09;&#xff0c;所以就…

C++ list容器的实现及讲解

所需要的基础知识 对C类的基本了解 默认构造函数 操作符重载 this指针 引用 模板等知识具有一定的了解&#xff0c;阅读该文章会很轻松。 链表节点 template<class T>struct list_node{T _data;list_node<T>* _next;list_node<T>* _prev;list_node(const T&…

Lyapunov optimization 李雅普诺夫优化

文章目录 正文引言Lyapunov drift for queueing networks 排队网络的Lyapunov漂移Quadratic Lyapunov functions 二次李雅普诺夫函数Bounding the Lyapunov drift 李亚普诺夫漂移的边界A basic Lyapunov drift theorem 一个基本的李雅普诺夫漂移定理 Lyapunov optimization for…

甲方测试如何做好外包项目的测试验收?

春节匆匆而过&#xff0c;打工人陆续回到了工作岗位又开始卷了起来。小酋也一样&#xff0c;已经返岗几天&#xff0c;今天趁着节后综合症消去大半又该聊点什么了。最近在做一个视频AI分析项目的测试验收&#xff0c;今天就围绕“如何做好外包项目的测试验收”为题&#xff0c;…

详细学习Mybatis(1)

详细学习Mybatis&#xff08;1&#xff09; 一、MyBatis概述1.1 框架1.2 三层框架1.3 了解Mybatis 二、Mybatis入门开发2.1 入门2.2、MyBatis入门程序的一些小细节2.3、MyBatis事务管理机制深度解析2.4、在开发中junit是如何使用的2.5、Mybatis集成日志框架logback 一、MyBatis…

从网络方面解决Android Sutdio遇到的Unable to access Android SDK add-on list问题

依然说一下环境&#xff1a; 家庭宽带网络win11环境安装的Android Studio版本&#xff1a;android-studio-2022.3.1.19-windowsJava版本&#xff1a;OpenJDK11 &#xff08;这个应该无所谓&#xff09; 问题描述&#xff1a; Unable to access Android SDK add-on list 要我…

Pytorch之LeNet-5图像分类

&#x1f482; 个人主页:风间琉璃&#x1f91f; 版权: 本文由【风间琉璃】原创、在CSDN首发、需要转载请联系博主&#x1f4ac; 如果文章对你有帮助、欢迎关注、点赞、收藏(一键三连)和订阅专栏哦 目录 前言 一、LeNet-5 二、LeNet-5网络实现 1.定义LeNet-5模型 2.加载数…

解决apk签名时 no conscrypt_openjdk_jni in java.library.path 方法

使用下面命令时若出现no conscrypt_openjdk_jni in java.library.path java -jar signapk.jar platform.x509.pem platform.pk8 app-debug.apk app-debug_sign.apk 缺少相关库&#xff0c;从以下位置下载&#xff0c;只在 android11下测试通过。 https://download.csdn.net…

2023 年前端 UI 组件库概述,百花齐放!

UI组件库提供了各种常见的 UI 元素&#xff0c;比如按钮、输入框、菜单等&#xff0c;只需要调用相应的组件并按照需求进行配置&#xff0c;就能够快速构建出一个功能完善的 UI。 虽然市面上有许多不同的UI组件库可供选择&#xff0c;但在2023年底也并没有出现一两个明确的解决…

java面试题-常见技术场景

常见技术场景 1.单点登录这块怎么实现的 1.1 概述 单点登录的英文名叫做&#xff1a;Single Sign On&#xff08;简称SSO&#xff09;,只需要登录一次&#xff0c;就可以访问所有信任的应用系统 在以前的时候&#xff0c;一般我们就单系统&#xff0c;所有的功能都在同一个…

EtherCAT转Modbus网关做为 MODBUS 从站配置案例

兴达易控EtherCAT转Modbus网关可以用作MODBUS从站的配置。这种网关允许将Modbus协议与EtherCAT协议进行转换&#xff0c;从而实现不同通信系统之间的互操作性。通过将Modbus从站配置到网关中&#xff0c;可以实现对Modbus设备的访问和控制。同时&#xff0c;该网关还可以扩展Mo…

mysql基本语句学习(基本)

1.本地登录 mysql -u root -p 密码 mysql开启远程 1.查看数据库 show databases; 2.查看当前所示数据库 select database(); 3.创建数据库 create database 数据库名字&#xff1b; 4.查看创建数据库语句 show create database 数据库名字&#xff1b; 2.…

(十一)VBA常用基础知识:worksheet的各种操作之sheet删除

当前sheet确认 2.Sheets(1).Delete Sub Hello()8 Sheets(1).DeleteSheets(1).Delete End Sub实验得知&#xff0c; Sheets(1).Delete删除的是最左边的sheet 另外&#xff0c;因为有弹出提示信息的确认框&#xff0c;这个在代码执行时&#xff0c;会导致还需要手动点击一下&a…

仿制 Google Chrome 的恐龙小游戏

通过仿制 Google Chrome 的恐龙小游戏&#xff0c;我们可以掌握如下知识点&#xff1a; 灵活使用视口单位掌握绝对定位JavaScript 来操作 CSS 变量requestAnimationFrame 函数的使用无缝动画实现 页面结构 实现页面结构 通过上述的页面结构我们可以知道&#xff0c;此游戏中…

【多态】虚函数表存储在哪个区域?

A:栈 B:堆 C:代码段&#xff08;常量区&#xff09; D:数据段&#xff08;静态区&#xff09; 答案 &#xff1a; 代码段&#xff08;常量区&#xff09; 验证如下&#xff1a; class Person { public:virtual void BuyTicket() { cout << "Person::BuyTicket()&q…

【Hash表】判断有没有重复元素-力扣 217

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:kuan 的首页,持续学…