Linux多线程控制:深入理解与应用(万字详解!)

                                               🎬慕斯主页修仙—别有洞天

                                              ♈️今日夜电波:どうして (feat. 野田愛実)

                                                                0:44━━━━━━️💟──────── 3:01
                                                                    🔄   ◀️   ⏸   ▶️    ☰  

                                      💗关注👍点赞🙌收藏您的每一次鼓励都是对我莫大的支持😍


目录

如何创建线程?

pthread_self()

如何终止线程?

通过return nullptr来线程终止

通过pthread_exit()来线程终止

通过pthread_cancel()来取消线程(先看后面的等待在回头看这里)

线程等待

pthread_join()

pthread_detach()

一个小拓展

线程id详解

pthread库知识补充

clone()

系统调用问题

如何理解pthread库来管理线程

前面提到的LWP为什么和pthread_self()获得的不同?

线程局部存储是啥?

__thread

线性局部存储示例


如何创建线程?

        在Linux中,可以使用POSIX线程库(pthread)来创建线程pthread_create()函数是用于创建线程的函数。它定义在<pthread.h>头文件中,其声明如下:

#include <pthread.h>int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);

    pthread_create()函数的参数具体含义如下:

  1. pthread_t *thread:这是一个指向pthread_t类型的指针,用于存储新创建线程的ID。在调用pthread_create()时,可以传递一个pthread_t类型的指针变量,或者直接传递某个pthread_t类型变量的地址。
  2. const pthread_attr_t *attr:这个参数是一个指向pthread_attr_t类型的指针,用于设置线程的属性。如果设置为NULL,则使用默认属性创建线程。
  3. void *(*start_routine) (void *):这是一个函数指针,指向新线程将要执行的函数。这个函数通常被称为线程函数,它应该接受一个void *类型的参数,并返回一个void *类型的值。
  4. void *arg:这是传递给线程函数的参数,可以是任意类型的指针。

    pthread_create()函数在成功时返回0,失败时返回错误号。如果成功创建了线程,新线程将从start_routine指定的函数开始执行。

        例子:

#include <iostream>
#include <string>
#include <functional>
#include <vector>
#include <time.h>
#include <unistd.h>
#include <pthread.h>// typedef std::function<void()> func_t;
using func_t = std::function<void()>;const int threadnum = 5;class ThreadData
{
public:ThreadData(const std::string &name, const uint64_t &ctime, func_t f): threadname(name), createtime(ctime), func(f){}public:std::string threadname;uint64_t createtime;func_t func;
};void Print()
{std::cout << "我是线程执行的大任务的一部分" << std::endl;
}// 新线程
void *ThreadRountine(void *args)
{int a = 10;ThreadData *td = static_cast<ThreadData *>(args);while (true){std::cout << "new thread"<< " thread name: " << td->threadname << " create time: " << td->createtime << std::endl;td->func();if(td->threadname == "thread-4"){std::cout << td->threadname << " 触发了异常!!!!!" << std::endl;// a /= 0; // 故意制作异常}sleep(1);}
}
// 如何给线程传参,如何创建多线程呢??? -- done
// 研究两个问题: 1. 线程的健壮性问题 2. 观察一下thread id// 获取返回值
// 主线程
int main()
{std::vector<pthread_t> pthreads;for (size_t i = 0; i < threadnum; i++){char threadname[64];snprintf(threadname, sizeof(threadname), "%s-%lu", "thread", i);pthread_t tid;ThreadData *td = new ThreadData(threadname, (uint64_t)time(nullptr), Print);pthread_create(&tid, nullptr, ThreadRountine, td);pthreads.push_back(tid);sleep(1);}std::cout << "thread id: ";for(const auto &tid: pthreads){std::cout << tid << ",";}std::cout << std::endl;while (true){std::cout << "main thread" << std::endl;sleep(3);}
}

        解析:如上代码我们按照顺序进行解读:创建了一个ThreadData类,用于存储线程的名字、创建时间以及函数指针,接下来的Print()函数就是我们主要要传递的给ThreadData对象的函数,接下来的ThreadRountine函数则是用于传递给pthread_create()函数中的void *(*start_routine) (void *)函数指针变量,由于给线程执行。需要注意的是其中有段代码是故意制作除0错误的,用于验证Linux使用多线程会造成健壮性降低的问题(只要其中一个线程出错误,那么其它线程也会收到影响,全部退出)。主函数中先是new出来ThreadData类型的对象,再将他通过pthread_create()函数中的*arg参数传递给新创建的线程。接着新线程执行对应的指令,主线程执行对应的指令。

pthread_self()

        使用pthread_self()函数可以获取当前线程的ID。下面是pthread_self()函数的声明和用法示例:

#include <pthread.h>// 获取当前线程ID
pthread_t current_thread_id = pthread_self();

        在上面的示例中,pthread_self()函数被调用时,会返回当前线程的ID,并将其存储在current_thread_id变量中。

  pthread_self()函数通常用于多线程程序中,当需要获取当前线程的ID以进行一些特定的操作时使用。例如,可以使用当前线程的ID来区分不同线程的行为,或者将其作为参数传递给其他函数或数据结构。

        需要注意的是:pthread_self()函数只能获取当前线程的ID,不能用于获取其他线程的ID。如果需要获取其他线程的ID,可以使用pthread_equal()函数进行比较,或者将线程ID作为参数传递给其他函数进行处理。

        在了解了这个函数后我们通过打印与ps -aL指令中的LWP做对比:

#include <iostream>
#include <pthread.h>
#include <unistd.h>void *ThreadRountine(void *args)
{usleep(1000);int a = 10;std::string name = static_cast<const char *>(args);while (true){std::cout << "i am a new thread, my name:" << name << " my id:" << pthread_self() << std::endl;sleep(1);}
}int main()
{pthread_t tid;pthread_create(&tid,nullptr,ThreadRountine,(void*)("thread -1"));while(true){std::cout << "i am man thread,"  << " my id:" << pthread_self() << std::endl;sleep(1);}return 0;
}

        我们发现左半边为一串很大的数字,与右边完全不同:

        接着将左半边的数字转换为16进制看看?我们发现他同地址很像,他们之间难道有什么关联吗?是的,因为thread id的本质就是一个地址!

如何终止线程?

通过return nullptr来线程终止

        如下:在对应传入的函数中返回nullptr,需要特别注意:我们能使用exit()函数来退出吗?答案是不能!因为exit()是“进程终止”如果调用,整个进程都会退出!

void *ThreadRountine(void *args)
{usleep(1000);int a = 10;std::string name = static_cast<const char *>(args);while (a--){std::cout << "i am a new thread, my name:" << name << " my id:" << ToHex(pthread_self()) << std::endl;sleep(1);}return nullptr;
}

通过pthread_exit()来线程终止

    pthread_exit()是POSIX线程库中的一个函数,用于终止当前线程的执行。

        下面是pthread_exit()函数的声明和用法示例:

#include <pthread.h>// 终止当前线程的执行
pthread_exit(nullptr);

        在上面的示例中,pthread_exit()函数被调用时,会立即终止当前线程的执行,并返回到主线程或调用者。传递给pthread_exit()函数的参数是一个指向返回值的指针,这个返回值可以被其他线程通过pthread_join()函数获取。如果不需要传递返回值,可以传递nullptr作为参数。

        需要注意的是:pthread_exit()函数不会释放线程所占用的资源,如堆栈、文件描述符等。这些资源的释放需要程序员手动进行。

通过pthread_cancel()来取消线程(先看后面的等待在回头看这里)

    pthread_cancel()函数用于取消一个线程的执行。它的原型如下:

#include <pthread.h>
int pthread_cancel(pthread_t thread);

参数说明:

  • thread:需要取消执行的线程ID。

返回值:

  • 成功时返回0;失败时返回错误码。

        使用pthread_cancel()函数可以强制终止一个线程的执行,但需要注意的是:该函数并不会释放线程所占用的资源,如堆栈、线程描述符等。因此,在线程被取消后,还需要调用其他函数来回收这些资源。

        下面是一个使用pthread_cancel()函数的例子:

#include <iostream>
#include <pthread.h>
#include <unistd.h>void* thread_function(void* arg) {int i;for (i = 0; i < 10; i++) {std::cout << "Thread is running..." << std::endl;sleep(1);}return NULL;
}int main() {pthread_t thread;int result = pthread_create(&thread, NULL, thread_function, NULL);if (result != 0) {std::cerr << "Error creating thread!" << std::endl;return 1;}sleep(3); // 让线程运行一段时间result = pthread_cancel(thread); // 取消线程执行if (result != 0) {std::cerr << "Error cancelling thread!" << std::endl;return 1;}// 等待线程结束,并回收资源void* retval;result = pthread_join(thread, &retval);if (result != 0) {std::cerr << "Error joining thread!" << std::endl;return 1;}std::cout << "Thread has been cancelled and joined successfully." << std::endl;return 0;
}

        在这个例子中,我们创建了一个新线程,并在主线程中等待3秒钟后调用pthread_cancel()函数来取消该线程的执行。然后,我们使用pthread_join()函数等待线程结束,并回收其资源。

        需要注意的是:线程如果是被分离的,他是可以被取消的,但是不能被join。thread线程以不同的方法终止,通过pthread_join得到的终止状态是不同的,总结如下:

  1. 如果thread线程通过return返回,value_ ptr所指向的单元里存放的是thread线程函数的返回值。
  2. 如果thread线程被别的线程调用pthread cancel异常终掉,value ptr所指向的单元里存放的是常数
    PTHREAD_ CANCELED。
  3. 如果thread线程是自己调用pthread_exit终止的,value_ptr所指向的单元存放的是传给pthread_exit的参数。
  4. 如果对thread线程的终止状态不感兴趣,可以传NULL给value_ ptr参数。

线程等待

        我们都知道进程退出,他的PCB不会立即释放,会处于僵尸状态,进程要等待。那么线程也需要等待吗?

        是的,线程也是需要等待的!因为:线程退出没有等待,会导致累充进程的僵尸问题我们可以通过pthread_join()来等待线程!

pthread_join()

    pthread_join()函数用于等待一个线程的结束,并回收其资源。它的原型如下:

#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);

参数说明:

  • thread:需要等待的线程ID。
  • retval:指向一个指针的指针,用于存储被等待线程的返回值。如果不关心返回值,可以设置为nullptr。(为什么是void ** 类型呢?因为我们通过pthread_create传入线程的函数的返回值是void *类型的返回值,我们用void **就可以接收这个函数的返回值,retval是一个输出型的参数)

返回值:

  • 成功时返回0;失败时返回错误码。

        例子1:

#include <iostream>
#include <pthread.h>
#include <unistd.h>void* print_hello(void* arg) {std::cout << "Hello from thread!" << std::endl;sleep(2);return NULL;
}int main() {pthread_t thread;void* retval;int ret;// 创建线程ret = pthread_create(&thread, NULL, print_hello, retval);if (ret != 0) {std::cerr << "Error creating thread!" << std::endl;return 1;}// 等待线程结束ret = pthread_join(thread, nullptr);if (ret != 0) {std::cerr << "Error joining thread!" << std::endl;return 1;}std::cout << "Thread joined successfully!" << std::endl;return 0;
}

        在这个例子中,我们创建了一个新线程,该线程执行print_hello函数。然后,我们使用pthread_join()函数等待线程结束。当线程结束时,pthread_join()函数返回0,表示成功。

        例子2:

#include <iostream>
#include <pthread.h>
#include <unistd.h>std::string ToHex(pthread_t tid)
{char id[64];snprintf(id, sizeof(id), "0x%lx", tid);return id;
}class ThreadReturn
{
public:ThreadReturn(pthread_t id, const std::string &info, int code): id_(id), info_(info), code_(code){}public:pthread_t id_;std::string info_;int code_;
};void *threadRoutine(void *arg)
{int cnt = 5;while (cnt--){std::cout << (const char *)arg << " is running..." << std::endl;sleep(1);}ThreadReturn *ret = new ThreadReturn(pthread_self(), "thread quit normal", 10);return ret;
}int main()
{// newpthread_t tid;pthread_create(&tid, nullptr, threadRoutine, (void *)("thread -1"));void *ret = nullptr;pthread_join(tid, &ret);ThreadReturn *r = static_cast<ThreadReturn *>(ret);std::cout << "main thread get new thread info:" << r->code_ << ", " << ToHex(r->id_) << ", " << r->info_ << std::endl;delete r;// mainwhile (true){std::cout << "i am man thread,"<< " my id:" << ToHex(pthread_self()) << std::endl;sleep(1);}return 0;
}

        本例返回的是一个ThreadReturn *的值,运行效果如下:

pthread_detach()

        线程默认的模式是joinable的,也就是说线程退出必须得等待,主线程必须是阻塞的等待新线程的。但是,我们也是可以设置为分离状态的!即:我们可以设置为非阻塞状态的,线程在退出时,对应的资源会被直接被回收。

    pthread_detach()函数用于将线程设置为分离状态,从而在线程终止时自动回收其资源。它的原型如下:

#include <pthread.h>
int pthread_detach(pthread_t thread);

参数说明:

  • thread:需要设置为分离状态的线程ID。

返回值:

  • 成功时返回0;失败时返回错误码。

需要注意的是:可以线程自己分离也可以由主线程分离。

        例子:

#include <iostream>
#include <pthread.h>void* thread_function(void* arg) {// 线程执行的代码return nullptr;
}int main() {pthread_t thread;int result = pthread_create(&thread, nullptr, thread_function, nullptr);if (result != 0) {std::cerr << "Error creating thread!" << std::endl;return 1;}// 分离线程result = pthread_detach(thread);if (result != 0) {std::cerr << "Error detaching thread!" << std::endl;return 1;}// 主线程的其他操作return 0;
}

        在这个例子中,我们首先创建了一个新线程,然后立即将其分离。这样,当线程结束时,它的资源会被自动回收,而不需要主线程显式等待其结束。

一个小拓展

        我们都知道进程退出会有退出码,那线程退出会有退出码吗?答案是没有!因为如果线程因为异常终止了,那么整个进程也会跟着终止,所以不需要退出码!

线程id详解

pthread库知识补充

        前面我们提到的对于线程控制的接口,实际上都不是系统直接提供的接口,而是原生线程库pthread(系统会自带这个库)提供的接口。图示如下:

        通过pthread库,可以对线程进行管理,我们通过“先描述,在组织”的原则会在pthread库里面实现对应的结构体对象来描述,再通过一定的数据结构来组织。里面会包涵系统中“轻量级进程”的信息也会包涵用户的“用户级线程”信息。

 

clone()

        clone()函数是Linux中的一个系统调用,用于创建新的执行线程或进程。它是fork()系统调用的泛化形式,具有更高的灵活性。实际上进程与线程的创建都是它的封装。以下是对clone()函数的详细解析:

1、函数原型

#include <sched.h>
int clone(int (*fn)(void *), void *child_stack, int flags, void *arg, ... /* pid_t *pid, struct user_desc *tls, pid_t *ctid */); 

2、参数说明

    • (*fn)(void *):子进程(或线程)执行时调用的函数。
    • child_stack:为子进程分配的堆栈指针。
    • flags:控制新进程与原进程之间的共享资源以及其它行为的标志位集合。
    • arg:传给子进程的参数,一般为0。
    • ...:可选的附加参数,包括 pid_t *pid, struct user_desc *tls, pid_t *ctid

3、flags参数详解

    • CLONE_PARENT:创建的子进程的父进程是调用者的父进程,使新进程与创建它的进程成为“兄弟”关系。
    • CLONE_FS:子进程与父进程共享相同的文件系统,包括root、当前目录、umask等。

        4、与fork()的区别:直接调用fork()等效于调用clone()时仅指定flags为SIGCHLD(共享信号句柄表)。fork()是Unix标准的复制进程的系统调用,而Linux实现了fork(), vfork(), clone三个系统调用。其中vfork()创造出来的是轻量级进程,也叫线程,是共享资源的进程。

        5、使用场景:clone()通常用于实现多线程编程,因为它可以精细地控制哪些资源是共享的,哪些是私有的。这在多线程编程中是非常重要的,因为它允许创建高度定制的线程行为。

        6、glibc封装:从Linux 2.3.3开始,glibc的fork()封装作为NPTL(Native POSIX Threads Library)线程实现的一部分。创建线程的函数pthread_create内部使用的也是clone函数。

系统调用问题

        前面我们谈到线程虽然有很多的共享资源,但是也要有独立的属性,其中最重要的是:1、上下文。2、栈。

        其中栈是每个新线程会在pthread库中维护的,而默认地址空间中的栈由主线程使用。前面在学习Linux动静态库的时候提到:加载库会将库加载到栈与堆之间的共享区中,pthread库当然也是,而库中的栈则是在其所属进程的虚拟地址空间中分配

如何理解pthread库来管理线程

        如下这张图想必大家都已经很熟悉了,我们在磁盘上存储的pthread库以及使用到pthread库的可执行程序都会被加载到物理内存中,然后通过页表映射到地址空间上。动态库,也叫共享库,只要在物理内存中映射了一次,之后都会被其他进程所共享。因此,pthread库会管理整个系统中所创建的进程!理解上:线程库是共享的,所以,内部要管理整个系统,多个用户所启动的所有线程。

        如下为较为详细的pthread理解:其中mmap区域就是共享区,而其中动态库中struct_pthread可以理解为“先描述”,也可以理解为“TCB”。线程栈可以理解为一个指针,指向对应栈的地址。struct_pthread、线程局部存储和线程栈可以理解为一个一个的块每一个线程都有。与接下来的一个一个的块被管理起来,可以理解为“在组织”。这些属性都会被库所维护。

        如上struct_pthread会储存对应的退出信息,而我们的pthread_join()函数接口就是读取上面struct_pthread中的信息。如何找到的呢?我们是根据pthread_t tid来找到的,pthread_t tid就是线程属性集合在库中的地址!而其他pthread库中的接口也就是根据这个就是对库中的这些数据来进行维护的!

前面提到的LWP为什么和pthread_self()获得的不同?

        pthread_ create函数会产生一个线程ID,存放在第一个参数指向的地址中。该线程ID和前面说的线程ID不是一回事。

        前面讲的线程ID属于进程调度的范畴。因为线程是轻量级进程,是操作系统调度器的最小单位,所以需要一个数值来唯一表示该线程。

        pthread_ create函数第一个参数指向一个虚拟内存单元,该内存单元的地址即为新创建线程的线程ID,属于NPTL线程库的范畴。线程库的后续操作,就是根据该线程ID来操作线程的。

 

线程局部存储是啥?

__thread

    __thread是GCC提供的线程局部存储(Thread-Local Storage, TLS)的关键字

    __thread用于声明线程局部变量,这意味着每个线程都会有该变量的一个独立实例。不同线程中的__thread变量互不干扰,各自保有自己独立的值,这对于多线程编程中需要为每个线程保持独立状态的场景非常有用。

        具体来说,__thread关键字的使用场景和限制包括:

  • 应用场景:适合修饰那些具有全局性质且值可能会发生变化的变量,但又不需要像全局变量那样进行保护的情况。
  • 效率优势__thread变量的存取效率可以与全局变量相媲美,这使得它在性能上非常有吸引力。
  • 使用限制:只能修饰POD(Plain Old Data)类型,即那些不含有自定义构造、拷贝、赋值、析构函数的简单数据类型。因为__thread无法自动调用类的构造和析构函数,所以它不能用来修饰类类型的变量。
  • 作用范围:可以用来修饰全局变量和函数内的静态变量,但不能修饰函数的局部变量或类的普通成员变量。
  • 初始化限制__thread变量的值只能初始化为编译器常量。

        总的来说,在多线程编程中,__thread提供了一种方便高效的方式来为每个线程创建独立的变量副本,从而避免了共享数据带来的竞争条件和同步问题。

线性局部存储示例

        如下为正常的全局变量在多线程情况下的示例:也印证了多线程共享资源的特性!

#include <iostream>
#include <string>
#include <cstdlib>
#include <unistd.h>
#include <sys/types.h>
#include <sys/syscall.h>
#include <pthread.h>using namespace std;int g_val = 100; // 全局变量,本身就是被所有线程共享的void *threadRoutine(void *args)
{std::string name = static_cast<const char *>(args);sleep(1);while (true){sleep(1);std::cout << name << ", g_val: " << g_val << " ,&g_val: " << &g_val << "\n"<< std::endl;g_val++;}return nullptr;
}int main()
{pthread_t tid;pthread_create(&tid, nullptr, threadRoutine, (void *)"thread1");while (true){sleep(1);std::cout << "main thread, g_val: " << g_val << " ,&g_val: " << &g_val << "\n"<< std::endl;}pthread_join(tid, nullptr);return 0;
}

        如下为使用__thread的示例:可以发现本来共享的全局变量变成了线性局部的变量,值和地址都会变化!其中拓展了对于获取LWP通过调用系统调用 SYS_gettid 获取当前线程的 TID

#include <iostream>
#include <string>
#include <cstdlib>
#include <unistd.h>
#include <sys/types.h>
#include <sys/syscall.h>
#include <pthread.h>using namespace std;// int g_val = 100; // 全局变量,本身就是被所有线程共享的
__thread int g_val = 100; // 线程的局部存储!有什么用?有什么坑?__thread pid_t lwp = 0;// __thread std::string threadname;pid_t gettid() {return syscall(SYS_gettid);
}void *threadRoutine(void *args)
{std::string name = static_cast<const char *>(args);lwp = gettid(); // 调用系统调用 SYS_gettid 获取当前线程的 TIDwhile (true){sleep(1);std::cout << name << ", g_val: " << g_val << " ,&g_val: " << &g_val << "\n"<< std::endl;std::cout <<"new thread: " << lwp << std::endl;g_val++;}return nullptr;
}int main()
{pthread_t tid;pthread_create(&tid, nullptr, threadRoutine, (void *)"thread1");lwp = gettid(); // 调用系统调用 SYS_gettid 获取当前线程的 TIDstd::cout <<"main thread: " << lwp << std::endl;while (true){sleep(1);std::cout << "main thread, g_val: " << g_val << " ,&g_val: " << &g_val << "\n"<< std::endl;}pthread_join(tid, nullptr);
}


                       感谢你耐心的看到这里ღ( ´・ᴗ・` )比心,如有哪里有错误请踢一脚作者o(╥﹏╥)o! 

                                       

                                                                        给个三连再走嘛~  

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

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

相关文章

6.2 指标的应用与设计(12%)

1、指标的作用 用简约的汇总数据量化业务强弱。 2、指标的理解 特点&#xff1a; &#xff08;1&#xff09;指标是游离态的&#xff0c;无法单独实现数据统计 eg&#xff1a;总销售额、总销售量 &#xff08;2&#xff09;需与统计维度结合&#xff0c;明确统计指标的对…

047 内部类

成员内部类用法 /*** 成员内部类** author Admin*/ public class OuterClass {public void say(){System.out.println("这是类的方法");}class InnerClass{public void say(){System.out.println("这是成员内部类的方法");}}public static void main(Stri…

(二)逻辑回归与交叉熵--九五小庞

什么是逻辑回归 线性回归预测的是一个连续值&#xff0c;逻辑回归给出的“是”和“否”的回答 Singmoid sigmoid函数是一个概率分布函数&#xff0c;给定某个输入&#xff0c;它将输出为一个概率值 逻辑回归损失函数 平方差所惩罚的是与损失为同一数量级的情形&#xff0…

SandBox中的JavaAgent技术

8.1 JavaAgent Java Agent 是一种强大的技术&#xff0c;在运行时动态修改已加载类的字节码&#xff0c;为应用程序注入额外的功能和行为。 JDK 1.5 支持静态 Instrumentation&#xff0c;基本的思路是在 JVM 启动的时候添加一个代理&#xff08;javaagent&#xff09;&#…

基于阿里云OSS上传图片实战案例

一、案例描述 基于Springboot框架实现一个上传图片到阿里云服务端保存的小案例。 二、准备工作 基于Springboot免费搭载轻量级阿里云OSS数据存储库&#xff08;将本地文本、照片、视频、音频等上传云服务保存&#xff09;-CSDN博客 三、代码 新建这两个类&#xff1a;一个…

LANA: A Language-Capable Navigator for Instruction Following and Generation

摘要 最近&#xff0c;视觉语言导航&#xff08;VLN&#xff09;——要求机器人代理遵循导航指令——已经取得了巨大的进步。然而&#xff0c;现有文献最强调将指令解释为行动&#xff0c;只提供“愚蠢”的寻路代理。在本文中&#xff0c;我们设计了 LANA&#xff0c;一种支持…

【LeetCode-1143】最长公共子序列(动归)

目录 题目描述 解法1&#xff1a;动态规划 代码实现 题目链接 题目描述 给定两个字符串 text1 和 text2&#xff0c;返回这两个字符串的最长公共子序列的长度。 一个字符串的 子序列 是指这样一个新的字符串&#xff1a;它是由原字符串在不改变字符的相对顺序的情况下删除…

社区店选址评估:利用大数据选址的技巧与策略

在当今数字化的时代&#xff0c;利用大数据进行社区店选址评估已成为一种高效、科学的方法。作为一名开鲜奶吧5年的创业者&#xff0c;我将分享一些利用大数据选址的技巧与策略&#xff0c;帮助你找到最适合的店铺位置。 1、确定目标商圈 在选址之前&#xff0c;首先要明确自己…

爬虫的一些小技巧总结

一、在爬虫中&#xff0c;爬取的数据类型如下 1.document:返回的是一个HTML文档 2.png:无损的图片&#xff0c;jpg:压缩后的图片,wbep:有损压缩&#xff0c;比png差&#xff0c;比jpg好 3.avgxml图像编码字符串 4.script:脚本文件&#xff0c;依据一定格式编写的可执行的文…

【大厂AI课学习笔记NO.58】(11)混淆矩阵

混淆矩阵&#xff08;confusion matrix&#xff09;—— 混淆矩阵&#xff08;Confusion Matrix&#xff09;是人工智能领域&#xff0c;特别是在机器学习和深度学习中&#xff0c;用于衡量分类模型性能的重要工具。它通过统计分类模型的真实分类与预测分类之间的结果&#xf…

【python debug】python常见编译问题解决方法_2

序言 记录python使用过程中碰到的一些问题及其解决方法上一篇&#xff1a;python常见编译问题解决方法_1 1. PermissionError: [Errno 13] Permission denied: ‘/lostfound’ 修改前&#xff1a; 修改后&#xff08;解决&#xff09;&#xff1a; 此外&#xff0c;可能文件夹…

leetcode 热题 100_接雨水

题解一&#xff1a; 按列求&#xff1a;分别考虑每一列的雨水高度&#xff0c;某列的雨水高度只与其左侧最高墙和右侧最高墙有关&#xff0c;一种情况是该列比左右侧的墙都低&#xff0c;则根据木桶效应该列雨水高度为min(左侧墙高&#xff0c;右侧墙高)-列高&#xff0c;而其余…

智能驾驶及相关零部件摄像头毫米波雷达激光雷达和芯片渗透率

一、总体情况 乘联会数据显示&#xff0c;1月1日至1月28日&#xff0c;全国乘用车厂商新能源车批发销量为56.7万辆&#xff0c;同比增长76%&#xff0c;环比下降38%&#xff1b;国内新能源车市场零售销量为59.6万辆&#xff0c;同比增长92%&#xff0c;环比下降24%。 二、销…

【word】引用文献如何标注右上角

一、在Word文档中引用文献并标注在右上角的具体步骤如下 1、将光标移动到需要添加文献标注的位置&#xff1a; 2、在文档上方的工具栏中选择“引用”选项&#xff1a; 3、点击“插入脚注”或“插入尾注”&#xff1a; ①如果选择的是脚注&#xff0c;则脚注区域会出现在本页的…

多路转接之epoll

常用的三个API&#xff1a; epoll_create(); //例如 int epfd epoll(10);创建一棵有10个结点的红黑树&#xff0c;注意&#xff1a;这个数只是对内核建议的数值&#xff0c;内核参照这个参数去构建epoll_ctrl();//参数2 op可以取值 EPOLL_CTL_ADD/MOD/DELevents:EPOLLIN/…

Pyglet图形界面版2048游戏——详尽实现教程(上)

目录 Pyglet图形界面版2048游戏 一、色块展示 二、绘制标题 三、方阵色块 四、界面布局 五、键鼠操作 Pyglet图形界面版2048游戏 一、色块展示 准备好游戏数字的背景颜色&#xff0c;如以下12种&#xff1a; COLOR ((206, 194, 180, 255), (237, 229, 218, 255), (23…

SpringCloud负载均衡源码解析 | 带你从表层一步步剖析Ribbon组件如何实现负载均衡功能

目录 1、负载均衡原理 2、源码分析 2.1、LoadBalanced 2.2、LoadBalancerClient 2.3、RibbonAutoConfiguration 2.4、LoadBalancerAutoConfiguration 2.5、LoadBalancerIntercepor⭐ 2.6、再回LoadBalancerClient 2.7、RibbonLoadBalancerClient 2.7.1、DynamicServe…

OpenCV 4基础篇| OpenCV图像的拼接

目录 1. Numpy (np.hstack&#xff0c;np.vstack)1.1 注意事项1.2 代码示例 2. matplotlib2.1 注意事项2.2 代码示例 3. 扩展示例&#xff1a;多张小图合并成一张大图4. 总结 1. Numpy (np.hstack&#xff0c;np.vstack) 语法结构&#xff1a; retval np.hstack(tup) # 水平…

c++之通讯录管理系统

1&#xff0c;系统需求 通讯录是一个记录亲人&#xff0c;好友信息的工具 系统中需要实现的功能如下&#xff1a; 1&#xff0c;添加联系人&#xff1a;向通讯录中添加新人&#xff0c;信息包括&#xff08;姓名&#xff0c;性别&#xff0c;年龄&#xff0c;联系电话&#…

构建高效的接口自动化测试框架思路

在选择接口测试自动化框架时&#xff0c;需要根据团队的技术栈和项目需求来综合考虑。对于测试团队来说&#xff0c;使用Python相关的测试框架更为便捷。无论选择哪种框架&#xff0c;重要的是确保 框架功能完备&#xff0c;易于维护和扩展&#xff0c;提高测试效率和准确性。今…