Linux学习记录——이십사 多线程(1)

news2025/1/10 17:07:42

文章目录

  • 1、以Linux角度理解
  • 2、并不是所有的操作系统都这样管理
  • 3、页表和物理内存
  • 4、线程优缺点
  • 5、进程和线程的区别
  • 6、线程接口
    • 1、pthread_create.
    • 2、pthread_join
    • 3、线程终止
      • 取消正在终止的线程
    • 4、线程分离


1、以Linux角度理解

创建一个进程时,会有pcb结构体,CPU中有很多寄存器,存有很多各个类型的数据,比如其中一个就指向进程pcb,只要pcb一换,那么指向的进程就不一样了。假如再创建进程的时候只创建pcb,而不是像之前创建子进程那样还有地址空间等的操作,这些新的pcb和最一开始的task_struct指向同一个地址空间,我们可以在代码中写上几个不同的函数,然后每个pcb都可以访问到地址空间的代码区运行不同的代码,CPU指向哪一个pcb就运行哪一个函数像这样在进程内创建出来的多个pcb就称作一个个线程,这样运行的方法也就是并发执行。

由于一个线程执行的是一个进程的一部分,所以线程的执行粒度比进程更细。对于CPU来说,它分不清楚是线程和进程,因为它只看pcb。线程是进程内部的一个执行流。线程的切换只需要切换pcb,不需要切换地址空间和页表。既然CPU不知道是不是线程,那切换时怎么确定要不要换地址空间?实际上切换的工作是操作系统做的,CPU执行系统的代码来切换。

CPU有一个硬件cache,是高速缓存。一个进程在运行时,如果访问到了100行代码,也有可能访问到101行,所以为了加快速度,cache就会把100行附近的数据都先缓存进来。在切换线程时,这个缓存数据不用变化,因为都是同一个进程,但是切换一个不同地址空间的进程时,这个缓存数据就需要设为失效,清理掉,然后再缓存新进程的数据。所以线程的调度成本更低。

现在所知道的线程其实应当是执行流,而包含了多个执行流,地址空间和页表的整个部分是进程,进程是承担分配系统资源的基本实体。进程内部可以多个task_struct。在CPU角度来看,它只认pcb,所以线程是CPU调度的基本单位。

2、并不是所有的操作系统都这样管理

可以看出来,线程数量一定比进程多,操作系统需要管理线程,同样地,需要先描述再组织,有些操作系统是有线程结构体的,也就是TCB,线程控制块。TCB属于PCB,需要维护两者之间的关系,加上调度进程,调度线程,如果这样设计操作系统,这个系统就很复杂,但windows就是这样做的,它的内核有真线程。

Linux不是这样设计的。在实际应用中会发现调度线程和调度进程有很多的相同点,所以Linux的TCB就是直接复制了PCB,代码上复用PCB的结构体,用PCB模拟线程的TCB,所以Linux没有真正意义上的线程,而是用进程方案模拟的线程,这一点也决定了Linux系统可以一直不关机,但是windows如果一直不关机,就完了。

复用代码和结构这一做法让线程执行更简单,并且好维护,效率更高,也更安全,所以Linux才能一直不间断地运行。一款系统,使用最频繁的功能,除了系统本身,就是进程。

Linux的调度执行流叫做轻量级进程。

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
#include <pthread.h>

void* thread1_run(void* args)
{
    while(1)
    {
        printf("我是线程1,我正在运行\n");
        sleep(1);
    }
}

void* thread2_run(void* args)
{
    while(1)
    {
        printf("我是线程2,我正在运行\n");
        sleep(1);
    }
}

void* thread3_run(void* args)
{
    while(1)
    {
        printf("我是线程3,我正在运行\n");
        sleep(1);
    }
}

int main()
{
    pthread_t t1, t2, t3;
    pthread_create(&t1, NULL, thread1_run, NULL);
    pthread_create(&t2, NULL, thread2_run, NULL);
    pthread_create(&t3, NULL, thread3_run, NULL);
    while(1)
    {
        printf("我是主线程,我正在运行\n");
        sleep(1);
    }
}

可以用ps -aL | head -1 && ps -aL | grep 可执行程序名来查看线程,其中的LWP就是轻量级进程ID,但是它和PID相同,那么系统如何辨别这两个?系统调度的时候看的是LWP,如果只有一个线程,那么自然看的就是PID。如果要切换的PID和现在LWP不一样,那么这就是跨进程调度,那就需要切换地址空间和页表了;相同就是进程内调度。

3、页表和物理内存

虚拟地址的空间的基本单位是字节,那么32位机器下虚拟地址空间内有多少地址?4G就是2 ^ 32次方个字节。虚拟转物理,需要页表来转换。页表是一种软件,它肯定要占据一定内存,那么页表有多大,它要维护2 ^ 32次方个虚拟空间的字节,还有对应物理内存,它要维护kv关系,除此之外,页表还有其他属性,1个地址占据4个字节,那么虚拟和物理就是8个字节,假设属性那一列也是4字节,那么就是2 ^ 32 * 12,48G!这可能吗?这不可能,所以这点是错误的。

物理内存在寻址的单位是1字节,我们也可以一个个字节访问内存,也可以使用比特位,但物理内存并不是一个个字节排列起来的。物理内存会高频地和磁盘进行交互,磁盘的速度会比较慢,那么物理内存如果规规矩矩地按照字节为单位来访问数据的话,效率就不高了;如果一次IO就搬运很多数据,会比多次IO搬运少量数据更高效。所以系统在和磁盘这样的设备进行IO交互的时候,是要以块为单位的,根据之前写的文件系统,这个单位是4kb,8个扇区,当然不同系统也会有不同。那如果想更改一个比特位呢?这也必须IO4kb。由于要IO4kb,文件等数据在磁盘存储时也是按照4kb为单位来存储的,比如9kb的数据就是三个4kb,这样IO时就可以很块地找到要访问的区域,并且找到头尾,返回回来。文件包括动静态库和可执行程序这些,它们也是按照4kb为单位向上取整存放的。

磁盘这样管理,物理内存和操作系统也一样。内存实际进行管理的时候,就已经被划分成了一个个4kb,所以内存管理的本质就是将磁盘中特定的4kb的块(数据内容)放入到哪一个物理内存的4kb的空间(数据保存的空间),物理内存中这一个4kb的内容叫做页,这个空间叫做页框,磁盘中的每一个4kb空间叫做页帧。操作系统对于物理内存的页也会管理,先管理再组织,会有一个page的结构体,里面有页的各种属性。对于page的组织,系统用的是数组。这个数组以字节为基本单位,=有1048576个数组元素。当找到指定内存没有被使用,那么申请内存时就会把它置为1,也就申请上了。操作系统的管理方式还有其他种,比如LRU,伙伴系统。

虽然不管怎么样,一次IO会传过来4kb数据,但只访问1字节也要传4kb,这是不是有些浪费啊?实际上操作系统不只是传了4kb。系统有一个局部性原理,是现代计算机预加载数据的理论基础,它会提前加载我们正在访问的数据的相邻或附近的数据,所以会通过预先记载要访问的数据的附近来减少未来IO的次数。

回到最一开始的一个问题,页表真的有48GB?!所以页表不可能是这样的。一个虚拟地址是32个比特位,它传进页表时不是被整体使用的,它是被划分成10+10+12的。系统会拿出前10个比特位充当key去页表查找value,那么现在这个页表应当是2 ^ 10个行,然后会找到对应的二级页表;再拿10个比特位,去这个二级页表查找,这个二级页表最多也是2 ^ 10方的行,这时候查找的value是页框地址,刚才写到管理页的数组有1048576个元素,算到具体每一个,二级页表里的value需要有20个比特位,对应的就是物理内存的页框的起始地址;最后的12个比特位是页内偏移,页框的起始地址+页内偏移就能找到任意一个起始字节地址。

12个比特位有4095个数据,所以IO的基本单位是4kb。操作系统定位任意一个内存字节位置的方法就是基地址+偏移量。

这样管理后,页表最多有2 ^ 10个数据,也就是1M,每个地址4字节,所以就是最多就是4M。用户不可能访问到所有的物理内存,页表只在用的时候才创建,用多少创建多少,所以实际上页表会更小。

刚才写的一级页表叫页目录,二级页表叫页表项,也有不同的叫法,但都是一个东西。

我们现实中用语言的时候,很少直接访问比特位或者只有一个字节的变量,比如定义好几个int类型的变量,数组,类对象等等,难道就是这样从1个字节开始访问?任何一个对象或者变量,可能存在多个字节,但是取地址的时候,永远只拿到了一个地址数字,这个地址就是起始地址,加上类型,也就是偏移量就是整个地址。

实际在申请malloc内存的时候,系统只在虚拟地址空间上申请,当用户真正访问时,系统才会自动申请或者填充页表,申请具体的物理内存。系统是怎么做到自动填充页表的?通过缺页中断。一旦要开始转换的时候,MMU会发现这时候页表还不存在,就会向寄存器发送中断信号,系统执行默认动作来创建页表,申请物理内存。同理,要访问磁盘数据时,文件会先加载一部分,然后实际访问的时候再继续加载。

关于页表和物理内存的操作执行流不关心这些,它们时通过页表,缺页中断等来进行耦合的。

页表不止是这些,在页表项中还有其他属性,比如是否命中代表着物理内存是否已经存在要访问的数据,RWX代表对访问数据的权限等。

在之前的代码中,char* s的内容不允许修改,它存储在字符常量区,只允许被读取。因为s保存的是指向的字符串的虚拟起始地址,*s在寻址的时候会有虚拟到物理的转化,那么就一定要查页表,会对你的操作进行权限审查,发现这个操作非法,MMU就会发生异常,系统识别到异常后转化成信号,将信号发送给目标进程,在进程从内核态转为用户态的时候进行信号处理,进程就会终止。

4、线程优缺点

优点:
1、创建一个新线程的代价要比创建一个新进程小得多
2、与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多
3、线程占用的资源要比进程少很多
4、能充分利用多处理器的可并行数量
5、在等待慢速I/O操作结束的同时,程序可执行其他的计算任务
7、计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现(加密解密,文件压缩和解压等和算法相关的,但是线程数量要合适才好,进程/线程和cpu的个数/核数一致)
8、I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作(下载,上传,IO主要消耗IO资源,磁盘的IO,网络的带宽。在这里线程也不是越多越好,可以比较多)

缺点:
1、性能损失
一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器。如果计算密集型
线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的
同步和调度开销,而可用的资源不变。
2、健壮性降低
编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了
不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。
3、缺乏访问控制
进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。
4、编程难度提高
编写与调试一个多线程程序比单线程程序困难得多

要使用线程的话,在gcc最后加上-lpthread。线程创建函数时pthread_create,变量类型是pthread_t。

在多线程程序中,任何一个线程崩溃都会导致进程崩溃。以系统角度来看,线程是进程的执行分支,线程被杀掉就是进程被杀掉;从信号角度看,页表转换的时候MMU识别写入权限的时候没有通过,系统就会拿到异常给进程发信号,信号是以进程为主的,会给进程的每个线程写异常,所以全部挂掉。

因为一个进程内执行流看到的资源是通过地址空间看到的,多个LWP看到的是同一个地址空间,所以所有的线程可能会共享大部分数据。

5、进程和线程的区别

线程也有属于自己的一部分数据:线程ID,一组寄存器,栈,errno,信号屏蔽字,调度优先级。这里面最重要的就是一组寄存器和独立的栈结构。寄存器与线程的动态切换有关;栈与线程自己的数据有关。线程共享的数据有这些:文件描述符表,每种信号的处理方式,当前工作目录,用户ID和组ID,也就是数据区代码区堆区都可以共享。

6、线程接口

以系统角度来看,Linux没有真正意义的线程,而是用进程模拟的线程(LWP),所以Linux不会提供直接创建线程的系统调用,他会给我们最多提供创建轻量级进程的接口。

以用户角度来看,用户只认线程,那么库就对下将Linux接口封装,对上给用户提供进行线程控制的接口,这种库叫做用户级线程库,这个库也叫pthread库,这个库任何系统都需要自带,在Linux中这个库叫做原生线程库。

下面开始写一些代码

1、pthread_create.

线程的创建是pthread_create.

在这里插入图片描述

第一个参数是线程ID,是一个输出型参数;attr是线程属性,一般设置为NULL;第三个参数是一个函数指针,是新线程执行的函数、方法;arg就是这个函数的参数。

使用线程接口时,makefile的g++后面必须写上-lpthread。

threadtest:thread.cc
    g++ -o $@ $^ -lpthread
.PHONY:clean
clean:
    rm -f threadtest
#include <iostream>
#include <unistd.h>
#include <phread.h>

void* thread_run(void* args)
{
    while(true)
    {
        cout << "new thread running" << endl;
        sleep(1);
    }
    return nullptr;
}

int main()
{
    pthread_t t;
    pthread_create(%t, nullptr, thread_run, nullptr);
    while(true)
    {
        cout << "main thread running, new thread id: " << t << endl;
        sleep(1);
    }
}

当正式打印时,用指令ps -aL | head -l && ps -aL | grep threadtest,会发现我们看到的LWP,和程序打印出来的结果不一样,那为什么会不一样,各自存在的意义是什么?

下面会写到这个问题的答案。代码中我们创建了新线程,实际运行的时候运行顺序由调度器决定。

创建一批线程的话

void* thread_run(void* args)
{
    char* name = (char*)args;
    while(true)
    {
        cout << "new thread running" << endl;
        sleep(1);
    }
    return nullptr;
}

int main()
{
    pthread_t tids[NUM];
    for(int i = 0; i < NUM; i++)
    {
        char tname[64];
        snprintf(tname, sizeof(tname), "thread-%d", i + 1);
        pthread_create(tids + i, nullptr, thread_run, tname);
    }
    while(true)
    {
        cout << "main thread running, new thread id: " << endl;
        sleep(1);
    }    
}

tname相当于十个线程指向同一个主线程的临时空间,tname拿到的是起始地址,每一次这个数组都会被覆盖,所以最后只会留下最后一个的地址,打印出来会看到十个线程打印出来的东西都是第十个。要打印出全部线程名字的话

        char* tname = new char[64];
        snprintf(tname, 64, "thread-%d", i + 1);

new了一块空间,这样开在堆上,是在循环中创建的,thread_run函数里return nullptr之前delete name,这样相当于每个线程都有自己的一块小空间,用完就去掉。在pthread_create函数调用前就会把所有参数都拷贝好,调用,传过去,然后会再次new,然后再去执行函数调用,每个线程用的空间都不一样。不过这个写法一般。

如果主线程后面没有死循环,是直接return 0,主线程和分线程都会持续执行,可以在return0之前sleep几秒,控制这10个分线程全部跑完以前return 0,那么所有线程都会退出。主线程一退出,就是进程退出,资源数据全部释放,分线程也全部退出;分线程创建时主线程需要等待,要不容易造成类似僵尸进程的现象。

2、pthread_join

在这里插入图片描述

    for(int i = 0; i < NUM; i++)
    {
        pthread_join(tids[i], nullptr);
    }

主线程要等新线程,新线程退出后主线程才能退,在这期间需要等待,join就用来等待。这个等待默认是阻塞的。第二个参数是一个输出型参数,会拿到新线程退出的结果。若成功则返回0。

3、线程终止

线程的终止有多种方式

1、线程函数执行完毕
2、exit退出。exit是进程退出,不是线程退出,所以哪个线程调用了exit,那么整个进程全部退出
3、pthread_exit线程退出接口

在这里插入图片描述

void* thread_run(void* args)
{
    char* name = (char*)args;
    while(true)
    {
        cout << "new thread running" << endl;
        sleep(1);
        break;
    }
    delete name;
    pthread_exit(nullptr);
    //return nullptr;
}

这个接口的参数和jion的第二个一样类型,用来接收新线程的结果。创建新线程后,我们需要知道给它的任务它完成得怎么样,需要返回结果。在pthread_ exit的括号里,我们可以传(void*)1,return也可以,对应上thread_run函数的类型以及pthread_ exit 函数的参数类型,主线程去接收它,接收的地方就在调用join接口之处,join的第二个参数是void**retval,应当这样理解void* *retval,所以就是变量类型和传入的变量的地址。

    delete name;
    pthread_exit(nullptr);
    //return nullptr;
}

int main()
{
    pthread_t tids[NUM];
    for(int i = 0; i < NUM; i++)
    {
        char* tname = new char[64];
        snprintf(tname, 64, "thread-%d", i + 1);
        pthread_create(tids + i, nullptr, thread_run, tname);
    }
    void* ret = nullptr;
    for(int i = 0; i < NUM; i++)
    {
        int n = pthread_join(tids[i], &ret);
        if(n != 0) cerr << "pthread_join error" << endl;
        cout << "thread quit: " << (uint64_t)ret << endl;
    }
    cout << "all thread quit..." << endl;

ret里存着1,所以最后再强转一下变成int,打印出来。这里设置为1,最后会看到每个线程退出时都打印了一个1.

但这里会发现我们只是得到了一个返回来的数字而已,那么怎么知道线程有没有异常?其实不用担心这个问题,因为一旦异常,整个进程就结束了。

退出的时候既然类型是void,那么我们就可以传各种各样的类型,所以也就可以传类,可以把要看到的信息放在类里面。

#include <iostream>
#include <unistd.h>
#include <string>
#include <phread.h>
#include <ctime>
using namespace std;

#define NUM 10

class ThreadData
{
public:
    ThreadData(const string& name, int id, time_t createTime):_name(name), _id(id), _createTime((uint64_t)createTime)
    {}
    ~ThreadData()
    {}
public:
    string _name;
    int _id;
    uint64_t _createTime;
}


void* thread_run(void* args)
{
    //char* name = (char*)args;
    ThreadData* td = static_cast<ThreadData*>(args);//强转类型
    while(true)
    {
        cout << "thread is running, name" << td->_name << "create time: " << td->_createTime << "index: " <<td->_id << endl;
        sleep(4);
        break;
    }
    delete td ;
    pthread_exit(td);
    //return nullptr;
}

int main()
{
    pthread_t tids[NUM];
    for(int i = 0; i < NUM; i++)
    {
        //char* tname = new char[64];
        char tname[64];
        snprintf(tname, 64, "thread-%d", i + 1);
        ThreadData* td = new ThreadData(tname, i + 1, time(nullptr));
        pthread_create(tids + i, nullptr, thread_run, td);
    }
    void* ret = nullptr;
    for(int i = 0; i < NUM; i++)
    {
        int n = pthread_join(tids[i], &ret);
        if(n != 0) cerr << "pthread_join error" << endl;
        cout << "thread quit: " << (uint64_t)ret << endl;
    }
    cout << "all thread quit..." << endl;
    /*while(true)
    {
        cout << "main thread running, new thread id: " << endl;
        sleep(1);
    }*/
    return 0;
}

也可以把上一个进程退出时的数据等放回到类里面。在类里面放一个int变量,表示状态位,用enum来指定状态位,下面就这样写

    void* ret = nullptr;
    for(int i = 0; i < NUM; i++)
    {
        int n = pthread_join(tids[i], &ret);
        if(n != 0) cerr << "pthread_join error" << endl;
        //cout << "thread quit: " << (uint64_t)ret << endl;
        ThreadData* td = static_cast<ThreadData*>(ret);
        if(td->_status == OK)
        {
            cout << td->_name << endl;
        }
        delete td;
    }

取消正在终止的线程

在这里插入图片描述

void* threadRun(void* args)
{
    const char* name = (const char*)args;//static_cast<const char*>,传过来args参数的时候实际上是一个指针,指向字符串常量的起始地址
    int cnt = 5;
    while(cnt)
    {
        cout << name << "is running: " << cnt-- << "obtain self id: " << pthread_self() << endl;//查看本线程的id
        sleep(1);
    }
    pthread_exit((void*)1);
    //PTHREAD_CANCELED == ((void*)-1)
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, threadRun, (void*)"thread 1");//省去一个变量,直接传一个字符串
    sleep(1);
    pthread_cancel(tid);
    void* ret = nullptr;
    pthread_join(tid, &ret);//join必须做,防止僵尸进程的类似问题
    cout << "new thread exit: " << (int64_t)ret << "quit thread: " << tid << endl;
    return 0;
}

如果一个进程被取消,那么返回的是PTHREAD_CANCELED,也就是((void*)-1)。

4、线程分离

主线程在等待新线程时可以不阻塞,而采用线程分离的方法来让主线程去做其它事,也可以主线程自己分离自己。一个线程如果被分离了就无法再被join,强行join就会报错。一个线程自带一个属性,就是可join,一旦分离,这个属性就无了。

在这里插入图片描述

#include <iostream>
#include <unistd.h>
#include <string>
#include <cstdio>
#include <cstring>
#include <phread.h>
#include <ctime>
using namespace std;

void* threadRun(void* args)
{
    string name = static_cast<const char*>(args);
    int cnt = 5;
    while(cnt)
    {
        cout << name << " : " << cnt-- << endl;
        sleep(1);
    }
    return nullptr;
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, threadRun, (void*)"thread 1");
    pthread_detach(tid);
    int n = pthread_join(tid, nullptr);
    if(0 != n)
    {
        cerr << "error: " << n << " : " << strerror(n) << endl;
    }
    return 0;
}

会打印错误。如果不在主线程分离,而在新线程分离,开头写上pthread_detach(pthread_self()),最后发现会正常输出,但如果在join之前sleep(1),就会打印一个就退出。

线程在创建的时候并没有决定执行的顺序,如果没有sleep。直接join,join相当于把主线程挂起,并且此时会检查这个新线程是否是可以join的,然后进入函数,才进行分离,但是主线程不知道。

本篇代码

结束。

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

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

相关文章

Java集合回顾

能不能和你竭尽全力奔跑 / 向着海平线 / 余晖消逝之前都不算终点 文章目录 集合概述Java 集合概览List, Set, Queue, Map 四者的区别&#xff1f;集合框架底层数据结构总结如何选用集合?为什么要使用集合&#xff1f; ListArrayList 和 Array&#xff08;数组&#xff09;的区…

Java SpringBoot自动化网页爬虫项目

介绍 Java SpringBoot自动化网页爬虫&#xff0c;以图形化方式定义爬虫流程&#xff0c;不写代码即可完成爬虫。 平台以流程图的方式定义爬虫,是一个高度灵活可配置的爬虫平台 功能根据需要可定制化开发。 特性 支持Xpath/JsonPath/css选择器/正则提取/混搭提取 支持JSON/XML/二…

aop+springboot实现数据字典表

文章目录 概要整体架构流程目录结构方式pom文件信息application.yml文件信息aop实现方式(重点方式)我们这里主要的实现了&#xff0c;就是在前段请求数据的时候&#xff0c;我们利用aop&#xff0c;拦截数据&#xff0c;将code编码进行翻译&#xff0c;翻译的方式就是我们将cod…

LabVIEWCompactRIO 开发指南34 在模拟模式下调试

LabVIEWCompactRIO 开发指南34 在模拟模式下调试 在仿真模式下执行LabVIEW FPGA VI时&#xff0c;可以访问标准LabVIEW调试功能&#xff0c;包括突出显示执行、探测和断点。LabVIEW2013及更高版本包含了一个额外的调试工具&#xff0c;称为采样探针。在仿真中运行时插入FPGA设…

U盘超级加密3000试用版与正式版的区别有哪些?

U盘超级加密3000是一款专业的U盘加密软件&#xff0c;它可以为U盘、移动硬盘、内存卡等移动存储设备加密。软件拥有正式版和试用版&#xff0c;那么这两个版本有什么区别呢&#xff1f;下面我们就一起来了解一下。 U盘超级加密3000试用版和正式版的区别 打开软件时的区别 试用…

C++第三章:字符串、向量和数组

字符串、向量和数组 一、命名空间的using声明每个名字独立using声明头文件不应包含using声明 二、标准库类型string2.1 定义和初始化string对象直接初始化和拷贝初始化 2.2 string对象上的操作读写string对象读取未知数量的string对象使用getline读取一整行string的empty和size…

TypeScript9-声明文件

本篇文章来讲 TypeScript 的声明文件。 当我们在使用第三方库的时候&#xff0c;很多第三方库不是用 TS 写的&#xff0c;它们是通过原生的 JavaScript 或者是浏览器 / 或者是 node 提供的 run time 对象。如果我们直接使用 TS 肯定就会报编译不通过。 1. 声明语句 假设一个…

【学习日记2023.5.24】 之 用户端模块开发 用户端小程序_服务端接入微信认证_完善用户端商品浏览模块

文章目录 6. 用户端模块开发6.1 HttpClient6.1.1 介绍6.1.2 入门案例6.1.2.1 GET方式请求6.1.2.2 POST方式请求 6.2 微信小程序开发6.2.1 介绍6.2.2 准备工作6.2.3 入门案例6.2.3.1 小程序目录结构6.2.3.2 编写和编译小程序6.2.3.3 发布小程序 6.3 微信登录6.3.1 导入小程序代码…

MATLAB 之 绘制三维图形的基本函数、三维曲面和其他三维图形

文章目录 一、绘制三维曲线的基本函数二、三维曲面1. 平面网格坐标矩阵的生成2. 绘制三维曲面的函数3. 标准三维曲面 三、其他三维图形1. 三维条形图2. 三维饼图3. 三维实心图4. 三维散点图5. 三维杆图6. 三维箭头图 三维图形具有更强的数据表现能力&#xff0c;为此 MATLAB 提…

关于CSDN如何获得铁粉

一、发表高质量技术博客 获得铁粉首先是需要有粉丝关注&#xff0c;在CSDN有粉丝关注&#xff0c;就需要多发表写技术文章而且最好是高质量文章&#xff0c;条理清晰&#xff0c;复合当下主流技术&#xff0c;或者新的技术方向&#xff0c;图文并茂的那种。这样通过搜索引擎搜到…

虚拟专用网络-那些年你错过的“VPN 盲点”

我们先和海翎光电的小编一起了解一下什么是VPN,VPN的分类。对基础知识有一定的了解后&#xff0c;我们再来讲一下VPN的盲点。 VPN(全称&#xff1a;Virtual Private Network)虚拟专用网络&#xff0c;是依靠ISP和其他的NSP&#xff0c;在公共网络中建立专用的数据通信的网络技术…

Linux 网络基础(2)应用层(http/https协议、请求格式、响应格式、session、cookie、加密传输)

说明&#xff1a;网络基础2讲解的是应用层的典型协议&#xff0c; 通过对于典型协议的理解&#xff0c;来体会数据的网络传输的软件层面的流程与原理。 面试中网络通信相关问题占了很大的比重&#xff0c;而网络通信相关的问题大多都集中在网络基础2这个单元中 下面是应用层的位…

解决dpdk reserve的内存返回的虚拟地址和iova地址一样的问题

1. 背景: 在ubuntu20.04上用dpdk API: rte_memzone_reserve_aligned("L1L2_PCIE_MEMORY", 1.5*1024*1024*1024, rte_socket_id(), RTE_MEMZONE_1GB|RTE_MEMZONE_IOVA_CONTIG, RTE_CACHE_LINE_SIZE); 分配1.5…

a-form中的label超出隐藏

效果 代码&#xff1a; :deep(.ant-form-item-label) {display: flex;justify-content: flex-end;line-height: 16px; //这个数值视具体情况而定label { //这是关键white-space: nowrap;text-align: right;// color: #8a8a8a;max-width: 150px;// padding-right: 3…

OpenCV+ Qt Designer 开发人脸识别考勤系统

文章目录 1. 系统介绍2. 系统架构3. 开发步骤3.1 安装必要的库3.2 设计用户界面3.3 编写代码3.3.1 导入库3.3.2 连接数据库3.3.3 定义主窗口类3.3.4 实时显示摄像头画面3.3.5 进行人脸识别3.3.6 手动打卡3.3.7 显示打卡时间3.3.8 显示图片3.3.9 运行主程序 4. 总结 1. 系统介绍…

day13 - 对指纹图片进行噪声消除

在指纹识别的过程中&#xff0c;指纹图片通常都是现场采集的&#xff0c;受环境的影响会有产生很多的噪声点&#xff0c;如果直接使用&#xff0c;会对指纹的识别产生很大的影响&#xff0c;而指纹识别的应用场景又都是一些比较严肃不容有错的场合&#xff0c;所以去除噪声又不…

MySQL——存储引擎与索引应用

文章目录 一、 存储引擎1.1 MySQL结构1.2 存储引擎简介1.3 存储引擎特点1.3.1 InnoDB1.3.1.1 InnoDB 基本介绍1.3.1.2 InnoDB 逻辑存储结构 1.3.2 MyISAM1.3.3 Memory 1.4 三种引擎特点及区别1.5 存储引擎选择 二、 索引 - 重点2.1 介绍2.2 索引结构2.2.1 B-Tree 多路平衡二叉树…

网络安全面试题汇总(附答案)

作为从业多年的网络安全工程师&#xff0c;我深知在面试过程中面试官所关注的重点及考察的技能点。网络安全作为当前信息技术领域中非常重要的一部分&#xff0c;对于每一个从事网络安全工作的人员来说&#xff0c;不仅需要掌握一定的技术能力&#xff0c;更需要具备全面的综合…

Python学习之pygame模块介绍并制作代码雨

前言 对Python游戏有所了解的朋友都知道&#xff0c;在2D的游戏制作中&#xff0c;经常会用到一个模块pygame&#xff0c;他能帮助我们实现很多方便使用的功能&#xff0c;例如绘制窗口&#xff0c;反馈键盘鼠标信息&#xff0c;播放音频文件&#xff0c;渲染图片文字等等功能…

Java接口测试实战:掌握JMeter技能,让测试更高效!

目录 引言 一.java环境搭建 二.操作示例 1.添加信息头 2.添加请求路径和请求参数 3.添加单个文件 4添加文件集合 三.实操注意事项 1.登录 1.1登录界面断言注意事项 1.2登录界面定义token 2.首页 2.1http请求只有路径 2.2需要引用token变量 3.产品管理 3.1增加产…