Linux:多线程的操作
多线程操作
- 进程与线程
- 线程的创建 create_pthread
- 创建线程池
- 给线程传入对象的指针
- 线程等待 pthread_join
- 退出线程 pthread_exit
- 线程等待参数 retval 与 线程退出参数 retval
- 线程中断 pthread_cancel
- 获取线程编号 pthread_self
- 线程分离 pthread_detach
进程与线程
- 进程是资源分配的基本单位
- 线程是调度的基本单位,共享进程的数据,拥有自己的一部分数据
线程私有的属性:线程的ID、一组寄存器(上下文数据)、栈(独立的栈结构)、调度优先级
进程的多个线程共享同一块地址空间,对堆区、栈区都是共享的
线程共享进程的资源有:文件描述符表、每种信号的处理方式(默认动作、忽略动作、自定义动作)、当前工作目录
线程的创建 create_pthread
Linux下没有真正意义的线程,而是用进程模拟的线程(LWP)。对此,Linux不会提供直接创建线程的系统调用,只会提供创建轻量级进程的接口。
在用户看来会很变扭,进程是进程,线程是线程就要区分开来。
所以出现了用户级线程库 pthread:对Linux接口进行封装,给用户提供进行线程控制的接口
(pthread 线程库在任何版本的Linux操作系统都会存在, pthread也被称为原生线程库)
可以通过 man 的3号手册来查看线程库的使用,这里不作演示
接下来介绍一些线程库的接口使用:
使用原生线程库需要包含头文件:#include <pthread>
- 创建线程
int pthread_create(pthread_t *thread, const pthread_attr_t* attr,void* (*start_routine)(void*), void* arg);
pthread_create 函数参数介绍
thread:线程 id 地址,pthread_t 为无符号整数
attr:线程属性(线程优先级)
start_routine:函数指针,执行对应的函数功能(可以对函数进行传参),也被称为回调函数
arg:是指向任意数据的指针,将参数传递给 start_routine 函数
返回值:线程创建成功返回0,失败错误码被设置
示例:
#include <iostream>
#include <pthread>
#include <unistd.h>void* thread_run(void* arg)
{while(true){std::cout << "new thread running" << std::endl;sleep(1);}return nullptr;
}int main()
{pthread_t t;pthread_create(&t, nullptr, thread_run, nullptr);//创建线程,t是输出型参数//主进程while(true){std::cout << "main thread running, new thread id:" << t << std::endl;sleep(1);}return 0;
}
上面代码直接编译的话会出现链接报错,这是因为这个多线程是一个库,直接编译 g++ 会找不到这个库,需要指定编译器去找线程库。

对此,在编译时,使用 g++ 进行编译要加上 -lpthread 选项
g++ -o threadTest threadTest .c -std=c++11 -lpthread

可以通过 ldd 对编译好的可执行文件来查看线程库的位置:
ldd threadTest

执行程序可以看到,主线程与子线程同时运行:


此时输出的线程id会很大,很奇怪。其实这些线程的id是地址,创建的线程会被线程库管理起来,形成数组,每个对应的线程id 其实就是数组的下标。
创建的线程是不能确定先后顺序的. Linux下的线程是轻量级的进程,进程创建执行的先后顺序是由调度器决定的,对此线程谁先谁后的问题也要看调度器来决定的
创建线程池
下面来创建一个线程池,让每一个线程都执行 thread_run 这个函数,打印对应的创建编号
#include <iostream>
#include <pthread>
#include <unistd.h>#define NUM 10void* thread_run(void* arg)
{char* name = (char*)arg;while(true){std::cout << "new thread running,thread name is:" << name << std::endl;sleep(1);}return nullptr;
}int main()
{pthread_t tids[NUM];for(int i = 0; i < NUM; i++){char thname[64];snprintf(thname, sizeof(thname), "thread-%d", i + 1);pthread_create(tids + i, nullptr, thread_run, thname);//创建线程池,将thname传参}//主进程while(true){std::cout << "main thread running" << std::endl;sleep(1);}return 0;
}
编译运行:

结果很不对,输出的结果都是一样的。
在给线程回调函数进行传参时,传入的是 thname 地址。thname 字符数组是属于主线程的,属于临时变量。前面提到线程会共享进程中的数据。对此,每个线程都会对这个变量进行读写,导致最终显示的结果都是一样的。
解决方式如下:
对 thname 变量在堆上申请空间,待到回调函数使用完后对这个资源进行释放:
void* thread_run(void* arg)
{char* name = (char*)arg;while(true){std::cout << "new thread running,thread name is:" << name << std::endl;sleep(1);}delete name; //释放空间return nullptr;
}int main()
{pthread_t tids[NUM];for(int i = 0; i < NUM; i++){char* thname = new char[64]; //堆上开辟空间snprintf(thname, 64, "thread-%d", i + 1);pthread_create(tids + i, nullptr, thread_run, thname);//创建线程池,将thname传参}//主进程while(true){std::cout << "main thread running" << std::endl;sleep(1);}return 0;
}
编译运行:

创建线程前,每次对 thname 进行资源申请,回调函数之后对资源进行释放,可以很好的避免资源共享情况发生。从结果也可以看出不同线程的执行先后顺序也是不确定的。
给线程传入对象的指针
创建线程时,不仅仅只可以传入内置类型变量的指针,还可以传入自定义类型变量的指针
示例:构建 ThreadDate 类,其内部包含线程的基本信息。在类中实现输入型参数和输出型参数,方便我们获取线程处理后的数据结果
#include <iostream>
#include <unistd.h>
#include <pthread.h>
#include <string>
#include <ctime>#define NUM 3enum { OK=0, ERROR };struct ThreadDate
{//构造ThreadDate(const string& name, pthread_t tid, time_t createTime, size_t top = 0):_name(name), _tid(tid), _createTime((uint64_t)createTime),_status(OK),_top(top),_result(0){}~ThreadDate(){}//成员变量//输入型变量string _name;pthread_t _tid;uint64_t _createTime; //创建时间//输出型变量int _status; //线程退出状态size_t _top;//累加到最大值int _result;
};
下面通过实例化这个类,来演示线程中传入对象:
int main()
{pthread_t tids[NUM];// 创建线程池for (int i = 0; i < NUM; i++){char *thname = new char[64];snprintf(thname, 64, "thread-%d", i + 1);//定义ThreadDate类,传入到线程中ThreadDate* tdate = new ThreadDate(std::string(thname), i+1, time(nullptr), (100+ i * 5));pthread_create(tids + i, nullptr, thread_run, tdate); //将tdate对象进行传参}void *ret = nullptr; // 用于保存子线程退出的信息for (size_t i = 0; i < NUM; i++){int n = pthread_join(tids[i], &ret); //传入ret指针的地址if(n != 0) std::cerr << "pthread_join error" << std::endl;ThreadDate* td = static_cast<ThreadDate*>(ret); //指针类型转换if(td->_status == OK) //输出对象内容std::cout << td->_name << " 计算的结果是: " << td->_result << " (它要计算的是[1, " << td->_top << "])" << std::endl;//释放资源delete td;}return 0;
}

线程等待 pthread_join
上面实现的代码中,我们将主线程用死循环的方式,一直维持进程的运行。
如果去掉死循环,线程还能继续执行下去吗?
对上面的代码进行修改:在线程被创建后,维持 3 秒后主进程退出
int main()
{pthread_t tids[NUM];for(int i = 0; i < NUM; i++){char* thname = new char[64]; //堆上开辟空间snprintf(thname, 64, "thread-%d", i + 1);pthread_create(tids + i, nullptr, thread_run, thname);//创建线程池,将thname传参}//主进程sleep(3);return 0;
}

进程是资源的申请的主体,进程退出了,不管子进程还在进行什么操作都会终止运行。
这样会造成什么后果?
会造成资源泄漏,如果此时的线程在堆区申请了资源还没来得及释放,会导致内存泄漏。
线程与子进程一样,线程退出后需要被回收处理。就拿子进程来说,当子进程退出后会处于僵尸状态,父进程如果没有等待子进程,对子进程的僵尸状态进行回收的话会造成资源的泄漏。
有僵尸进程,但是有没有僵尸线程一说。与进程相似,线程退出后也会处于一种被回收的状态,没有及时回收线程的话,也会造成内存泄漏!
对此,线程退出是需要进行等待的
下面来介绍一个函数接口:pthread_join 等待线程
int pthread_join(pthread_t thread, void **retval);
参数介绍:
thread:等待的线程 id 号
retval:是一个指向指针的指针,用于存储被等待线程的返回值
返回值:等待成功返回0,失败错误码被返回
对上面的代码进行修改,写一个等待进程的版本:
void* thread_run(void* arg)
{char* name = (char*)arg;while(true){std::cout << "new thread running,thread name is:" << name << std::endl;sleep(1);}delete name; //释放空间return nullptr;
}int main()
{pthread_t tids[NUM];for(int i = 0; i < NUM; i++){// char thname[64];char* thname = new char[64];// snprintf(thname, sizeof(thname), "thread-%d", i + 1);snprintf(thname, 64, "thread-%d", i + 1);pthread_create(tids + i, nullptr, thread_run, thname);//创建线程池,将thname传参}for(size_t i = 0; i < NUM; i++){pthread_join(tids[i], nullptr);//等待线程}return 0;
}
有了线程等待,可以很好的避免内存泄漏。主进程会等待所有的子线程,只有当所有的线程都退出后才会结束整个程序的运行。
退出线程 pthread_exit
如何控制线程的退出呢?
这里还是拿进程来说,也比较好举例(前面也说过线程是轻量级的进程)。进程退出的方式可以在main函数中使用 return 语句、在任意行代码处调用 exit 函数。
那么线程可以使用类似的方法吗?
先来看看 return 语句的作用,还是拿刚刚编写的代码来举例。这里我们直接往死循环内部编写 3 秒的停顿,之后直接执行break 语句,后续执行 return 语句。为了方便展示,下面只展示修改的代码:
void* thread_run(void* arg)
{char* name = (char*)arg;while(true){std::cout << "new thread running,thread name is:" << name << std::endl;sleep(3);break; //跳出循环}delete name; return nullptr;
}
编译运行,来看看执行结果:

所有的线程都会打印一次,然后停顿卡住,到执行 return 语句后所有的线程都会退出。执行的效果也是符合我们的预期的。
下面来使用 exit 函数来测试线程退出情况,还是上面的代码,将 break 语句换成 exit 函数
void* thread_run(void* arg)
{char* name = (char*)arg;while(true){std::cout << "new thread running,thread name is:" << name << std::endl;exit(10); //调用exit函数}delete name; //释放空间return nullptr;
}
下面来看看现象:

线程池只创建了一部分,然后直接终止了运行。在右边监视 threadTest 进程也没有任何显示。
exit 函数退出作用是整个 threadTest 进程,当某一子线程调用了 exit 函数的时候,就会导致整个进程都退出。这也是为什么会只创建了一些子线程,然后导致整个进程都结束运行了。
对此,在线程执行流中,非必要情况下,不要轻易的调用 exit 函数。
不能使用 exit 函数,但是线程库中提供了一个API,用于退出某一线程:pthread_exit
void pthread_exit(void *retval);
参数介绍:
retval:指向线程退出状态的指针
当线程调用 pthread_exit 时,它会立即停止执行,并释放其栈空间。但是,线程的资源(如线程ID和线程属性)直到其他线程调用 pthread_join 来回收它时才会被完全释放。
示例:
#define NUM 3void* thread_run(void* arg)
{char* name = (char*)arg;while(true){std::cout << "new thread running,thread name is:" << name << std::endl;sleep(4);break;}delete name; //释放空间pthread_exit(nullptr); //退出调用的线程
}int main()
{pthread_t tids[NUM];for(int i = 0; i < NUM; i++){// char thname[64];char* thname = new char[64];// snprintf(thname, sizeof(thname), "thread-%d", i + 1);snprintf(thname, 64, "thread-%d", i + 1);pthread_create(tids + i, nullptr, thread_run, thname);//创建线程池,将thname传参}//等待线程for(size_t i = 0; i < NUM; i++){ pthread_join(tids[i], nullptr); }return 0;
}
这里只创建了三个子线程的线程池,来看看运行的效果:

线程等待参数 retval 与 线程退出参数 retval
先来看看这两个API的接口声明:
int pthread_join(pthread_t thread, void **retval); //线程等待接口
void pthread_exit(void *retval); //线程退出接口
两个函数之间的 retval 参数有关联吗?答案是有的。
一般创建进程都是为了帮助我们去完成某些任务,线程也是如此,创建线程也是为了帮助进程完成一部分任务。进程在完成任务后正常退出,返回对应的退出码。当然,进程完成到一定的任务时也会直接退出。
下面是进程退出的几个情况:
- 在 main 函数中调用 return 语句,返回对应的退出码;
- 在进程中任意代码处调用 exit 函数。当然调用 exit 函数需要传参,进程退出的退出码也就是传入exit 函数参数的值;
- 收到OS的终止信号
进程的退出码、退出信号的返回,是方便我们去查看当前进程是不是完成了指定的任务。线程也是如此,线程退出是否正常我们也要知道。对此,上面提到的 两个 API 接口的参数作用就是用于获取线程退出的退出信息!
线程退出接口 pthread_exit 一般是用在回调函数内部,也就是子线程中。我们可以先将 pthread_exit 功能想象成 exit 函数那般,在子线程退出后我们将子线程退出码带出来。
但是问题来了,为什么 pthread_exit 传入的参数是 void* retval 一级指针?
这个要结合 pthread_join 来看:
int pthread_join(pthread_t thread, void **retval);
pthread_join 是等待线程的一个接口,会回收退出的子线程(线程的ID、线程的属性等)。pthread_join 的 retval 是一个输出型参数。
这里的 retval 如同在进程中调用的 wait 函数时,传入 status 参数,这个 status 也是输出型参数,会将 子进程的退出码、退出信号带出来。
retval 参数的作用就是将子线程的退出数据带出来,不同的是这里是二级指针。在使用前需要定义一个指针,然后将这个指针的地址传入 pthread_join 的 retval 参数中。在子线程调用 pthread_exit 函数时,传出对应的数据即可。
光说不做,假把戏。下面来看看测试案例:
void *thread_run(void *arg)
{char *name = (char *)arg;while (true){std::cout << "new thread running,thread name is:" << name << std::endl;sleep(3);break;}delete name; // 释放空间pthread_exit((void*)1); //子线程退出,退出信息设置为1
}int main()
{pthread_t tids[NUM];for (int i = 0; i < NUM; i++){char *thname = new char[64];snprintf(thname, 64, "thread-%d", i + 1);pthread_create(tids + i, nullptr, thread_run, thname); // 创建线程池,将thname传参}void *ret = nullptr; // 用于保存子线程退出的信息for (size_t i = 0; i < NUM; i++){int n = pthread_join(tids[i], &ret); //传入ret指针的地址if(n != 0) std::cerr << "pthread_join error" << std::endl;std::cout << "子线程:thread->" << i+1 << ",退出码为:" << (uint64_t)ret << std::endl;}return 0;
}
这里需要注意就是传指针的问题:
定义 ret 一级指针,传参到 pthread_join 内部时,传入的是 ret 指针的地址。pthread_exit 传参需要传入指针类型,对此上面代码需要对 1 进行 void* 类型的强转。在输出子线程退出信息时,ret 是指针,经过子线程的等待,ret内部值已经被设置为了除了低位的第一位为1其他全为 0 的二进制序列,在通过 uint64_t 类型强转即可将数据打印输出!

还要提一点就是:在获取线程的退出码时,是不需要考虑异常的。如果一个线程中出现了异常,那么就会带动的整个主进程退出。主进程都退出了还需要考虑等待进程的异常吗?是不需要的。对此,在多线程中是不需要考虑异常的!异常问题通常是由进程来考虑。
线程中断 pthread_cancel
在实际开发需求中,如果想要将创建的线程中断运行需要用到 API:pthread_cancel
int pthread_cancel(pthread_t thread);
参数介绍:
thread:传入的线程编号
示例:我们先来创建一个正常线程,再执行一段任务后线程会自动退出:
void* thread_run(void* args)
{//静态类型转换const char* str = static_cast<const char*>(args);int cnt = 5;while(cnt){cout << str << "is runing :" << cnt-- << endl;sleep(1);}//退出线程pthread_exit((void*)1);
} int main()
{//创建线程pthread_t tid;pthread_create(&tid, nullptr, thread_run, (void*)"thread 1");//等待线程void* ret = nullptr;pthread_join(tid, &ret);return 0;
}

修改上述代码,在线程执行两秒任务后,直接调用 pthread_cancel 接口,查看现象:
void* thread_run(void* args)
{//静态类型转换const char* str = static_cast<const char*>(args);int cnt = 5;while(cnt){cout << str << "is runing :" << cnt-- << endl;sleep(1);}//退出线程pthread_exit((void*)1);
} int main()
{//创建线程pthread_t tid;pthread_create(&tid, nullptr, thread_run, (void*)"thread 1");//2秒后,中断线程sleep(2);pthread_cancel(tid);//等待线程void* ret = nullptr;pthread_join(tid, &ret);return 0;
}

可以看到当线程执行两秒后直接中断
获取线程编号 pthread_self
pthread_t pthread_self(void);
谁调用这个接口就获取谁的线程 id 编号,示例:
void* thread_run(void* args)
{//静态类型转换const char* str = static_cast<const char*>(args);int cnt = 5;while(cnt){cout << str << "is runing :" << cnt-- << "obtain self id ->" << pthread_self() << endl; //获取线程idsleep(1);}//退出线程pthread_exit((void*)1);
} int main()
{//创建线程pthread_t tid;pthread_create(&tid, nullptr, thread_run, (void*)"thread 1");//等待线程void* ret = nullptr;pthread_join(tid, &ret);cout << " new thread exit : " << (int64_t)ret << "quit thread: " << tid << endl;return 0;
}

线程分离 pthread_detach
新线程被创建,默认情况下是 joinable 的,线程退出,主进程需要对这个线程进行 pthread_join 操作。不对线程进行等待的操作就会造成内存泄漏,无法释放资源。
如果不关心线程的返回值,那么等待就会变成一种负担。
就是主线程自己为了等待子线程,难道不用去做自己的事情了吗?这个时候,我们可以告诉OS,当线程退出的时候,自己去释放资源。如何操作呢?需要用到下面这个 API :
int pthread_detach(pthread_t thread);
pthread_detach 功能是将一个线程分离出来,但是要记住一个点:被分离的线程在后续操作是不能被等待的!!如果对被分离的线程进行 pthread_join 操作,主进程是会报错的。报错出现后,就不会再对子线程进行等待操作,直接向后运行属于主进程的代码。
线程分离好比现实生活中的:已婚与未婚,是属于一种属性。
线程分离,并不是字面上的意思将线程与进程分离开那种。分离是一种属性,没有被分离的线程,是 joinable 的。该线程需要被等待回收资源;已经被分离的线程,其内部属性会发生变化,表示这个线程不需要再被等待回收资源。
示例:创建一个子线程,在等待子线程之前对该子线程进行分离操作
#include <pthread.h>
#include <iostream>
#include <cstdio>
#include <cstring>
#include <unistd.h>
#include <string>using namespace std;void* threadRoution(void* arg)
{const char* tname = static_cast<const char*>(arg);int cnt = 5;while(cnt){cout << tname << ":" << cnt-- << endl;sleep(1);}return nullptr;
}int main()
{//创建线程pthread_t tid;pthread_create(&tid, nullptr, threadRoution, (void*)"thread 1");//对子线程进行分离操作pthread_detach(tid);//等待线程void* ret = nullptr;int n = pthread_join(tid, &ret);if(n != 0) cerr << "error:" << errno << strerror(n) << endl;return 0;
}
编译查看效果:

主进程在等待子线程时,发现该线程已经被分离。对此,不会再阻塞等待子线程,程序直接向后运行走,子线程也没有机会继续执行对应的功能,整个进程就退出了。
因此,线程分离的主要功能就是将子线程分离出来,让主进程有更多的时间去处理属于自己事情,也不需要对子线程的资源释放与否而担心。
不过在使用线程分离的时候,要注意执行流先后问题,不然会出现奇奇怪怪的现象。
下面来举个例子:在子线程内部去调用本线程的分离
void* threadRoution(void* arg)
{//将调用的线程分离开来pthread_detach(pthread_self());const char* tname = static_cast<const char*>(arg);int cnt = 5;while(cnt){cout << tname << ":" << cnt-- << endl;sleep(1);}return nullptr;
}int main()
{//创建线程pthread_t tid;pthread_create(&tid, nullptr, threadRoution, (void*)"thread 1");int n = pthread_join(tid, nullptr);if(n != 0) cerr << "error:" << errno << strerror(n) << endl;return 0;
}

此时会发现,线程正常的跑,主进程也等待成功。
子线程调用分离没有用吗?其实不然,这是由于执行流先后问题:
子线程被创建出来之前,主进程就执行到了 pthread_join 代码处,子线程还没有来得及分离,分离属性没有被修改,造成主进程阻塞等待子线程。对此,就算子线程将自己分离开来,主进程早就处于进行了等待状态,也就造成了子线程继续往后执行的现象。
提示:使用线程分离的接口,尽量在创建线程之后进行调用,防止奇奇怪怪的执行流的问题产生
线程操作就讲到这里,感谢大家的支持!!
相关文章:
Linux:多线程的操作
多线程操作 进程与线程线程的创建 create_pthread创建线程池给线程传入对象的指针 线程等待 pthread_join退出线程 pthread_exit线程等待参数 retval 与 线程退出参数 retval 线程中断 pthread_cancel获取线程编号 pthread_self线程分离 pthread_detach 进程与线程 进程是资源…...
kunpeng的aarch64架构cpu、openeuler系统、昇腾服务器适配文档转换功能(doc转docx、ppt转pptx)
一、安装flatpak sudo yum install flatpak flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo二、安装libreoffice flatpak install flathub org.libreoffice.LibreOffice三、使用 对于使用 flatpak 安装的 LibreOffice,不需要手…...
unity 打包PC安装包中常见文件的功能
目录 前言 一、打包好的文件 二、常用文件 1.文件夹XXX_Data 2.文件夹MonoBleedingEdge 3.文件夹XXX_Data内部 三、文件的应用 1.如果你替换了一个图片 2.如果你新增了或减少了图片和资源 3.场景中有变动 4.resources代码加载的资源改了 5.如果你代码替换了 四、作…...
【Ardiuno】实验使用ESP32单片机实现高级web服务器暂时动态图表功能(图文)
接下来,我们继续实验示例代码中的Wifi“高级web服务器”,配置相关的无线密码后,开始实验 #include <WiFi.h> #include <WiFiClient.h> #include <WebServer.h> #include <ESPmDNS.h>const char *ssid "XIAOFE…...
深入浅出服务网格(Service Mesh):现代微服务架构的护航者
什么是服务网格? 服务网格是一种专用于处理微服务间通信的基础设施层,通常以轻量级代理(sidecar)的形式部署在每个服务实例旁边。它主要负责以下几项任务: 服务发现:自动检测和注册服务实例,使…...
node调试
vscode安装插件:JavaScript Debugger (Nightly) 点击后生成一个launch.json文件 打断点,并发送一个请求来执行代码到断点处 按右上的向下箭头,进入源码,进行查看,左边查看变量等值...
docker拉取镜像失败超时的解决方法,docker配置国内镜像源
更换国内源 创建或修改 /etc/docker/daemon.json 文件 安装docker后一般只有 /etc/docker 这个目录 下面并没有 daemon.json 文件 我们直接创建 : vim /etc/docker/daemon.json {"registry-mirrors" : ["https://registry.docker-cn.com"…...
建造气膜结构体育馆需要注意的事项—轻空间
气膜结构体育馆以其快速建造、低成本、灵活性高等优势,越来越受到各类运动场所的青睐。气膜结构利用空气压力支撑膜材,从而形成自持结构,无需传统的钢筋混凝土框架。这类建筑适用于各种气候条件,且可根据需要快速搭建和拆卸。然而…...
使用脚手架创建vue2项目(关闭eslint语法检查 、运行项目时自动打开网址、src文件夹简写方法)
使用脚手架创建vue2项目会默认安装的插件(eslint) 这个插件是检查语法的。 假设我们在main.js中定义了一个变量,没有使用 eslint 就会检测出错误 (事实是我们并没有写错而是eslint 给我们判断是错的,所以这样会很麻烦ÿ…...
谷粒商城实战(036 k8s集群学习2-集群的安装)
Java项目《谷粒商城》架构师级Java项目实战,对标阿里P6-P7,全网最强 总时长 104:45:00 共408P 此文章包含第343p-第p345的内容 k8s 集群安装 kubectl --》命令行操作 要进入服务器 而且对一些不懂代码的产品经理和运维人员不太友好 所以我们使用可视化…...
复旦微FMQL20SM全国产ARM+FPGA核心板,替代xilinx ZYNQ7020系列
FMQL20SM核心板一款全国产工业核心板。基于复旦微FMQL20S400M四核ARM Cortex-A7(PS端) FPGA可编程逻辑资源(PL端)异构多核SoC处理器设计的全国产工业核心板,PS端主频高达1GHz。 核心板简介 FMQL20SM核心板是一款全国…...
NPM常见问题
文章目录 NPM常见问题1. 使用淘宝源安装包出错2. listen EADDRINUSE 服务端口被占用报错3. npm start 启动后过一会崩溃结束:内存溢出4. npm install的时候使用特定的源安装5. npm安装指定版本、最新版本6. npm ERR! cb() never called! 解决7. Unable to authentic…...
二开版视频CMS完整运营源码/新版漂亮APP手机模板/集成员分销功能等
一个二开的影视CMS,直接上传源码至网站根目录,访问网站域名即可安装。 测试环境:Nginx 1.20.1—MySQL 5.6.50–PHP-7.2(安装拓展/fileinfo) 上传源码,访问域名直接安装 后台地址:域名/MDadmi…...
JavaScript的数组排序
天行健,君子以自强不息;地势坤,君子以厚德载物。 每个人都有惰性,但不断学习是好好生活的根本,共勉! 文章均为学习整理笔记,分享记录为主,如有错误请指正,共同学习进步。…...
从Log4j和Fastjson RCE漏洞认识jndi注入
文章目录 前言JNDI注入基础介绍靶场搭建漏洞验证注入工具 log4j RCE漏洞分析漏洞靶场检测工具补丁绕过 Fastjson RCE漏洞分析漏洞靶场检测工具补丁绕过 总结 前言 接着前文的学习《Java反序列化漏洞与URLDNS利用链分析》,想了解为什么 Fastjson 反序列化漏洞的利用…...
7-25 数字三角形问题
7-25 数字三角形问题 分数 10 全屏浏览 作者 夏仁强 单位 贵州工程应用技术学院 给定一个由n行数字组成的数字三角形如下图所示。试设计一个算法,计算出从三角形的顶至底的一条路径,使该路径经过的数字总和最大。 对于给定的由n行数字组成的数字三角…...
【Kafka专栏 08】ZooKeeper的Watch机制:不就是个“小喇叭”吗?
作者名称:夏之以寒 作者简介:专注于Java和大数据领域,致力于探索技术的边界,分享前沿的实践和洞见 文章专栏:夏之以寒-kafka专栏 专栏介绍:本专栏旨在以浅显易懂的方式介绍Kafka的基本概念、核心组件和使用…...
三极管的厄利效应(early effect)
詹姆斯M厄利(James M. Early)发现的现象,厄利效应(英语:Early effect),又译厄尔利效应,也称基区宽度调制效应,是指当双极性晶体管(BJT)的集电极-射极电压VCE改…...
Maven: 编码GBK的不可映射字符不能编译
使用mvn compile命令,出现错误: 编码GBK的不可映射字符不能编译。这是因为代码或注释中存在中文引起的,一般在ide中会自动处理编译时的字符集,就不会碰到这个错误。这个错误是在生成代码后,其中自动加上了中 文注释,手…...
《web应用技术》第十一次课后作业
1、验证过滤器进行权限验证的原理。 Filter过滤器:javaweb三大组件(Servlet,Filter,Listener)之一;过滤器可以把对资源的请求拦截下来,从而实现一些特殊功能;过滤器一般完成一些通用操作,比如登录校验等。 执行对应的…...
Xshell远程连接Kali(默认 | 私钥)Note版
前言:xshell远程连接,私钥连接和常规默认连接 任务一 开启ssh服务 service ssh status //查看ssh服务状态 service ssh start //开启ssh服务 update-rc.d ssh enable //开启自启动ssh服务 任务二 修改配置文件 vi /etc/ssh/ssh_config //第一…...
376. Wiggle Subsequence
376. Wiggle Subsequence 代码 class Solution { public:int wiggleMaxLength(vector<int>& nums) {int n nums.size();int res 1;int prediff 0;int curdiff 0;for(int i 0;i < n-1;i){curdiff nums[i1] - nums[i];if( (prediff > 0 && curdif…...
ArcGIS Pro制作水平横向图例+多级标注
今天介绍下载ArcGIS Pro中如何设置水平横向图例。 之前我们介绍了ArcGIS的横向图例制作:ArcGIS横向、多列图例、顺序重排、符号居中、批量更改图例符号等等(ArcGIS出图图例8大技巧),那这次我们看看ArcGIS Pro如何更加快捷的操作。…...
蓝桥杯3498 01串的熵
问题描述 对于一个长度为 23333333的 01 串, 如果其信息熵为 11625907.5798, 且 0 出现次数比 1 少, 那么这个 01 串中 0 出现了多少次? #include<iostream> #include<cmath> using namespace std;int n 23333333;int main() {//枚举 0 出现的次数//因…...
Aspose.PDF 限制绕过方案:Java 字节码技术实战分享(仅供学习)
Aspose.PDF 限制绕过方案:Java 字节码技术实战分享(仅供学习) 一、Aspose.PDF 简介二、说明(⚠️仅供学习与研究使用)三、技术流程总览四、准备工作1. 下载 Jar 包2. Maven 项目依赖配置 五、字节码修改实现代码&#…...
JVM虚拟机:内存结构、垃圾回收、性能优化
1、JVM虚拟机的简介 Java 虚拟机(Java Virtual Machine 简称:JVM)是运行所有 Java 程序的抽象计算机,是 Java 语言的运行环境,实现了 Java 程序的跨平台特性。JVM 屏蔽了与具体操作系统平台相关的信息,使得 Java 程序只需生成在 JVM 上运行的目标代码(字节码),就可以…...
【JVM面试篇】高频八股汇总——类加载和类加载器
目录 1. 讲一下类加载过程? 2. Java创建对象的过程? 3. 对象的生命周期? 4. 类加载器有哪些? 5. 双亲委派模型的作用(好处)? 6. 讲一下类的加载和双亲委派原则? 7. 双亲委派模…...
RabbitMQ入门4.1.0版本(基于java、SpringBoot操作)
RabbitMQ 一、RabbitMQ概述 RabbitMQ RabbitMQ最初由LShift和CohesiveFT于2007年开发,后来由Pivotal Software Inc.(现为VMware子公司)接管。RabbitMQ 是一个开源的消息代理和队列服务器,用 Erlang 语言编写。广泛应用于各种分布…...
【学习笔记】erase 删除顺序迭代器后迭代器失效的解决方案
目录 使用 erase 返回值继续迭代使用索引进行遍历 我们知道类似 vector 的顺序迭代器被删除后,迭代器会失效,因为顺序迭代器在内存中是连续存储的,元素删除后,后续元素会前移。 但一些场景中,我们又需要在执行删除操作…...
stm32wle5 lpuart DMA数据不接收
配置波特率9600时,需要使用外部低速晶振...
