• 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()函数是用于创建线程的函数。它定义在头文件中,其声明如下:

    1. #include
    2. 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指定的函数开始执行。

            例子:

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. #include
    8. // typedef std::function func_t;
    9. using func_t = std::function<void()>;
    10. const int threadnum = 5;
    11. class ThreadData
    12. {
    13. public:
    14. ThreadData(const std::string &name, const uint64_t &ctime, func_t f)
    15. : threadname(name), createtime(ctime), func(f)
    16. {
    17. }
    18. public:
    19. std::string threadname;
    20. uint64_t createtime;
    21. func_t func;
    22. };
    23. void Print()
    24. {
    25. std::cout << "我是线程执行的大任务的一部分" << std::endl;
    26. }
    27. // 新线程
    28. void *ThreadRountine(void *args)
    29. {
    30. int a = 10;
    31. ThreadData *td = static_cast(args);
    32. while (true)
    33. {
    34. std::cout << "new thread"
    35. << " thread name: " << td->threadname << " create time: " << td->createtime << std::endl;
    36. td->func();
    37. if(td->threadname == "thread-4")
    38. {
    39. std::cout << td->threadname << " 触发了异常!!!!!" << std::endl;
    40. // a /= 0; // 故意制作异常
    41. }
    42. sleep(1);
    43. }
    44. }
    45. // 如何给线程传参,如何创建多线程呢??? -- done
    46. // 研究两个问题: 1. 线程的健壮性问题 2. 观察一下thread id
    47. // 获取返回值
    48. // 主线程
    49. int main()
    50. {
    51. std::vector<pthread_t> pthreads;
    52. for (size_t i = 0; i < threadnum; i++)
    53. {
    54. char threadname[64];
    55. snprintf(threadname, sizeof(threadname), "%s-%lu", "thread", i);
    56. pthread_t tid;
    57. ThreadData *td = new ThreadData(threadname, (uint64_t)time(nullptr), Print);
    58. pthread_create(&tid, nullptr, ThreadRountine, td);
    59. pthreads.push_back(tid);
    60. sleep(1);
    61. }
    62. std::cout << "thread id: ";
    63. for(const auto &tid: pthreads)
    64. {
    65. std::cout << tid << ",";
    66. }
    67. std::cout << std::endl;
    68. while (true)
    69. {
    70. std::cout << "main thread" << std::endl;
    71. sleep(3);
    72. }
    73. }

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

    pthread_self()

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

    1. #include
    2. // 获取当前线程ID
    3. 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做对比:

    1. #include
    2. #include
    3. #include
    4. void *ThreadRountine(void *args)
    5. {
    6. usleep(1000);
    7. int a = 10;
    8. std::string name = static_cast<const char *>(args);
    9. while (true)
    10. {
    11. std::cout << "i am a new thread, my name:" << name << " my id:" << pthread_self() << std::endl;
    12. sleep(1);
    13. }
    14. }
    15. int main()
    16. {
    17. pthread_t tid;
    18. pthread_create(&tid,nullptr,ThreadRountine,(void*)("thread -1"));
    19. while(true)
    20. {
    21. std::cout << "i am man thread," << " my id:" << pthread_self() << std::endl;
    22. sleep(1);
    23. }
    24. return 0;
    25. }

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

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

    如何终止线程?

    通过return nullptr来线程终止

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

    1. void *ThreadRountine(void *args)
    2. {
    3. usleep(1000);
    4. int a = 10;
    5. std::string name = static_cast<const char *>(args);
    6. while (a--)
    7. {
    8. std::cout << "i am a new thread, my name:" << name << " my id:" << ToHex(pthread_self()) << std::endl;
    9. sleep(1);
    10. }
    11. return nullptr;
    12. }

    通过pthread_exit()来线程终止

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

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

    1. #include
    2. // 终止当前线程的执行
    3. pthread_exit(nullptr);

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

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

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

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

    1. #include
    2. int pthread_cancel(pthread_t thread);

    参数说明:

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

    返回值:

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

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

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

    1. #include
    2. #include
    3. #include
    4. void* thread_function(void* arg) {
    5. int i;
    6. for (i = 0; i < 10; i++) {
    7. std::cout << "Thread is running..." << std::endl;
    8. sleep(1);
    9. }
    10. return NULL;
    11. }
    12. int main() {
    13. pthread_t thread;
    14. int result = pthread_create(&thread, NULL, thread_function, NULL);
    15. if (result != 0) {
    16. std::cerr << "Error creating thread!" << std::endl;
    17. return 1;
    18. }
    19. sleep(3); // 让线程运行一段时间
    20. result = pthread_cancel(thread); // 取消线程执行
    21. if (result != 0) {
    22. std::cerr << "Error cancelling thread!" << std::endl;
    23. return 1;
    24. }
    25. // 等待线程结束,并回收资源
    26. void* retval;
    27. result = pthread_join(thread, &retval);
    28. if (result != 0) {
    29. std::cerr << "Error joining thread!" << std::endl;
    30. return 1;
    31. }
    32. std::cout << "Thread has been cancelled and joined successfully." << std::endl;
    33. return 0;
    34. }

            在这个例子中,我们创建了一个新线程,并在主线程中等待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()函数用于等待一个线程的结束,并回收其资源。它的原型如下:

    1. #include
    2. int pthread_join(pthread_t thread, void **retval);

    参数说明:

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

    返回值:

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

            例子1:

    1. #include
    2. #include
    3. #include
    4. void* print_hello(void* arg) {
    5. std::cout << "Hello from thread!" << std::endl;
    6. sleep(2);
    7. return NULL;
    8. }
    9. int main() {
    10. pthread_t thread;
    11. void* retval;
    12. int ret;
    13. // 创建线程
    14. ret = pthread_create(&thread, NULL, print_hello, retval);
    15. if (ret != 0) {
    16. std::cerr << "Error creating thread!" << std::endl;
    17. return 1;
    18. }
    19. // 等待线程结束
    20. ret = pthread_join(thread, nullptr);
    21. if (ret != 0) {
    22. std::cerr << "Error joining thread!" << std::endl;
    23. return 1;
    24. }
    25. std::cout << "Thread joined successfully!" << std::endl;
    26. return 0;
    27. }

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

            例子2:

    1. #include
    2. #include
    3. #include
    4. std::string ToHex(pthread_t tid)
    5. {
    6. char id[64];
    7. snprintf(id, sizeof(id), "0x%lx", tid);
    8. return id;
    9. }
    10. class ThreadReturn
    11. {
    12. public:
    13. ThreadReturn(pthread_t id, const std::string &info, int code)
    14. : id_(id), info_(info), code_(code)
    15. {
    16. }
    17. public:
    18. pthread_t id_;
    19. std::string info_;
    20. int code_;
    21. };
    22. void *threadRoutine(void *arg)
    23. {
    24. int cnt = 5;
    25. while (cnt--)
    26. {
    27. std::cout << (const char *)arg << " is running..." << std::endl;
    28. sleep(1);
    29. }
    30. ThreadReturn *ret = new ThreadReturn(pthread_self(), "thread quit normal", 10);
    31. return ret;
    32. }
    33. int main()
    34. {
    35. // new
    36. pthread_t tid;
    37. pthread_create(&tid, nullptr, threadRoutine, (void *)("thread -1"));
    38. void *ret = nullptr;
    39. pthread_join(tid, &ret);
    40. ThreadReturn *r = static_cast(ret);
    41. std::cout << "main thread get new thread info:" << r->code_ << ", " << ToHex(r->id_) << ", " << r->info_ << std::endl;
    42. delete r;
    43. // main
    44. while (true)
    45. {
    46. std::cout << "i am man thread,"
    47. << " my id:" << ToHex(pthread_self()) << std::endl;
    48. sleep(1);
    49. }
    50. return 0;
    51. }

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

    pthread_detach()

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

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

    1. #include
    2. int pthread_detach(pthread_t thread);

    参数说明:

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

    返回值:

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

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

            例子:

    1. #include
    2. #include
    3. void* thread_function(void* arg) {
    4. // 线程执行的代码
    5. return nullptr;
    6. }
    7. int main() {
    8. pthread_t thread;
    9. int result = pthread_create(&thread, nullptr, thread_function, nullptr);
    10. if (result != 0) {
    11. std::cerr << "Error creating thread!" << std::endl;
    12. return 1;
    13. }
    14. // 分离线程
    15. result = pthread_detach(thread);
    16. if (result != 0) {
    17. std::cerr << "Error detaching thread!" << std::endl;
    18. return 1;
    19. }
    20. // 主线程的其他操作
    21. return 0;
    22. }

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

    一个小拓展

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

    线程id详解

    pthread库知识补充

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

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

     

    clone()

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

    1、函数原型

    1. #include
    2. 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提供了一种方便高效的方式来为每个线程创建独立的变量副本,从而避免了共享数据带来的竞争条件和同步问题。

    线性局部存储示例

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

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. #include
    8. using namespace std;
    9. int g_val = 100; // 全局变量,本身就是被所有线程共享的
    10. void *threadRoutine(void *args)
    11. {
    12. std::string name = static_cast<const char *>(args);
    13. sleep(1);
    14. while (true)
    15. {
    16. sleep(1);
    17. std::cout << name << ", g_val: " << g_val << " ,&g_val: " << &g_val << "\n"
    18. << std::endl;
    19. g_val++;
    20. }
    21. return nullptr;
    22. }
    23. int main()
    24. {
    25. pthread_t tid;
    26. pthread_create(&tid, nullptr, threadRoutine, (void *)"thread1");
    27. while (true)
    28. {
    29. sleep(1);
    30. std::cout << "main thread, g_val: " << g_val << " ,&g_val: " << &g_val << "\n"
    31. << std::endl;
    32. }
    33. pthread_join(tid, nullptr);
    34. return 0;
    35. }

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

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. #include
    8. using namespace std;
    9. // int g_val = 100; // 全局变量,本身就是被所有线程共享的
    10. __thread int g_val = 100; // 线程的局部存储!有什么用?有什么坑?
    11. __thread pid_t lwp = 0;
    12. // __thread std::string threadname;
    13. pid_t gettid() {
    14. return syscall(SYS_gettid);
    15. }
    16. void *threadRoutine(void *args)
    17. {
    18. std::string name = static_cast<const char *>(args);
    19. lwp = gettid(); // 调用系统调用 SYS_gettid 获取当前线程的 TID
    20. while (true)
    21. {
    22. sleep(1);
    23. std::cout << name << ", g_val: " << g_val << " ,&g_val: " << &g_val << "\n"
    24. << std::endl;
    25. std::cout <<"new thread: " << lwp << std::endl;
    26. g_val++;
    27. }
    28. return nullptr;
    29. }
    30. int main()
    31. {
    32. pthread_t tid;
    33. pthread_create(&tid, nullptr, threadRoutine, (void *)"thread1");
    34. lwp = gettid(); // 调用系统调用 SYS_gettid 获取当前线程的 TID
    35. std::cout <<"main thread: " << lwp << std::endl;
    36. while (true)
    37. {
    38. sleep(1);
    39. std::cout << "main thread, g_val: " << g_val << " ,&g_val: " << &g_val << "\n"
    40. << std::endl;
    41. }
    42. pthread_join(tid, nullptr);
    43. }


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

                                           

                                                                            给个三连再走嘛~  

  • 相关阅读:
    代码混淆界面介绍
    从零开始实现一个量化回测系统(一)
    第 365 场 LeetCode 周赛题解
    企业薪资系统
    【ARM】MDK Debug模式下Disassembly窗口介绍
    Vue:实战快速上手
    数据结构与算法——14.栈
    AD硬件电路学习笔记(二) GND的种类及PCB布线实战
    前端点击获取验证码,进行60秒倒计时
    【AUTOSAR-CAN-1】DataBase for Can——深入理解 DBC 通信矩阵
  • 原文地址:https://blog.csdn.net/weixin_64038246/article/details/136439386