Linux---多线程(上)

news2024/11/19 1:39:34

一、线程概念

  • 线程是比进程更加轻量化的一种执行流 / 线程是在进程内部执行的一种执行流
  • 线程是CPU调度的基本单位,进程是承担系统资源的基本实体

在说线程之前我们来回顾一下进程的创建过程,如下图

那么以进程为参考,我们该如何去设计创建一个线程呢?
线程不止一个,就注定它也需要被管理,即先描述在组织,即线程需要有一个TCB(和PCB类似),同时线程也是执行流,也需要被调度运行,被管理,所以我们还需要给它设计一系列的算法,然后还得让它和进程联系起来,这样很麻烦

那有没有更简单的做法呢?
由于线程同进程一样也是一个执行流,那么管理进程用的一些信息,线程也应该需要,只不过线程更加起轻量化而已(这个后面会有具体说明),所以进程的结构体和管理用的内核数据结构对于线程也应该同样适用,所以我们可以选择复用进程的代码和数据结构,轻松的完成任务,并且也不用考虑进程和线程的耦合问题,因为它们处在同一个体系框架下
(上面介绍的是Linux的做法,当然也有OS是将进程和线程分开来设计的)

具体如下

但是现在又出现了一个问题,如何看待进程???根据上面这张图,我们会发现进程和线程似乎一样了,都有数据结构和各自的代码和数据。下面我们来进一步理解进程

感性的理解进程和线程

我们可以将进程想象成一个家庭,线程则是家庭中的人,进程的任务就是将这个家变得越来越好,所以家中的每个人都要分工合作干好各自的事,每个家庭都具有独立性(进程独立性),即你生活的好不好跟你的邻居没太大关系,但是你们可能会有交集(进程间通信),之前我们讲的进程可以看成是家中只有一个人的情况。


下面写一段代码(不用管创建线程的函数,后面会讲,主要是观察现象)

#include <iostream>
#include <pthread.h>
#include <unistd.h>
//新线程
void *Threadroutine(void *args)
{
    char *p = (char *)args;
    while (1)
    {
        std::cout << "I am a new thread : " << p << ", pid : " << getpid() << std::endl;
        sleep(1);
    }
    return nullptr;
}

int main()
{
    //已经有进程了
    pthread_t tid;
    pthread_create(&tid, nullptr, Threadroutine, (void *)"thread1");
    //主线程
    while (1)
    {
        std::cout << "I am a main thread, pid : " << getpid() << std::endl;
        sleep(1);
    }
    return 0;
}

首先确实有两个执行流在循环打印语句,并且它们的进程pid相同,但是它们的LWP(light weight process)不同,即它们是线程(在Linux中线程的底层是轻量级进程),所以OS在调度时看的是LWP,当然我们会发现有一个线程的LWP和PID是一样的,它就是主线程。(如果你看到乱序的打印也是正常的,因为OS如何调度两个线程是未知的)

而由于线程共享同一个进程地址空间,所以线程间的共享资源很多,也更容易通信(不保证安全)

#include <iostream>
#include <pthread.h>
#include <unistd.h>

int cnt = 0;
void *Threadroutine(void *args)
{
    char *p = (char *)args;
    while (1)
    {
        std::cout << "I am a new thread : " << p << ", pid : " << getpid() << " cnt:" << cnt << std::endl;
        cnt++;
        sleep(1);
    }
    return nullptr;
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, Threadroutine, (void *)"thread1");
    sleep(1);

    pthread_t tid1;
    pthread_create(&tid1, nullptr, Threadroutine, (void *)"thread2");
    sleep(1);

    pthread_t tid2;
    pthread_create(&tid2, nullptr, Threadroutine, (void *)"thread3");
    sleep(1);

    while (1)
    {
        std::cout << "I am a main thread, pid : " << getpid() << " cnt:" << cnt << std::endl;
        sleep(1);
    }
    return 0;
}

注意:打印乱序是正常现象,因为Threadroutine是不可重入函数,但是我们重入了。但是这不妨碍我们能看到cnt在不断增加的,并且每个线程都能看到,也就是说cnt这个全局变量对于线程来说是共享的。

如何理解线程更加轻量化???

1、线程的创建更加简单

2、线程的切换更加高效

  • 要修改的寄存器少了---因为线程中有很多数据是一样的,比如存放页表地址的寄存器就不用修改,因为它们共用一个页表
  • 不需要重新更新cache(缓存)---根据局部性原理,在执行某条语句之后,更有可能执行它的上下文中的代码,所以我们会提前将它附近的代码和数据加载到缓存(称为热数据),来提高CPU效率,对于线程来说这样的热数据大概率是有效的,而对于进程来说,则是基本无效的,需要重新加载

(注意:这里谈论的线程切换是指在同一个进程中的线程的切换,不同进程的线程切换还是属于进程切换)

二、进一步理解进程地址空间

三、线程的优缺点+资源+异常

 1、优点

  • 创建一个新线程的代价要比创建一个新进程小得多
  • 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多
  • 线程占用的资源要比进程少很多
  • 能充分利用多处理器的可并行数量
  • 在等待慢速I/O操作结束的同时,程序可执行其他的计算任务
  • 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
  • I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作

2、缺点

  • 性能损失:一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变。
  • 健壮性降低:编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。
  • 缺乏访问控制:进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。
  • 编程难度提高:编写与调试一个多线程程序比单线程程序困难得多(要更加细节)

3、资源

线程共享进程数据,但也拥有自己的一部分数据:线程ID、硬件上下文、栈、errno、信号屏蔽字、调度优先级

进程的多个线程共享同一地址空间,因此Text Segment、Data Segment都是共享的,如果定义一个函数,在各线程中都可以调用,如果定义一个全局变量,在各线程中都可以访问到,除此之外,各线程还共享以下进程资源和环境:文件描述符表、每种信号的处理方式(SIG_ IGN、SIG_ DFL或者自定义的信号处理函数)、当前工作目录、用户id和组id

(注意:信号的pending位图、block位图都是各自私有的)
 

4、线程异常

  • 单个线程如果出现除零,野指针问题导致线程崩溃,进程也会随着崩溃
  • 线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该进程内的所有线程也就随即退出

四、线程控制---相关函数接口介绍

1、线程创建

功能:创建一个新的线程
参数:

  • thread:返回线程ID
  • attr:设置线程的属性,attr为NULL表示使用默认属性
  • start_routine:是个函数地址,线程启动后要执行的函数
  • arg:传给线程启动函数的参数

返回值:成功返回0;失败返回错误码

#include <iostream>
#include <pthread.h>
#include <unistd.h>

void* ThreadRoutine(void * args)
{
    while(1)
    {
        std::cout << "I am a new thread" << std::endl;
        sleep(1);
    }
}
int main()
{
    pthread_t tid;
    pthread_create(&tid,nullptr,ThreadRoutine,nullptr);
    while(1)
    {
        std::cout << "I am a main thread" << std::endl;
        sleep(1);
    }
    return 0;
}

 

1)给线程传参的问题

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

using func_t = std::function<void()>;
class ThreadDate
{
public:
    std::string _name;
    u_int64_t _createtime;
    func_t _f;

public:
    ThreadDate(std::string name, u_int64_t createtime, func_t f)
        : _name(name), _createtime(createtime), _f(f)
    {
    }
};

void Print()
{
    std::cout << "我正在执行某个任务" << std::endl;
}

//函数的参数为void*,即可以传任意类型的指针,我们可以把结构体对象当参数传进去
void *ThreadRoutine(void *args)
{
    ThreadDate *ptd = static_cast<ThreadDate *>(args);
    while (1)
    {
        std::cout << "new thread name: " << ptd->_name << " , create time: " << ptd->_createtime << std::endl;
        ptd->_f();
        sleep(1);
    }
}

int main()
{
    std::vector<pthread_t> v;
    for (int i = 0; i < 5; i++)
    {
        char name[64] = {0};
        sprintf(name, "%s-%d", "thread", i);
        ThreadDate *p = new ThreadDate(name, (u_int64_t)time(nullptr), Print);
        pthread_t tid;
        pthread_create(&tid, nullptr, ThreadRoutine, (void *)p);
        v.push_back(tid);
    }

    sleep(3);
    std::cout << "thread id: ";
    for (auto x : v)
    {
        std::cout << x << " ";
    }
    std::cout << std::endl;

    while (1)
    {
        std::cout << "I am a main thread" << std::endl;
        sleep(1);
    }
    return 0;
}

我们确实能通过函数的参数将我们想要的数据(放在结构体对象中)传给线程,并且也可以传函数,同理该线程的返回值也是一样(这个后面会演示)

打印出来的tid显然和LWP不相同,它具体是什么呢?(后面会说,这里只是抛出问题)。

也可以通过上面这个函数获取线程自身的tid,注意不是LWP!!!

2)线程异常问题

显然在第四个线程出现异常,收到信号后,整个进程都退出了,也说明线程的健壮性比较差

2、线程退出

原型:void pthread_exit(void *value_ptr)

功能:线程终止
参数:

  • value_ptr:value_ptr不要指向一个局部变量。

返回值:无返回值,跟进程一样,线程结束的时候无法返回到它的调用者(自身)

#include <iostream>
#include <pthread.h>
#include <unistd.h>

void* ThreadRoutine(void* args)
{
    int cnt = 5;
    while(cnt--)
    {
        std::cout << " I am a new thread " << std::endl;
        sleep(1);
    }
    
    pthread_exit(nullptr); // 终止线程
    // return nullptr; // 也可以终止线程
    // exit(1); // 注意:该函数是用来结束进程的!!!
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, ThreadRoutine, nullptr);
    //...
    return 0;
}

 我们也可以直接return,这样也是能终止线程的。

3、线程等待

线程退出默认也是需要被等待的(就像进程一样)

  • 线程退出,没有等待,会导致类似进程的僵尸问题
  • 线程退出,主线程也需要获取新线程的返回值

原型:int pthread_join(pthread_t thread, void **value_ptr)

功能:等待线程结束
参数

  • thread:线程ID
  • value_ptr(输出型参数):它指向一个指针,该指针指向线程的返回值,可以接收任意类型的指针

返回值:成功返回0,失败返回错误码

#include <iostream>
#include <pthread.h>
#include <unistd.h>

void* ThreadRoutine(void* args)
{
    int cnt = 5;
    while(cnt--)
    {
        std::cout << " I am a new thread " << std::endl;
        sleep(1);
    }
    
    pthread_exit((void*)"thread end"); //结束线程
    // return (void*)"thread end";
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, ThreadRoutine, nullptr);
    void*ret = nullptr;
    int n = pthread_join(tid,&ret);
    std::cout << "main thread, n: " << n << std::endl;
    std::cout << "new thread return val: " << (char*)ret << std::endl;
    return 0;
}

和线程创建时传给线程的参数一样,这里的返回值可以是任意类型的指针(可以指向结构体,该结构体中可以存放任何你想通过线程得到的数据,这个就不演示了,类比线程创建即可)

这里简单说明一下:为什么进程退出时既关心是否正常退出,又关心异常问题,但是线程出现异常我们并不关心?因为线程一旦异常,会导致进程整个挂掉,所以线程的异常就没必要关心了

4、线程分离

当主线程不关心线程的的返回结果时,我们可以将线程设置为分离状态,这样该线程结束后就会自动被OS回收,不需要主线程在等待了

int pthread_detach(pthread_t thread)

功能:分离线程

参数:

  • thread:线程ID

返回值:成功返回0,失败返回错误码

#include <iostream>
#include <pthread.h>
#include <unistd.h>

void* ThreadRoutine(void* args)
{
    // pthread_detach(pthread_self()); // 可以在线程中进行线程分离
    int cnt = 5;
    while(cnt--)
    {
        std::cout << " I am a new thread " << std::endl;
        sleep(1);
    }
    return nullptr;
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, ThreadRoutine, nullptr);
    pthread_detach(tid);
    sleep(1);
    int n = pthread_join(tid,nullptr);
    std::cout << "main thread, n: " << n << std::endl;
    return 0;
}

显然线程等待失败,这里的线程分离,也可以在创建的线程中使用 (注意:线程分离后,出现异常还是会导致整个进程挂掉)

5、线程取消

原型:int pthread_cancel(pthread_t thread)

功能:取消一个执行中的线程
参数:

  • thread:线程ID

返回值:成功返回0,失败返回错误码

#include <iostream>
#include <pthread.h>
#include <unistd.h>

void* ThreadRoutine(void* args)
{
    while(true)
    {
        std::cout << " I am a new thread " << std::endl;
        sleep(1);
    }
    return nullptr;
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, ThreadRoutine, nullptr);
    // pthread_detach(tid);
    sleep(5);
    int n = pthread_cancel(tid);
    std::cout << "main thread, cancel return: " << n << std::endl;

    void*ret = nullptr;
    n = pthread_join(tid, &ret);
    std::cout << "main thread, join return: " << n << " ret: " << (long long)ret << std::endl;
    return 0;
}

(注意:如果线程在pthread_cancel之前终止,那么该函数调用失败,返回错误码) 

thread线程以不同的方法终止,通过pthread_join得到的终止状态是不同的,总结如下
1. 如果thread线程通过return返回,value_ ptr所指向的单元里存放的是thread线程函数的返回值。
2. 如果thread线程被别的线程调用pthread_ cancel异常终掉,value_ ptr所指向的单元里存放的是常数PTHREAD_ CANCELED [#define PTHREAD_CANCELED ((void *) -1)]
3. 如果thread线程是自己调用pthread_exit终止的,value_ptr所指向的单元里存放的是传给pthread_exit的参数
4. 如果对thread线程的终止状态不感兴趣,可以传NULL给value_ ptr参数

  • 默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄漏
  • 如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源,即线程分离
  • 一个线程要么是joinable的,要么是分离的

五、理解线程库

在语言的角度:其实C++11中的线程库就是对原生线程库的封装,其他语句同理

注意:__thread只能修饰内置类型,自定义类型不行,在C++11中可以用thread_local对自定义类型进行修饰

六、模拟实现C++线程库(简易版)

#pragma once
#include <iostream>
#include <functional>
#include <string>
#include <pthread.h>

using func_t = std::function<void()>; // 该函数类型可以按照需求改变
class thread
{
public:
    thread(std::string name, func_t f)
        : _tid(0), _name(name), _isrunning(false), _fun(f)
    {
    }

    // 注意,如果是非静态成员,则会多一个this作为参数(c++语法)
    static void *ThreadRoutine(void *args)
    {
        thread *t = static_cast<thread *>(args);
        t->_fun(); // 要想访问类成员,要传类对象
        return nullptr;
    }

    bool Start()
    {
        int n = pthread_create(&_tid, nullptr, ThreadRoutine, this);
        if (n == 0)
        {
            _isrunning = true;
            return true;
        }
        return false;
    }

    bool Join()
    {
        if (!_isrunning)
            return true;
        int n = pthread_join(_tid, nullptr);
        if (n == 0)
        {
            _isrunning = false;
            return true;
        }
        return false;
    }

    std::string getname()
    {
        return _name;
    }

    bool IsRunning()
    {
        return _isrunning;
    }
    
    ~thread()
    {
    }

private:
    pthread_t _tid;
    std::string _name;
    bool _isrunning;
    func_t _fun;
};


//进阶---用模板
template <class T>
using func_t = std::function<void(T)>;

template <class T>
class thread
{
public:
    thread(std::string name, func_t<T> f, T data)
        : _tid(0), _name(name), _isrunning(false), _fun(f),_data(data)
    {
    }

    // 注意,如果是非静态成员,则会多一个this作为参数(c++语法)
    static void *ThreadRoutine(void *args)
    {
        thread *t = static_cast<thread *>(args);
        t->_fun(t->_data); // 要想访问对象,要传递对象
        return nullptr;
    }

    bool Start()
    {
        int n = pthread_create(&_tid, nullptr, ThreadRoutine, this);
        if (n == 0)
        {
            _isrunning = true;
            return true;
        }
        return false;
    }

    bool Join()
    {
        if (!_isrunning)
            return true;
        int n = pthread_join(_tid, nullptr);
        if (n == 0)
        {
            _isrunning = false;
            return true;
        }
        return false;
    }

    std::string getname()
    {
        return _name;
    }

    bool IsRunning()
    {
        return _isrunning;
    }

    ~thread()
    {
    }

private:
    pthread_t _tid;
    std::string _name;
    bool _isrunning;
    func_t<T> _fun;
    T _data;// 如果需要也可以加一个成员变量存储线程的结果
};

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

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

相关文章

STM32串口:DMA空闲中断实现接收不定长数据(基于HAL库)

STM32串口&#xff1a;DMA空闲中断实现接收不定长数据&#xff08;基于HAL库&#xff09;&#xff1a; 第一步&#xff1a;设置rcc&#xff0c;时钟频率&#xff0c;下载方式 设置system core->RCC如图所示&#xff1a;&#xff08;即High Speed Clock和Low Speed Clock都选…

EasyNVR级联EasyCVR,在EasyCVR播放视频会导致EasyNVR崩溃的原因排查与解决

视频综合管理平台EasyCVR视频监控系统支持多协议接入、兼容多类型设备&#xff0c;平台可以将监控区域内所有部署的监控设备进行统一接入与集中汇聚管理&#xff0c;实现对监控区域的实时视频监控、录像与存储、设备管理、云台控制、语音对讲、级联共享等&#xff0c;在监控中心…

跨境账号养号怎么做?Facebook、亚马逊运营必看

之前我们讨论过很多关于代理器的问题。它们的工作原理是什么?在不同的软件中要使用那些代理服务器?这些代理服务器之间的区别是什么?什么是反检测浏览器等等。 除了这些问题&#xff0c;相信很多人也会关心在使用不同平台的时代理器的选择问题。比如&#xff0c;为什么最好…

深入理解React中的useState:函数组件状态管理的利器

&#x1f90d; 前端开发工程师、技术日更博主、已过CET6 &#x1f368; 阿珊和她的猫_CSDN博客专家、23年度博客之星前端领域TOP1 &#x1f560; 牛客高级专题作者、打造专栏《前端面试必备》 、《2024面试高频手撕题》 &#x1f35a; 蓝桥云课签约作者、上架课程《Vue.js 和 E…

别再盲目推广!用Xinstall第三方统计,精准衡量广告ROI

在移动互联网时代&#xff0c;App推广已经成为各大广告主和开发者的必修课。然而&#xff0c;面对复杂的推广环境和多变的用户需求&#xff0c;如何提升推广效率、洞悉推广效果、衡量广告ROI&#xff0c;一直是困扰着广大广告主和开发者的难题。今天&#xff0c;我们就来聊聊一…

ChatGPT怎么用 ChatGPT小白能掌握的技巧原理

ChatGPT(Chat Generative Pre-training Transformer) 是一个 AI 模型,属于自然语言处理( Natural Language Processing , NLP ) 领域,NLP 是人工智能的一个分支。所谓自然语言,就是人们日常生活中接触和使用的英语、汉语、德语等等。自然语言处理是指,让计算机来理解并…

#LT8711V适用于Type-C/DP1.2/EDP转VGA应用方案,分辨率高达1080P。

1. 概述 LT8711V是一款高性能 Type-C/DP1.2 转 VGA 转换器&#xff0c;设计用于将 USB Type-C 源或 DP1.2 源连接到 VGA 接收器。 该LT8711V集成了一个符合DP1.2标准的接收器和一个高速三通道视频DAC。此外&#xff0c;还包括两个用于 CC 通信的 CC 控制器&#xff0c;以实现 …

揭秘PostgreSQL:超越传统数据库的无限可能!

介绍&#xff1a;PostgreSQL是一个功能强大的开源对象关系数据库系统。以下是对PostgreSQL的详细介绍&#xff1a; 开源性&#xff1a;PostgreSQL是完全开源的&#xff0c;这意味着任何人都可以自由地获取、使用和修改它的源代码。 可定制性&#xff1a;它具有高度可定制性&…

gitee分支管理,合并冲突

1、gitee展示分支 git branch 2、展示远程分支 git branch -r 3、新建分支 git branch base 4、切换分支 git checkout base 合并冲突 当代码在服务器上被提交了&#xff0c;再在本地提交会提示报错 点击merge

《互联网的世界》第六讲-去中心化和安全

互联网构建于开放互联的中立原则之上&#xff0c;公平接入&#xff0c;数据互联互通&#xff0c;流量被无差别对待&#xff0c;这意味着互联网本质上是匿名&#xff0c;去中心的&#xff0c;这与我们的现实世界完全不同。 但互联网上的主流业务却是 c/s 产销模式&#xff0c;试…

ansible基础与基础命令模块

一Ansible 1. ansible 的概念 Ansible是一个基于Python开发的配置管理和应用部署工具&#xff0c;现在也在自动化管理领域大放异彩。它融合了众多老牌运维工具的优点&#xff0c;Pubbet和Saltstack能实现的功能&#xff0c;Ansible基本上都可以实现。 Ansible能批量配置、部署、…

Power Apps 学习笔记 -- Action

文章目录 1. Action 简介2. Action 配置3. 待补充 1. Action 简介 Action基础教程 : Action概述 操作Action: 1. 操作Action类似于工作流Workflow&#xff0c;提供一些重用性的操作&#xff0c;允许工作流或其他Web服务端点调用(例如javascript). 2. Action 类似于c#当中的一个…

HTML:注释的 5 种场景和 5 点注意事项

你好&#xff0c;我是云桃桃。 HTML 代码注释是用来在 HTML 源代码中添加一些说明性文字&#xff0c;而不会显示在页面中的内容。它们不会在浏览器中显示或渲染。 现在我们一起来看看它的语法&#xff0c;用途和注意事项吧。 注释语法 HTML 注释的基本语法格式是: <!--…

微信小程序-分包

分包 1.什么是分包 分包指的是把一个完整的小程序项目&#xff0c;按照需求划分为不同的子包&#xff0c;在构建时打包成不同的分包&#xff0c;用户在使用时按需进行加载。 2.分包的好处 对小程序进行分包的好处主要有以下两点&#xff1a; 可以优化小程序首次启动的下载时间…

从焦虑到成功:一个软件测试工程师的逆袭之路

日常大家聊天时经常提及一个关键词——大环境不好&#xff0c;由此带来了很多行为的变化&#xff0c;有的人迷茫&#xff0c;有的人躺平。本文给大家介绍发生在我身上和身边的真实案例&#xff0c;希望能带给你一些输入。 案例一&#xff1a;曾经的我也极度焦虑 我是2008年参加…

java(框架) springboot-1 基础使用+mybaits使用

学习视频&#xff1a;b站黑马java教程 tomcat spring-boot工程内嵌了tomcat服务器 所有请求经过DispatcherServlet(实现servlet接口的类)(核心控制器/前端控制器)处理&#xff0c;再通过DispatcherServlet转发给各个controller。 最后通过DispatcherServlet给浏览器响应数据…

笔记79:ROS入门之前的准备

一、ROS是什么 ROS其实是一个伪操作系统&#xff0c;是基于Liunx操作系统的一个用于机器人各个节点之间通信的系统&#xff1b;ROS制定了一系列规则使得每个节点之间遵循相同的通信规则&#xff0c;使得每个人都可以有一个守则区遵守开发自己的节点&#xff0c;也能和别人开发…

【组合回溯递归】【树层去重used标记】Leetcode 40. 组合总和 II

【组合回溯递归】【树层去重used标记】Leetcode 40. 组合总和 II 解法 组合问题常用解法 树层去重 ---------------&#x1f388;&#x1f388;40. 组合总和 II 题目链接&#x1f388;&#x1f388;------------------- 解法 组合问题常用解法 树层去重 问题描述&#xff1…

代码随想录 贪心算法-中等题目-序列问题

376.摆动序列 376. 摆动序列 中等 如果连续数字之间的差严格地在正数和负数之间交替&#xff0c;则数字序列称为 摆动序列 。第一个差&#xff08;如果存在的话&#xff09;可能是正数或负数。仅有一个元素或者含两个不等元素的序列也视作摆动序列。 例如&#xff0c; [1, 7…

植物病害识别:YOLO水稻病害识别数据集(11000多张,yolo标注)

YOLO水稻病害识别数据集&#xff0c;包含叶斑病&#xff0c;褐斑病&#xff0c;细菌性枯萎病&#xff0c;东格鲁病毒病4个常见病害类别&#xff0c;共11000多张图像&#xff0c;yolo标注完整&#xff0c;可直接训练。 适用于CV项目&#xff0c;毕设&#xff0c;科研&#xff0c…