【Linux】线程概念 | 线程控制

news2024/11/17 9:44:19

​🌠 作者:@阿亮joy.
🎆专栏:《学会Linux》
🎇 座右铭:每个优秀的人都有一段沉默的时光,那段时光是付出了很多努力却得不到结果的日子,我们把它叫做扎根
在这里插入图片描述

目录

    • 👉知识补充👈
    • 👉Linux线程概念👈
      • 什么是线程
      • 线程 VS 进程
      • 线程的优点
      • 线程的缺点
      • 线程异常
      • 线程用途
    • 👉线程控制👈
      • 线程创建
      • 线程终止
      • 线程等待
      • 线程分离
    • 👉总结👈

👉知识补充👈

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

👉Linux线程概念👈

什么是线程

在这里插入图片描述
在这里插入图片描述

  • 线程是在进程内部执行的,也就是说线程是在进程的地址空间内运行的,其是操作系统调度的基本单位。
  • 进程等于内核数据结构加上该进程对应的代码和数据,内核数据结构可能不止一个 PCB,进程是承担分配系统资源的基本实体,将资源分配给线程!
  • 那如何理解我们之前写的代码呢?其实我们之前学习的是只有一个执行流的进程,而今天学习的是具有多个执行流的进程(task_struct 是进程内部的一个执行流),所以这两者是不冲突的。
  • 在运行队列中排队的都是 task_struct,CPU 只能看到 task_struct,CPU 根本不关系当前调度的是进程还是线程,只关心 task_struct。所以,CPU 调度的基本单位是”线程”。
  • Linux 下的线程是轻量级进程,没有真正意义上的线程结构,没有为线程专门设计内核数据结构,而是通过 PCB 来模拟实现出线程的。
  • Linux 并不能直接给我们提供线程相关的接口,只能提供轻量级进程的接口!在用户层实现了一套多进程方案,以库的方式提供给用户进行使用,这个库就是 pthread 线程库(原生线程库)。

知道了什么是线程,我们来学习创建线程的接口,来验证一下上面的结论!

这里是引用
pthread_create 函数的功能是创建一个新的进程。thread 是输出型参数,返回进程的 ID;attr 设置线程的属性,attr 为 nullptr 表示使用默认属性;start_routine 是一个函数地址,即线程启动后要执行的函数;arg 是传给线程启动函数的参数。调用成功是返回 0,错误是返回错误码。

Makefile

mythread:mythread.cc
	g++ $^ -o $@ -std=c++11 -lpthread
.PHONY:clean
clean:
	rm -f mythread

注:使用原生线程库时,必须带上 -lpthread,告诉编译器你要链接原生线程库,否则就会产生链接错误。

在这里插入图片描述
在这里插入图片描述

// 注:一下代码是示例代码,有些许问题
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <cstdio>
#include <string>

using namespace std;

void* threadRun(void* args)
{
    string name = (char*)args;
    while(1)
    {
        cout << name << " id: " << getpid() << '\n' << endl;
        sleep(1);
    }
    return nullptr;
}

int main()
{
    pthread_t tid[5];
    char name[64];
    for (int i = 0; i < 5; i++)
    {
        snprintf(name, sizeof name, "%s-%d", "thread", i);
        pthread_create(tid + i, nullptr, threadRun, (void*)name);
        sleep(3); // 缓解传参的bug
    }

    while (true)
    {
        cout << "main thread, pid: " << getpid() << endl;
        sleep(3);
    }

    return 0;
}
ps -aL | head -1 && ps -aL | grep mythread | grep -v grep  #查找线程

在这里插入图片描述
将进程 16889 杀掉时,全部执行流都会终止。因为线程用的资源都是进程给的,而杀掉进程就要回收进程的资源,那么线程终止了是理所当然的。

线程是如何看到进程内部的资源的呢?

我们知道,线程的运行依赖于进程的资源,一旦进程退出,线程也会退出。那进程的哪些资源是线程之间共享的,哪些资源又是线程独自占用的呢?

进程的大多数资源都被线程所共享:

  • 文件描述符表,如果一个线程打开了一个文件,那么其他的线程也能够看到。
  • 每种信号的处理方式(SIG_IGN、SIG_DFL 或者自定义的信号处理函数)
  • 当前工作目录
  • 用户 ID 和组 ID
  • 进程地址空间的代码区、共享区
  • 已初始化、未初始化数据区,也就是全局变量
  • 堆区一般也是被所有线程共享的,但在使用时,认为线程申请的堆空间是线程私有的,因为只有这个线程拿到这段空间的其实地址

线程独自占用的资源:

  • 线程 ID
  • 一组寄存器。线程是 CPU 调度的基本单位,一个线程被调度一定会形成自己的上下文,那么这组寄存器必须是私有的,才能保证正常的调度。
  • 栈。每个线程都是要通过函数来完成某种任务的,函数中会定义各种临时变量,那么线程就需要有自己私有的栈来保存这些局部变量。
  • 错误码 errno、信号屏蔽字、调度优先级

线程 VS 进程

为什么线程的调度切换的成本更低呢?

线程进行切换时,进程地址空间和页表是不用换的。而进程进行切换时,需要将进程的上下文,进程地址空间、页表、PCB 等都要切换。CPU 内部是有 L1 ~ L3 的 Cache,CPU 执行指令时,会更具局部性原理将内存中的代码和数据预读到 CPU 的缓存中。如果是多线程,CPU 预读的代码和数据很大可能就会被所有的线程共享,那么进行线程切换时,下一个线程所需要的代码和数据很有可能已经被预读了,这样线程切换的成本就会更低!而进程具有独立性,进行进程切换时,CPU 的 Cache 缓存的代码和数据就会立即失效,需要将新进程的代码和数据重新加载到 Cache 中,所以进程切换的成本是更高的。

进程和线程的关系如下图:

在这里插入图片描述

线程的优点

  • 创建一个新线程的代价要比创建一个新进程小得多
  • 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多
  • 线程占用的资源要比进程少很多
  • 能充分利用多处理器的可并行数量
  • 在等待慢速 I / O 操作结束的同时,程序可执行其他的计算任务
  • 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
  • I / O 密集型应用,为了提高性能,将 I / O 操作重叠。线程可以同时等待不同的 I / O 操作

注:线程不是创建越多越好,因为线程切换也是有成本的,并不是不需要成本。创建线程太多了,线程切换的成本有可能就是最大的成本了。

线程的缺点

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

线程异常

  • 单个线程如果出现除零,野指针问题导致线程崩溃,进程也会随着崩溃。
  • 线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程。进程终止,该进程内的所有线程也就随即退出。

线程用途

  • 合理的使用多线程,能提高 CPU 密集型程序的执行效率。
  • 合理的使用多线程,能提高 I / O 密集型程序的用户体验(如生活中我们一边写代码一边下载开发工具,就是多线程运行的一种表现)。

👉线程控制👈

在这里插入图片描述
clone 函数可以创建线程或者子进程,可以设置回调函数,子进程的栈区,还有各种属性等等。除了 clone 函数,还有一个 vfork 函数。vfork 函数创建出来的子进程是和父进程共享进程地址空间的。

#include <iostream>
#include <string>
#include <unistd.h>
#include <cassert>
#include <sys/types.h>
#include <sys/wait.h>

using namespace std;

int globalVal = 100;

int main()
{
    int id = vfork();
    // int id = fork();
    assert(id != -1);
    
    if(id == 0)
    {
        // child process
        int count = 0;
        while(1)
        {
            cout << "child process -> globalVal: " << globalVal << endl;
            sleep(1);
            ++count;
            if(count == 5)
            {
                globalVal = 200;
                cout << "child process change globalVal!" << endl;
                exit(1);
            }
        }
    }

    //waitpid(id, nullptr, 0); // 为了演示现象就不等待子进程了
    // parent process
    while(1)
    {
        cout << "parent process -> globalVal: " << globalVal << endl;
        sleep(1);
    }

    return 0;
}

在这里插入图片描述


线程创建

线程创建的函数在上面已经提过了,就不在赘述了。我们也已经知道通过 kill 命令来杀掉进程,其余线程也会跟着终止。那么现在,我们就来验证一下线程出现异常导致进程终止。

#include <iostream>
#include <pthread.h>
#include <unistd.h>

using namespace std;

void* threadRoutine(void* args)
{
    while(1)
    {
        cout << "新线程: " << (char*)args << " running ..." << endl;
        sleep(1);
        int a = 100;
        a /= 0;
    }
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, threadRoutine, (void*)"thread one");

    while(1)
    {
        cout << "主线程: running ..." << endl;
        sleep(1); 
    }

    return 0;
}

在这里插入图片描述
结论:线程谁先运行与调度器相关。线程一旦异常都有可能导致整个进程整体退出!


线程终止

如果需要只终止某个线程而不终止整个进程,可以有三种方法:

  • 从线程函数 return。这种方法对主线程不适用,从main 函数 return 相当于调用 exit。
  • 线程可以调用 pthread_ exit 终止自己。
  • 一个线程可以调用 pthread_ cancel 终止同一进程中的另一个线程。

注:在多线程场景下,不要使用 exit 函数,exit 函数是终止整个进程的!

pthread_exit 函数

这里是引用

  • pthread_exit 函数的功能是终止线程。
  • retval:retval 不要指向一个局部变量。
  • 无返回值,跟进程一样,线程结束的时候无法返回到它的调用者(自身)。
#include <iostream>
#include <pthread.h>
#include <unistd.h>

using namespace std;

void* threadRoutine(void* args)
{
    int i = 0;
    while(1)
    {
        cout << "新线程: " << (char*)args << " running ..." << endl;
        sleep(1);
        if(i++ == 3) break;
    }
    cout << (char*)args << " quit" << endl;
    pthread_exit((void*)10);
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, threadRoutine, (void*)"thread one");

    void* ret = nullptr;
    pthread_join(tid, &ret);
    cout << "ret: "<< (long long)ret << " main thread wait done... main quit too" << endl;

    return 0;
}

在这里插入图片描述


pthread_cancel 函数

这里是引用
pthread_cancel 函数的功能是取消一个执行中的线程。thread 是线程的 ID,调用成功是返回 0,失败是返回错误码。

#include <iostream>
#include <pthread.h>
#include <unistd.h>

using namespace std;

void* threadRoutine(void* args)
{
    int i = 0;
    while(1)
    {
        cout << "新线程: " << (char*)args << " running ..." << endl;
        sleep(1);
    }
    cout << (char*)args << " quit" << endl;
    pthread_exit((void*)13);
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, threadRoutine, (void*)"thread one");
    // pthread_cancel(tid); // 不要一创建线程就取消它

    int count = 0;
    while(1)
    {
        cout << "main线程 running ..." << endl;
        sleep(2);
        count++;
        if(count == 5) break;
    }

    pthread_cancel(tid);
    cout << "pthread cancel tid: " << tid << endl;

    void* ret = nullptr;
    pthread_join(tid, &ret);
    cout << "ret: "<< (long long)ret << " main thread wait done... main quit too" << endl;

    return 0;
}

在这里插入图片描述
当一个线程被取消时,线程的退出结果是 -1(PTHREAD_CANCELED)。使用 pthread_cancel 函数的前提是线程已经跑起来了才能够取消,所以不要穿甲一个线程后就立马取消(可能刚创建的线程还没有跑起来)。一般情况下,都是用主线程来取消新线程的。如果使用新线程来取消主线程的话,这样会影响整个进程。

线程 ID 的深入理解

线程 ID 本质是一个地址!!!因为我们目前用的不是 Linux 自带的创建线程的接口,用的是 pthread 库中的接口!用户需要的是线程,而 Linux 系统只提供轻量级进程,无法完全表示线程,所以在用户和操作系统之间加了个软件层 pthread 库。操作系统承担轻量级进程的调度和内核数据结构的管理,而线程库要给用户提供线程相关的属性字段,包括线程 ID、栈的大小等等。

在这里插入图片描述

pthread_self 函数可以获取当前线程的 ID,既然能获得当前线程的 ID,那么线程就可以自己取消自己,但是这种方式不推荐!

在这里插入图片描述

线程局部存储:用 __thread 修饰全局变量带来的结果就是让每一个线程各自拥有一个全局变量,这就是线程的局部存储。

#include <iostream>
#include <pthread.h>
#include <unistd.h>

using namespace std;

__thread int g_val = 0;

void* threadRoutine(void* args)
{
    while(1)
    {
        cout << (char*)args << "  g_val:" << g_val << "  &g_val:" << &g_val << endl;
        g_val++;
        sleep(1);
    }
    return nullptr;
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, threadRoutine, (void*)"thread 1");

    while(1)
    {
        cout << "main thread" << "  g_val:" << g_val << "  &g_val:" << &g_val << endl;
        sleep(2);
    }

    pthread_join(tid, nullptr);

    return 0;
}   

在这里插入图片描述
去掉 __thread 修饰后,所有线程看到的全局变量都是同一个!__thread 所有 pthread 库给 g++ 编译器的一个编译选项!

在多线程的场景下进行进程替换

#include <iostream>
#include <pthread.h>
#include <unistd.h>

using namespace std;

__thread int g_val = 0;

void* threadRoutine(void* args)
{
    sleep(5);
    execl("/bin/ls", "ls", "-l", nullptr);
    while(1)
    {
        cout << (char*)args << "  g_val:" << g_val << "  &g_val:" << &g_val << endl;
        g_val++;
        sleep(1);
    }
    return nullptr;
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, threadRoutine, (void*)"thread 1");

    while(1)
    {
        cout << "main thread" << "  g_val:" << g_val << "  &g_val:" << &g_val << endl;
        sleep(1);
    }

    pthread_join(tid, nullptr);

    return 0;
}   

在这里插入图片描述

在多线程的场景下执行进程替换,那么先会将除主线程外的其它线程都终止掉,然后再进行进程替换。


线程等待

线程在创建并执行的时候,线程也是需要被等待的。如果不等待线程的话,会引起类似于进程的僵尸问题,进而导致内存泄漏。已经退出的线程,其空间没有被释放,仍然在进程的地址空间内。创建新的线程不会复用刚才退出线程的地址空间。

在这里插入图片描述
pthread_join 函数的功能是等待线程结束。thread
是要线程的 ID,retval 指向线程所执行的函数的返回值。调用该函数的线程将阻塞等待,直到 ID为 thread 的线程终止。thread 线程以不同的方法终止,通过 pthread_join 得到的终止状态是不同的,总结如下:

  • 如果 thread 线程通过 return 返回,retval 所指向的单元里存放的是 thread 线程函数的返回值。
  • 如果 thread 线程被别的线程调用 pthread_ cancel 异常终掉,retval 所指向的单元里存放的是常数
    PTHREAD_ CANCELED。
  • 如果 thread 线程是自己调用 pthread_exit 终止的,retval 所指向的单元存放的是传给 pthread_exit 的参数。
  • 如果对 thread 线程的终止状态不感兴趣,可以传 nullptr 给 retval 参数。
  • thread 线程函数的返回值不会考虑异常的情况,如果线程出现了异常,那么整个进程都会崩掉。注:状态寄存器是所有线程共享的。

在这里插入图片描述

#include <iostream>
#include <pthread.h>
#include <unistd.h>

using namespace std;

void* threadRoutine(void* args)
{
    int i = 0;
    while(1)
    {
        cout << "新线程: " << (char*)args << " running ..." << endl;
        sleep(1);
        if(i++ == 6) break;
    }
    cout << (char*)args << " quit" << endl;
    return nullptr;
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, threadRoutine, (void*)"thread one");

    pthread_join(tid, nullptr);	// 默认会阻塞等待
    cout << "main thread wait done... main quit too" << endl;

    return 0;
}

在这里插入图片描述

线程执行的函数的返回值是返回给主线程的,主线程通过该返回值来获取线程退出的状态。

#include <iostream>
#include <pthread.h>
#include <unistd.h>

using namespace std;

void* threadRoutine(void* args)
{
    int i = 0;
    while(1)
    {
        cout << "新线程: " << (char*)args << " running ..." << endl;
        sleep(1);
        if(i++ == 6) break;
    }
    cout << (char*)args << " quit" << endl;
    return (void*)10;
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, threadRoutine, (void*)"thread one");

    void* ret = nullptr;
    pthread_join(tid, &ret);
    cout << "ret: "<< (long long)ret << " main thread wait done... main quit too" << endl;

    return 0;
}

在这里插入图片描述


线程执行的函数的返回值可以多种多样,比如返回一段堆空间的起始地址。

#include <iostream>
#include <pthread.h>
#include <unistd.h>

using namespace std;

void* threadRoutine(void* args)
{
    int i = 0;
    int* ret = new int[7];
    while(1)
    {
        cout << "新线程: " << (char*)args << " running ..." << endl;
        sleep(1);
        ret[i] = i;
        if(i++ == 6) break;
    }
    cout << (char*)args << " quit" << endl;
    return (void*)ret;
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, threadRoutine, (void*)"thread one");

    int* ret = nullptr;
    pthread_join(tid, (void**)&ret);
    for(int i = 0; i < 7; ++i)
        cout << ret[i] << ' ';
    
    cout << endl;
    cout << "ret: "<< (long long)ret << " main thread wait done... main quit too" << endl;

    return 0;
}

在这里插入图片描述


线程分离

  • 默认情况下,新创建的线程是 joinable 的,线程退出后,需要对其进行 pthread_join 操作,否则无法释放资源,从而造成系统泄漏。
  • 如果不关心线程的返回值,join 是一种负担。这个时候,我们可以告诉系统:当线程退出时,自动释放线程资源,这就是线程分离。
  • 一般主线程时不退出的,当用户有个任务要处理,主线程就可以创建新线程来执行用户的任务,但主线程不关心任务处理的结果,那么就可以将该线程分离出去。
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <cerrno>
#include <cstring>

using namespace std;

__thread int g_val = 0;

void* threadRoutine(void* args)
{
    pthread_detach(pthread_self());
    while(1)
    {
        cout << (char*)args << "  g_val:" << g_val << "  &g_val:" << &g_val << endl;
        g_val++;
        break;
    }
    pthread_exit((void*)11);
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, threadRoutine, (void*)"thread 1");

    while(1)
    {
        cout << "main thread" << "  g_val:" << g_val << "  &g_val:" << &g_val << endl;
        sleep(1);
        break;
    }

    int n = pthread_join(tid, nullptr);
    cout << "n: " << n << " error string: " << strerror(n) << endl;

    return 0;
}   

在这里插入图片描述

注:joinable 和分离是冲突的,一个线程不能既是joinable 又是分离的。

如果线程被分离,但是该线程出现了异常,这样也会影响到整个进程。线程执行的是进程派发的任务,尽管线程被分离了,线程也离不开进程的资源,所以线程出现了异常也会导致进程终止。

在这里插入图片描述

注:C++ 11 的线程库也是调用了原生线程库的,所以在使用 C++ 的线程库时也要指定链接原生线程库。

👉总结👈

本篇博客主要讲解了什么是线程、线程和进程的区别、线程的优缺点、线程异常、线程用途以及线程控制等。那么以上就是本篇博客的全部内容了,如果大家觉得有收获的话,可以点个三连支持一下!谢谢大家!💖💝❣️

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

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

相关文章

机载雷达的时间简史

从地基起步 蝙蝠&#xff0c;虽然像人一样拥有双眼&#xff0c;但它看起东西来&#xff0c;用到的却不是眼睛。蝙蝠从鼻子里发出的超声波在传输过程中遇到物体后会立刻反弹&#xff0c;根据声波发射和回波接收之间的时间差&#xff0c;蝙蝠就可以轻易地判断出物体的位置。这一工…

解读:“出境标准合同”与“出境安全评估”要点与异同

《数据安全法》第四条及《个人信息保护法》第三章对数据出境、个人信息跨境提出明确要求&#xff0c;《数据安全法》与《个人信息保护法》存在互相包含、被包含、衔接、特性、独立性、相互补充等内涵。本文通过上位法互相衔接、关联、特性的思路&#xff0c;去观察《个人信息出…

2018年MathorCup数学建模C题陆基导弹打击航母的数学建模与算法设计解题全过程文档及程序

2018年第八届MathorCup高校数学建模挑战赛 C题 陆基导弹打击航母的数学建模与算法设计 原题再现&#xff1a; 火箭军是保卫海疆主权的战略力量,导弹是国之利器。保家卫国,匹夫有责。为此,请参赛者认真阅读"陆基反舰导弹打击航母的建模示意图"。(附图 1 )参考图中的…

【MyBatis-Plus】实现字段自增的5种实现方式 setSql @TableField @Version

在使用mybatis-plus的时候&#xff0c;我们需要对某条数据的单一字段进行操作&#xff0c;又不想查出整条数据拿到字段值再加一赋值&#xff0c;此时可以用下面5种方式来实现。 方式一:setSql 官网文档Mybatis-Plus&#xff1a;setSql 官方文档示例: i.标准setSql 使用setS…

好的提高代码质量的方法有哪些?有什么经验和技巧?

用于确保代码质量的6个高层策略&#xff1a; 1 编写易于理解的代码 考虑如下这段文本。我们有意地使其变得难以理解&#xff0c;因此&#xff0c;不要浪费太多时间去解读。粗略地读一遍&#xff0c;尽可能吸收其中的内容。 〓ts〓取一个碗&#xff0c;我们现在称之为A。取一…

《实践论》笔记及当下反思(一)

目录 一句话概括核心观点 笔记 1、人的认识&#xff0c;主要地依赖于物质的生产活动 2、只有人们的社会实践&#xff0c;才是人们对于外界认识的真理性的标准 3、强调理论对于实践的依赖关系&#xff0c;理论的基础是实践&#xff0c;又转过来为实践服务 4、你要知道梨子…

浅析三款大规模分布式文件系统架构设计

什么是文件系统 当提到文件系统&#xff0c;大部分人都很陌生。但我们每个人几乎每天都会使用到文件系统&#xff0c;比如大家打开 Windows、macOS 或者 Linux&#xff0c;不管是用资源管理器还是 Finder&#xff0c;都是在和文件系统打交道。如果大家有自己动手装过操作系统的…

Nacos开机自动启动(不用手动shutdown.cmd)

目录 一.前言 二.利用win服务自动启动nacos 1.1下载WinSW 1.2.准备工作和测试 三.利用脚本启动 21.编写一个bat文件,建议放在nacos的根目录下 四.总结: 一.前言 在实际开发中我们的项目配置nacos时, 每次本机重启电脑和更换网络ip时nacos就会挂机(death), 我们还要手动再去…

什么是B+树

B树是一种树数据结构。B树索引是B树在数据库中的一种实现&#xff0c;是最常见也是数据库中使用最为频繁的一种索引。 先来了解一下什么是索引&#xff1f; 一、索引 数据都是存储在硬盘上的&#xff0c;查询数据不可避免的需要进行IO操作。 索引是一种数据结构&#xff0c…

学习MvvmLight工具

最近学习了一下MvvmLight&#xff0c;觉得有些功能还是挺有特色的&#xff0c;所以记录一下 首先新建也给WPF程序 然后在Nuget里面安装MvvmLightLib 包&#xff0c;安装上面那个也可以&#xff0c;但是安装上面那个会自动在代码里面添加一些MvvmLight的demo &#xff0c;安装M…

零钱兑换II 组合总和 Ⅳ 爬楼梯(0-1背包)

零钱兑换II 但本题和纯完全背包不一样&#xff0c;纯完全背包是凑成背包最大价值是多少&#xff0c;而本题是要求凑成总金额的物品组合个数&#xff01; 回归本题&#xff0c;动规五步曲来分析如下&#xff1a; 确定dp数组以及下标的含义 dp[j]&#xff1a;凑成总金额j的货币组…

【MySQL】P10 事务(1)事务执行,提交与回滚

事务执行&#xff0c;提交与回滚基本概念什么是事务&#xff1f;什么是自动提交&#xff1f;错误事务案例解决方案方式一&#xff1a;控制事务方式二&#xff1a;手动提交与回滚自动提交更改为手动提交进行手动提交错误进行回滚基本概念 什么是事务&#xff1f; 每一个SQL语句…

Unity资源热更新框架

什么是热更新&#xff1f; 游戏上线后&#xff0c;玩家下载第一个版本&#xff08;1G左右或者更大&#xff09;&#xff0c;在之后运营的过程中&#xff0c;如果需要更换UI显示&#xff0c;或者修改游戏的逻辑&#xff0c;这个时候&#xff0c;如果不使用热更新&#xff0c;就需…

VS2008生产动态库、静态库调用案例

静态库创建静态库打开VS2008创建项目如下&#xff1a;选择静态库创建文件// add.h #ifndef _ADD_H_ #define _ADD_H_#include <stdio.h>#ifdef __cplusplus extern "C" { #endif__declspec(dllexport) int myAdd(int a, int b);#ifdef __cplusplus } #endif#e…

Linux:IO库函数

目录标准库IO函数一、fopen二、fwrite三、fread四、fseek五、fclose在编写程序时&#xff0c;离不开IO操作&#xff0c;最常见的IO操作就是用printf函数进行打印&#xff0c;本文主要介绍的是封装后的IO库函数。 标准库IO函数 常使用的IO库函数如下&#xff1a; 函数作用fop…

u盘系统文件删除后的五种恢复方法

U盘是我们日常生活中使用较为普遍的移动存储设备&#xff0c;由于其便携性和易用性广受人们的欢迎。然而&#xff0c;在我们使用U盘的过程中&#xff0c;经常会出现误删文件的情况&#xff0c;例如本来要作为启动盘的u盘&#xff0c;误删里面的系统文件怎么办&#xff1f;当U盘…

MySQL的同步数据Replication功能

MySQL提供了Replication功能&#xff0c;可以实现将一个数据库的数据同步到多台其他数据库。前者通常称之为主库&#xff08;master&#xff09;&#xff0c;后者则被称从库&#xff08;slave&#xff09;。MySQL复制过程采用异步方式&#xff0c;但延时非常小&#xff0c;秒级…

C51---超声波测距

1.主要实现功能&#xff1a;当手靠近传感器时&#xff0c;灯亮&#xff1b;手离开&#xff0c;灯灭 2.器件&#xff1a;51单片机、HC-SR04超声波测距传感器 3.代码&#xff1a; #include "reg52.h" //距离小于10cm&#xff0c;D5亮&#xff0c;D6灭&#xff0c;反之…

Lombok使用@Builder无法build父类属性

文章目录问题描述解决方案使用示例lombok Builder注解和build父类属性问题1、简介2.使用3、Builder注解对类做了什么&#xff1f;问题描述 实体类使用Lombok的Builder来实现Builder模式&#xff0c;但是如果使用了extend继承&#xff0c;则子类无法通过Builder来Build父类属性…

深度学习应用技巧4-模型融合:投票法、加权平均法、集成模型法

大家好&#xff0c;我是微学AI&#xff0c;今天给大家介绍一下&#xff0c;深度学习中的模型融合。它是将多个深度学习模型或其预测结果结合起来&#xff0c;以提高模型整体性能的一种技术。 深度学习中的模型融合技术&#xff0c;也叫做集成学习&#xff0c;是指同时使用多个…