【Linux】从零开始认识多线程 --- 线程控制

在这里插入图片描述

在这个浮躁的时代
只有自律的人才能脱颖而出
-- 《觉醒年代》

从零开始认识多线程 --- 线程控制

  • 1 知识回顾
  • 2 线程控制
    • 2.1 线程创建
    • 2.2 线程等待
    • 2.3 线程终止
  • 3 测试运行
    • 3.1 小试牛刀 --- 创建线程
    • 3.2 探幽析微 --- 理解线程参数
    • 3.3 小有心得 --- 探索线程返回
    • 3.4 求索无厌 --- 实现多线程
    • 3.5 返璞归真 --- 线程终止与线程分离
  • 4 语言层的线程封装
  • Thanks♪(・ω・)ノ谢谢阅读!!!
  • 下一篇文章见!!!

1 知识回顾

上一篇文章中,我们通过对地址空间的再次学习来认识了线程:

  1. 物理空间不是连续的,是4kb的内存块(页框)组成的。
  2. 页表映射是通过虚拟地址来索引物理地址:
    • 虚拟地址共32位:前10位用来索引页目录中的元素(页表),中间10位用来索引页表中的对应的元素(页框),后12位用来索引页框中的每一个字节
  3. 虚拟地址本质是一种资源,可以进行分配!对一个进程的数据进行分配执行,就是多线程的本质!
  4. Linux中的线程是通过进程模拟的(并没有单独设计出一个单独的线程模块)
  5. 进程中可以有多个进程(之前学习的是进程的特殊情况),他们共用一个地址空间。进程从内核来看,是承担分配系统资源的基本实体!
  6. Linux中的执行流是线程 ,CPU看到的执行流 <= 进程

进程与线程需要注意:

  1. 线程的调度成本比进程低很多,是由于硬件原因:CPU中存在一个cache会储存热点数据(进程相关数据) ,要访问数据时,会先在cache中寻找,如果命中直接访问,反之进行置换。切换进程需要更换热点数据,切换线程不需要切换。
  2. 线程的健壮性很差!一个线程出错会导致整个线程退出,而不同进程是独立的互不影响!进程和线程各有特长!
  3. 线程的本质是代码块!只使用函数的对应代码,即拿页表的一部分来执行!!!
  4. 线程的使用场景多为计算密集型和IO密集型,可以充分使用CPU的并行能力!

同一个进程中的线程虽然共享一个地址空间,但是还是有独属于自己的一些东西:

  1. 一组寄存器:在硬件中储存上下文数据,保证线程可以动态并行运行!
  2. 栈空间:线程中可以处理自己的临时变量,临时变量储存在自己独立的栈区,可以独立完成任务。
  3. 线程ID
  4. errno信号屏蔽字
  5. 调度优先级

复习的差不多了,我们了解了线程的基本概念,接下来就要开始学习如何管理线程 — 线程控制。根据我们之前学习的进程控制,大概可以估计一下线程控制的基本接口:线程创建 , 线程等待 , 线程退出…

2 线程控制

2.1 线程创建

万事开头难,我们先来看线程怎么创建:

PTHREAD_CREATE(3)                                                   Linux Programmer's Manual                                                  PTHREAD_CREATE(3)NAMEpthread_create - create a new threadSYNOPSIS#include <pthread.h>int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine) (void *), void *arg);Compile and link with -pthread.

pthread_create是创建线程的接口,里面有4个参数:

  1. pthread_t *thread :输出型参数,线程ID。
  2. const pthread_attr_t *attr :线程属性(优先级,上下文…),默认传入nullptr
  3. void *(*start_routine) (void *) : 函数指针,线程需要执行的函数地址。
  4. void arg:想要传入到线程的信息,可以传入int,string地址或者传入一个类对象的地址。

再来看返回值:

RETURN VALUEOn success, pthread_create() returns 0; on error, it returns an error number, and the contents of *thread are undefined.

pthread系列的函数的返回值是都是一样的:成功返回0,反之返回错误码!

2.2 线程等待

学习进程的时候,如果进程创建出来了,但是不进行等待,就拿不到退出信息,还会造成僵尸进程,进而造成内存泄漏。同样线程也需要进行等待。由主线程来等待新线程

PTHREAD_JOIN(3)                                                     Linux Programmer's Manual                                                    PTHREAD_JOIN(3)NAMEpthread_join - join with a terminated threadSYNOPSIS#include <pthread.h>int pthread_join(pthread_t thread, void **retval);Compile and link with -pthread.

这个函数里面有2个参数:

  1. pthread_t thread:需要进行等待的线程ID
  2. void **retval: 获取的返回信息

2.3 线程终止

牢记:main线程结束那么进程结束,所以一定要保证main线程最后退出。

  1. 最简单的线程终止是线程函数返回return
  2. 切记不要使用exit(),我们在进程控制中学习过exit()可以退出进程,但是要注意线程是在一个进程中讨论的,新线程如果使用了exit()那整个进程就退出了!exit()不可以用来终止线程
  3. 操作系统也给我们提供了线程终止的接口:
PTHREAD_CANCEL(3)                                                   Linux Programmer's Manual                                                  PTHREAD_CANCEL(3)NAMEpthread_cancel - send a cancellation request to a threadSYNOPSIS#include <pthread.h>int pthread_cancel(pthread_t thread);Compile and link with -pthread.

通过这个参数,可以看出来这是个很简单的接口,终止对应tid的线程。只要线程存在,并且知道tid , 就可以终止线程(可以自己终止自己)。线程终止的返回值是一个整数!

3 测试运行

3.1 小试牛刀 — 创建线程

我们进行一个简单的测试,来使用这两个接口:
注意,使用线程库的接口需要动态链接g++ -o $@ $^ -std=c++11 -lpthread

#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <ctime>
#include <string>// 测试 1
void *ThreadRun(void *args)
{std::cout << "name: " << *(std::string*)args << " is running"<< std::endl;sleep(1);std::string* ret = new std::string(*(std::string*)args + "finish...") ;return (void*)ret;
}int main()
{// 创建一个新线程// int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);pthread_t tid;std::string name = "thread - 1";pthread_create(&tid, nullptr, ThreadRun, &name);//进程等待//int pthread_join(pthread_t thread, void **retval);std::string *ret = nullptr;pthread_join(tid, (void**)&ret);std::cout << *(std::string*)ret << std::endl;return 0;
}

编译运行一下,我们可以看到:
在这里插入图片描述
新线程完成了任务!

问题 1 : main线程和new线程谁先运行? 不确定,和进程的调度方式一致,由具体情况来定。

问题 2 : 我们期望谁先退出?肯定是main线程,所以就有join来进行等待,阻塞等待线程退出。如果不进行join,就会造成类似僵尸进程的情况(内存泄漏)!

问题 3 :tid是什么样子的,我们可不可以看一看?当然可以:

std::string ToHex(int x)
{char buffer[128];snprintf(buffer, sizeof(buffer), "0x%x", x);return buffer;
}

这样就可以打印出来tid的十六进制:
在这里插入图片描述
这数字好像和lwp不一致啊
在这里插入图片描述
为什么tid这么大?其实tid是一个虚拟地址!!!

3.2 探幽析微 — 理解线程参数

问题 4 : 全面看待线程函数传参。上面我们的程序传入了name变量的地址,让线程获取了对应的名字。如果想要传入多个变量或方法,可以传入类对象的地址:

class ThreadData
{
public:std::string name;int num;
};
vvoid *ThreadRun(void *args)
{ThreadData* td = static_cast<ThreadData*>(args);std::cout << "name: " << td->name << " is running" << std::endl;std::cout << "num: " << td->num << std::endl; sleep(1);std::string *ret = new std::string(*(std::string *)args + "finish...");return (void *)ret;
}std::string ToHex(int x)
{char buffer[128];snprintf(buffer, sizeof(buffer), "0x%x", x);return buffer;
}int main()
{// 创建一个新线程// int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);pthread_t tid;// std::string name = "thread - 1";ThreadData td;td.name = "thread - 1";td.num = 100;pthread_create(&tid, nullptr, ThreadRun, &td);// 查看tidsleep(1);std::cout << "tid: " << ToHex(tid) << std::endl;// 进程等待// int pthread_join(pthread_t thread, void **retval);std::string *ret = nullptr;pthread_join(tid, (void **)&ret);std::cout << *(std::string *)ret << std::endl;return 0;
}

这样就可以传入多个变量:
在这里插入图片描述
所以这个void*的变量是可以传入任何地址的,一定要想到可以传入类对象。但是刚写的有些问题,我们上面的写法是在主线程的栈区创建变量,让新线程读取主线程的栈,不太合适(破坏了一定独立性)!如果多个变量都传入了这个变量,那么修改一个就会造成所以的线程中的数据都发生改变!!!这可不行!推荐写:

	ThreadData* td = new ThreadData();td->name = "thread - 1";td->num = "100";pthread_create(&tid, nullptr, ThreadRun, td);

这是在堆区进行开辟空间,然后将该空间交给新线程来管理!就不会出现这样的问题了!以后我们都使用这种方式来传递参数!!!

3.3 小有心得 — 探索线程返回

问题 5 :线程的返回值输出型参数void** retval,他需要我们传递一个void*变量,然后返回值就交给了void*变量!这个过程就是对一个指针进行改变其指向的内容的操作。

下面是一个让新线程进行加法工作的程序

void *ThreadRun(void *args)
{ThreadData* td = static_cast<ThreadData*>(args);std::cout << "name: " << td->name << " is running" << std::endl;std::cout << "num: " << td->num << std::endl; sleep(1);delete td;//返回值std::string *ret = new std::string(*(std::string *)args + "finish...");return (void *)ret;
}

这就将void*变量返回给&(void* ret)变量,让ret指向对应的堆区。这就类似int a放入int * 中就可以改变a的值

问题 5 :如何全面的看待线程的返回。我们知道如果一个线程出现问题,整个进程就会退出。所以线程的返回只有正常的返回,没有异常的返回,出现异常整个进程会直接退出,根本没有返回错误信息的机会!和传入参数音参数一样,我们也可以返回一个类对象来传递多个变量。

#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <ctime>
#include <string>// 测试 1
class ThreadData
{
public:std::string name;int num1;int num2;
};class ThreadResult
{
public:std::string name;int num1;int num2;int ans;
};void *ThreadRun(void *args)
{ThreadData *td = static_cast<ThreadData *>(args);std::cout << "name: " << td->name << " is running" << std::endl;std::cout << "num1: " << td->num1 << " num2: " << td->num2 << std::endl;sleep(1);ThreadResult *ret = new ThreadResult();ret->name = td->name;ret->num1 = td->num1;ret->num2 = td->num2;ret->ans = td->num2 + td->num1;delete td;return (void *)ret;
}std::string ToHex(int x)
{char buffer[128];snprintf(buffer, sizeof(buffer), "0x%x", x);return buffer;
}int main()
{// 创建一个新线程// int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);pthread_t tid;// std::string name = "thread - 1";ThreadData *td = new ThreadData();td->name = "thread - 1";td->num1 = 100;td->num2 = 88;pthread_create(&tid, nullptr, ThreadRun, td);// 查看tidsleep(1);std::cout << "tid: " << ToHex(tid) << std::endl;// 进程等待// int pthread_join(pthread_t thread, void **retval);ThreadResult *ret = nullptr;pthread_join(tid, (void **)&ret);std::cout << ret->num1 << " + " << ret->num2 << " = " << ret->ans << std::endl;return 0;
}

来看返回值:
在这里插入图片描述
我们成功获取了新线程中设置的返回值!非常nice!

3.4 求索无厌 — 实现多线程

问题 6 :上面只是创建了单独的一个线程,那如何创建多线程呢?
可以通过维护一个vector数组来对tid进行统一管理

void *ThreadRun(void *args)
{std::string name = static_cast<const char *>(args);while (true){std::cout << name << "is running ..." << std::endl;sleep(1);}return (void *)0;
}std::string ToHex(int x)
{char buffer[128];snprintf(buffer, sizeof(buffer), "0x%x", x);return buffer;
}const int num = 10;int main()
{std::vector<pthread_t> tids;for (int i = 0; i < num; i++){// 1. 线程IDpthread_t tid;// 2. 线程名字char* name = new char[128];snprintf(name, 128, "thread - %d", i + 1);pthread_create(&tid, nullptr, ThreadRun, name);//保存所有线程的IDtids.push_back(tid) ;}//joinsleep(100);return 0;
}

在这里插入图片描述
这样就创建出了10个新线程,但是我们看这些新线程的的名字好像不太对:
在这里插入图片描述
怎么不是1 - 10???完全是乱的!因为线程谁先被调度运行不确定!而我们传入的名字是在主线程的栈区域,可能在新线程还没有调度,name就已经在主线程中被覆盖了!解决办法很简单,我们创建在堆区就可以了

for (int i = 0; i < num; i++){// 1. 线程IDpthread_t tid;// 2. 线程名字//在堆区进行创建。防止被重写覆盖char* name = new char[128];snprintf(name, 128, "thread - %d", i + 1);pthread_create(&tid, nullptr, ThreadRun, name);pids.push_back(tid) ;}

在这里插入图片描述
这样就整齐多了!

接下来就要进行等待:
我们已经通过vector容器来维护了创建所有线程的tid,所以只需要对所有的tid进行join就好了!

void *ThreadRun(void *args)
{std::string name = static_cast<const char *>(args);while (true){std::cout << name << "is running ..." << std::endl;sleep(3);break;}return nullptr;
}std::string ToHex(int x)
{char buffer[128];snprintf(buffer, sizeof(buffer), "0x%x", x);return buffer;
}const int num = 10;int main()
{std::vector<pthread_t> tids;for (int i = 0; i < num; i++){// 1. 线程IDpthread_t tid;// 2. 线程名字char* name = new char[128];snprintf(name, 128, "thread - %d", i + 1);pthread_create(&tid, nullptr, ThreadRun, name);//保存所有线程的IDtids.push_back(tid) ;}//joinfor (auto tid : tids){pthread_join(tid , nullptr);std::cout << ToHex(tid) << " quit..." << std::endl;}
} 

来看运行效果:
在这里插入图片描述
非常好!!!

我们也可以通过返回值来获取线程的名字:

    for (auto tid : tids){void* name = nullptr;pthread_join(tid , &name);std::cout << (const char*)name<< " quit..." << std::endl;delete (const char*)name;}

非常优雅!
在这里插入图片描述

3.5 返璞归真 — 线程终止与线程分离

问题 7 :线程终止的返回值
我们来看看通过线程终止接口终止的线程返回值是什么样的:

void *ThreadRun(void *args)
{std::string name = static_cast<const char *>(args);while (true){std::cout << name << "is running ..." << std::endl;sleep(3);//break;}return args;
}std::string ToHex(int x)
{char buffer[128];snprintf(buffer, sizeof(buffer), "0x%x", x);return buffer;
}const int num = 10;int main()
{std::vector<pthread_t> tids;for (int i = 0; i < num; i++){// 1. 线程IDpthread_t tid;// 2. 线程名字char* name = new char[128];snprintf(name, 128, "thread - %d", i + 1);pthread_create(&tid, nullptr, ThreadRun, name);//保存所有线程的IDtids.push_back(tid) ;}//joinsleep(3);for (auto tid : tids){pthread_cancel(tid);std::cout <<  " cancel: " << ToHex(tid) << std::endl;void* ret= nullptr;pthread_join(tid , &ret);std::cout << (long long int)ret << " quit..." << std::endl;}return 0;
}

在这里插入图片描述
可以看的,被phread_cancel()终止的线程的返回值是 -1!这个 -1其实是宏定义#define PTHREAD_CANCELED ((void *) -1)。线程终止的方式有三种:

  1. 线程函数 return
  2. pthread_cancel 新线程退出结果为-1
  3. pthread_exit

问题 8 :可不可以不通过join线程,让他执行完就退出呢,当然可以!
这里需要线程分离接口:

PTHREAD_DETACH(3)                                                   Linux Programmer's Manual                                                  PTHREAD_DETACH(3)NAMEpthread_detach - detach a threadSYNOPSIS#include <pthread.h>int pthread_detach(pthread_t thread);Compile and link with -pthread.

通过这个接口,分离出去的线程依然属于进程内部,但不需要被等待了。举个例子,之前再讲线程与进程的关系时,我们把不同的线程比作家庭成员,做好自己分内的事情,既可以让家庭幸福,即进程成功运行。而进程分离就好比你长大了,自己搬出去住,不受父母管了,但是依旧属于这个家庭。这种状态就是线程分离。

当然,如果想要将自己分离出去,就要知道自己的tid,这里需要接口:

PTHREAD_SELF(3)                                                     Linux Programmer's Manual                                                    PTHREAD_SELF(3)NAMEpthread_self - obtain ID of the calling threadSYNOPSIS#include <pthread.h>pthread_t pthread_self(void);Compile and link with -pthread.

这个接口会返回调用它的线程的ID。如同getpid()

void *ThreadRun(void *args)
{// 线程分离pthread_detach(pthread_self());std::string name = static_cast<const char *>(args);while (true){std::cout << name << "is running ..." << std::endl;sleep(3);break;}return args;
}std::string ToHex(int x)
{char buffer[128];snprintf(buffer, sizeof(buffer), "0x%x", x);return buffer;
}const int num = 10;int main()
{std::vector<pthread_t> tids;for (int i = 0; i < num; i++){// 1. 线程IDpthread_t tid;// 2. 线程名字char *name = new char[128];snprintf(name, 128, "thread - %d", i + 1);pthread_create(&tid, nullptr, ThreadRun, name);// 保存所有线程的IDtids.push_back(tid);}sleep(3);for (auto tid : tids){pthread_cancel(tid);std::cout << " cancel: " << ToHex(tid) << std::endl;void *ret = nullptr;int n = pthread_join(tid, &ret);std::cout << (long long int)ret << " quit... , n: " << n << std::endl;}return 0;
}

可以看到,如果我们等待一个已经分离出去的线程,会得到22号错误信息!所以不能 join 一个分离的线程!
在这里插入图片描述
所以主线程就可以不管新线程,可以继续做自己的事情,不用阻塞在join!

但是注意:线程分离了,依然是同一个进程!一个线程出异常,会导致整个进程退出!

上面是自己分离自己。也可以通过主线程分离新进程:

    for (auto tid : tids){pthread_detach(tid);//主线程分离新线程}

4 语言层的线程封装

上面讲的是Linux系统提供给我们的系统调用,帮助我们可以进行线程控制,也叫做原生线程库。我们熟悉了底层的原生线程库,就会方便很多。
我们来看C++11中的线程

#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <ctime>
#include <vector>
#include <string>
#include <thread>void threadrun( int num)
{while (num){std::cout << " num: " << num << std::endl;}
}// C++中线程库
int main()
{std::thread mythread(threadrun, 10);while (true){std::cout << "main thread..." << std::endl;sleep(1);}mythread.join();return 0;
}

注意,虽然是使用的语言层的线程库,但是依旧要连接thread动态库,因为语言层线程库的本质是对原生线程库接口的封装!!!无论是java还是python都要与原生线程库产生联系!

Thanks♪(・ω・)ノ谢谢阅读!!!

下一篇文章见!!!

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

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

相关文章

数据结构_顺序表专题

何为数据结构&#xff1f; 咱今天也来说道说道...... 数据结构介绍 准确概念 数据结构就是计算机存储、组织数据的方式 概念分析 从上句分析&#xff0c;数据结构是一种方式。一种管理数据的方式。为了做什么&#xff1f;为的就是计算机存储数据&#xff0c;组织数据。 …

元器件基础学习笔记——磁珠

一、磁珠的作用及构造 1.1 磁珠的作用 磁珠是一种用于抑制高频噪声的被动电子组件&#xff0c;通常由铁氧体材料制成&#xff0c;这种材料具有高电阻率和高磁导率&#xff0c;使其能够在高频下有效地将干扰信号以热能的形式消耗掉。在电路设计中&#xff0c;磁珠被广泛用于信号…

LeetCode 441, 57, 79

目录 441. 排列硬币题目链接标签思路代码 57. 插入区间题目链接标签思路两个区间的情况对每个区间的处理最终的处理 代码 79. 单词搜索题目链接标签原理思路代码 优化思路代码 441. 排列硬币 题目链接 441. 排列硬币 标签 数学 二分查找 思路 由于本题所返回的 答案在区间…

3d复制的模型怎么渲染不出来?----模大狮模型网

在展览3D模型设计领域&#xff0c;技术的进步和创新使得模型的复杂性和精细度有了显著提升。然而&#xff0c;有时设计师们在尝试渲染复杂的3D复制模型时&#xff0c;却面临着无法正确呈现的问题。模大狮将探讨这一现象的可能原因&#xff0c;并提供相应的解决方案和建议&#…

知识图谱和 LLM:利用Neo4j驾驭大型语言模型(探索真实用例)

这是关于 Neo4j 的 NaLLM 项目的一篇博客文章。这个项目是为了探索、开发和展示这些 LLM 与 Neo4j 结合的实际用途。 2023 年,ChatGPT 等大型语言模型 (LLM) 因其理解和生成类似人类的文本的能力而风靡全球。它们能够适应不同的对话环境、回答各种主题的问题,甚至模拟创意写…

3d导入模型后墙体变成黑色?---模大狮模型网

在展览3D模型设计领域&#xff0c;技术和设计的融合通常是创意和实现之间的桥梁。然而&#xff0c;有时设计师们会遇到一些技术上的挑战&#xff0c;如导入3D模型后&#xff0c;墙体却突然变成了黑色。这种问题不仅影响了设计的视觉效果&#xff0c;也反映了技术应用中的一些复…

【UE5.3】笔记10-时间轴的使用

时间轴 右键--Add Timeline(在最下面) --> 双击进入时间轴的编辑界面&#xff1a; 左上角可以添加不同类型的轨道&#xff0c;可以自定义轨道的长度&#xff0c;单位秒&#xff0c;一次可以添加多个 可以通过右键添加关键帧&#xff0c;快捷键&#xff1a;shift鼠标左键按…

C++基础语法:链表和数据结构

前言 "打牢基础,万事不愁" .C的基础语法的学习 引入 链表是最基础的数据集合,对标数组.数组是固定长度,随机访问,链表是非固定长度,不能随机访问.数组查找快,插入慢;链表是插入快,查找慢. 前面推导过"数据结构算法数据集合".想建立一个数据集合,就要设计数…

鸿蒙 画布来了 我不允许你不会

前言: 作者:徐庆 团队:坚果派 公众号:“大前端之旅” 润开鸿生态技术专家,华为HDE,CSDN博客专家,CSDN超级个体,CSDN特邀嘉宾,InfoQ签约作者,OpenHarmony布道师,电子发烧友专家博客,51CTO博客专家,擅长HarmonyOS/OpenHarmony应用开发、熟悉服务卡片开发。欢迎合作…

WPF+MvvmLight 项目入门完整教程(一)

WPF+MvvmLight入门完整教程一 创建项目MvvmLight框架安装完善整个项目的目录结构创建自定义的字体资源下载更新和使用字体资源创建项目 打开VS2022,点击创建新项目,选择**WPF应用(.NET Framework)** 创建一个名称为 CommonProject_DeskTop 的项目,如下图所示:MvvmLight框架…

OpenCV开发笔记(七十八):在ubuntu上搭建opencv+python开发环境以及匹配识别Demo

若该文为原创文章&#xff0c;转载请注明原文出处 本文章博客地址&#xff1a;https://hpzwl.blog.csdn.net/article/details/140435870 长沙红胖子Qt&#xff08;长沙创微智科&#xff09;博文大全&#xff1a;开发技术集合&#xff08;包含Qt实用技术、树莓派、三维、OpenCV…

【Nuxt3】vue3+tailwindcss+vuetify引入自定义字体样式

一、目的 在项目中引入自定义的字体样式&#xff08;全局页面都可使用&#xff09; 二、步骤 1、下载好字体 字体的后缀可以是ttf、otf、woff、eot或者svg&#xff08;推荐前三种&#xff09; 以抖音字体为例下载好放在静态文件夹&#xff08;font&#xff09;下 案例字…

【论文阅读】《Visual Prompt Tuning》

Abstract. 目前调整预训练模型的工作方式包括更新所有骨干参数&#xff0c;即全面微调。本文介绍了视觉提示调整&#xff08;VPT&#xff09;&#xff0c;作为大规模视觉变换器模型全面微调的高效替代方案。VPT 从高效调整大型语言模型的最新进展中汲取灵感&#xff0c;只在输…

软件测试——面试八股文(入门篇)

今天给大家分享软件测试面试题入门篇&#xff0c;看看大家能答对几题 一、 请你说一说测试用例的边界 参考回答&#xff1a; 边界值分析法就是对输入或输出的边界值进行测试的一种黑盒测试方法。通常边界值分析法是作为对等价类划分法的补充&#xff0c;这种情况下&#xff…

IAR全面支持芯驰科技E3系列车规MCU产品E3119/E3118

中国上海&#xff0c;2024年7月11日 — 全球领先的嵌入式系统开发软件解决方案供应商IAR与全场景智能车芯引领者芯驰科技宣布进一步扩大合作&#xff0c;最新版IAR Embedded Workbench for Arm已全面支持芯驰科技的E3119/E3118车规级MCU产品。IAR与芯驰科技有着悠久的合作历史&…

线程池及其底层工作原理

一、线程池是什么 线程池就是事先将多个线程对象放到一个容器中&#xff0c;当使用的时候就不用 new 线程而是直接去池中拿线程即可&#xff0c;节省了开辟子线程的时间&#xff0c;提高的代码执行效率在 JDK 的 java.util.concurrent.Executors 中提供了生成多种线程池的静态方…

分页stater

自定义aop,以添加注解的方法为切入点&#xff0c;对目标方法做一层增强 PageXAop代码如下&#xff1a; package cn.smart.pagex.aop;import com.github.pagehelper.PageHelper; import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.ProceedingJoinPoint; import org.a…

漏洞复现 | Showdoc反序列化

非常简单的一个靶场 靶场地址&#xff1a;https://hack.zkaq.cn/ 打开靶场&#xff0c;弹出了这种登录框&#xff0c;这也成为了后面的一个坑点&#xff0c;记住这个登录框。 看到了注册功能&#xff0c;showdoc有注册功能我们就不用尝试前台SQL注入了&#xff0c;直接注册…

Verilog基础:简单标识符和转义标识符

相关阅读 Verilog基础https://blog.csdn.net/weixin_45791458/category_12263729.html?spm1001.2014.3001.5482 标识符(identifier)是一个为了引用而给一个对象起的名字。一个标识符可以是一个简单标识符&#xff0c;也可以是一个转义标识符。本文将对两者进行详细阐述。 简…

Nuxt.js 错误侦探:useError 组合函数

title: Nuxt.js 错误侦探&#xff1a;useError 组合函数 date: 2024/7/14 updated: 2024/7/14 author: cmdragon excerpt: 摘要&#xff1a;文章介绍Nuxt.js中的useError组合函数&#xff0c;用于统一处理客户端和服务器端的错误&#xff0c;提供statusCode、statusMessage和…