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

news2024/11/15 13:54:46

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

                                              ♈️今日夜电波:どうして (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()
{
    // new
    pthread_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;

    // main
    while (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 获取当前线程的 TID
    while (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 获取当前线程的 TID
    std::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.coloradmin.cn/o/1486242.html

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

相关文章

基于redis实现【最热搜索】和【最近搜索】功能

目录 一、前言二、分析问题三、针对两个问题&#xff0c;使用redis怎么解决问题&#xff1f;1、字符串String2、列表List3、字典Hash4、集合Set5、有序集合ZSet6、需要解决的五大问题 四、编写代码1.pom依赖2.application.yml配置3.Product商品实体4.用户最近搜索信息5.redis辅…

C-V2X系列:C-V2X芯片及模组整理总结

C-V2X、车路协同、车联网、智能网联车学习 C-V2X芯片及模组整理总结

Typora旧版链接(Win+Mac+Linux版)

记得点赞本文&#xff01;&#xff01;&#xff01; 链接&#xff1a;https://pan.baidu.com/s/1IckUvQUBzQkfHNHXla0zkA?pwd8888 提取码&#xff1a;8888 –来自百度网盘超级会员V7的分享

2.模拟问题——4.日期问题

日期问题难度并不大&#xff0c;但是代码量非常大&#xff0c;需要较高的熟练度&#xff0c;因此需要着重练习&#xff0c;主要涉及数组和循环两个方面的知识点&#xff0c;需要熟练的测试代码。 两个经典题型 闰年 闰年满足以下两个条件的任意一个 能够被400整除不能够被1…

Golang Vs Java:为您的下一个项目选择正确的工具

Java 首次出现在 1995 年&#xff0c;由 James Gosling 和 Sun Microsystems 的其他人开发的一种新编程语言。从那时起&#xff0c;Java 已成为世界上最受欢迎和广泛使用的编程语言之一。Java 的主要特点包括其面向对象的设计、健壮性、平台独立性、自动内存管理以及广泛的内置…

JavaSec 基础之 JNDI 注入

文章目录 JNDI简介JNDI 支持的服务协议JNDI 注入JNDI 复现修复 JNDI 简介 JNDI(Java Naming and Directory Interface)是一个应用程序设计的 API&#xff0c;一种标准的 Java 命名系统接口。JNDI 提供统一的客户端 API&#xff0c;通过不同的访问提供者接口JNDI服务供应接口(…

武器大师——操作符详解(下)

目录 六、单目操作符 七、逗号表达式 八、下标引用以及函数调用 8.1.下标引用 8.2.函数调用 九、结构体 9.1.结构体 9.1.1结构的声明 9.1.2结构体的定义和初始化 9.2.结构成员访问操作符 9.2.1直接访问 9.2.2间接访问 十、操作符的属性 10.1.优先性 10.2.结合性 …

Ubuntu20.04使用XRDP安装原生远程桌面

Ubuntu20.04使用XRDP安装原生远程桌面 1.安装gnome桌面 # 如果没有更新过源缓存&#xff0c;先更新一下 sudo apt update# 安装gnome桌面 # 可选参数 --no-install-recommends&#xff0c;不安装推荐组件&#xff0c;减少安装时间和空间占用 sudo apt install ubuntu-desktop…

2.2_5 调度算法

文章目录 2.2_5 调度算法一、适用于早期的批处理系统&#xff08;一&#xff09;先来先服务&#xff08;FCFS&#xff0c;First Come First Serve&#xff09;&#xff08;二&#xff09;短作业优先&#xff08;SJF&#xff0c;Shortest Job First&#xff09;&#xff08;三&a…

力扣706:设计哈希映射

题目&#xff1a; 不使用任何内建的哈希表库设计一个哈希映射&#xff08;HashMap&#xff09;。 实现 MyHashMap 类&#xff1a; MyHashMap() 用空映射初始化对象void put(int key, int value) 向 HashMap 插入一个键值对 (key, value) 。如果 key 已经存在于映射中&#x…

设计模式学习笔记 - 设计原则 - 8.迪米特法则(LOD)

前言 迪米特法则&#xff0c;是一个非常实用的原则。利用这个原则&#xff0c;可以帮我们实现代码的 “高内聚、松耦合”。 围绕下面几个问题&#xff0c;来学习迪米特原则。 什么是 “高内聚、松耦合”&#xff1f;如何利用迪米特法则来实现 高内聚、松耦合&#xff1f;哪些…

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

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

开发者38万+,鸿蒙开发岗为何却无人敢应聘?

鸿蒙校园公开课已走进135家高校&#xff0c;305所高校学生参与鸿蒙活动&#xff0c;286家企业参加鸿蒙生态学堂&#xff0c;38万开发者通过鸿蒙认证。 居上华为官方是说有通过鸿蒙开发者认证的已有38万。具体有多少开发者并没有明确表示。除此之外还有200家头部应用加速鸿蒙原…

机器人 标准DH与改进DH

文章目录 1 建立机器人坐标系1.1 连杆编号1.2 关节编号1.3 坐标系方向2 标准DH(STD)2.1 确定X轴方向2.2 建模步骤2.3 变换顺序2.4 变换矩阵3 改进DH(MDH)3.1 确定X轴方向3.2 建模步骤3.3 变换顺序3.4 变换矩阵4 标准DH与改进DH区别5 Matlab示例参考链接1 建立机器人坐标系 1.1…

Java二叉树(1)

&#x1f435;本篇文章将对二叉树的相关概念、性质和遍历等知识进行讲解 一、什么是树 在讲二叉树之前&#xff0c;先了解一下什么是树&#xff1a;树是一种非线性结构&#xff0c;其由许多节点和子节点组成&#xff0c;整体形状如一颗倒挂的树&#xff0c;比如下图&#xff1…

探索设计模式的魅力:备忘录模式揭秘-实现时光回溯、一键还原、后悔药、历史的守护者和穿越时空隧道

​&#x1f308; 个人主页&#xff1a;danci_ &#x1f525; 系列专栏&#xff1a;《设计模式》 &#x1f4aa;&#x1f3fb; 制定明确可量化的目标&#xff0c;并且坚持默默的做事。 备忘录模式揭秘-实现时光回溯、一键还原、后悔药和穿越时空隧道 文章目录 一、案例场景&…

Docker架构概述

Docker是基于Go语言实现的开源容器项目&#xff0c;能够把开发的应用程序自动部署到容器的开源的应用容器引擎。Docker的构想是要实现"Build, Ship and Run Any App, Anywhere"&#xff0c;即通过对应用的封装(Packaging)、分发(Distribution)、部署(Deployment)、运…

Autosar Appl介绍

AUTOSAR架构中的应用层 AUTOSAR 应用层构成AUTOSAR 架构中的最顶层,被认为对所有车辆应用至关重要。AUTOSAR 标准使用“组件”概念指定应用层实现。 在谈论应用层实现时,应该考虑的三个最重要的部分是: AUTOSAR 应用软件组件这些组件的 AUTOSAR 端口AUTOSAR 端口接口 AUTOS…

LeetCode受限条件下可到达节点的数目

题目描述 现有一棵由 n 个节点组成的无向树&#xff0c;节点编号从 0 到 n - 1 &#xff0c;共有 n - 1 条边。 给你一个二维整数数组 edges &#xff0c;长度为 n - 1 &#xff0c;其中 edges[i] [ai, bi] 表示树中节点 ai 和 bi 之间存在一条边。另给你一个整数数组 restr…

Game With Sticks

最近思维实在是不活跃。。。。。。 题目链接&#xff1a;Submit - Codeforces 解题思路&#xff1a; 如果n > m,交换 直接判断n就行&#xff0c;偶数M赢&#xff0c;奇数A赢 下面是c代码&#xff1a; #include<iostream> using namespace std; int main() {int n…