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

文章目录

  • 👉知识补充👈
  • 👉Linux线程概念👈
    • 什么是线程
    • Makefile
    • 线程 VS 进程
    • 线程的优点
    • 线程的缺点
    • 线程异常
    • 线程用途
  • 👉线程控制👈
    • 线程终止
    • pthread_exit 函数
    • pthread_cancel 函数
    • 线程 ID 的深入理解
    • 在多线程的场景下进行进程替换
  • 线程等待
    • 线程分离
  • 👉总结👈

👉知识补充👈

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

👉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;
}

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

线程终止

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

  • 从线程函数 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++ 的线程库时也要指定链接原生线程库。

👉总结👈

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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

LongVU:用于长视频语言理解的空间时间自适应压缩

晚上闲暇时间看到一种用于长视频语言理解的空间时间自适应压缩机制的研究工作LongVU&#xff0c;主要内容包括&#xff1a; 背景与挑战&#xff1a;多模态大语言模型&#xff08;MLLMs&#xff09;在视频理解和分析方面取得了进展&#xff0c;但处理长视频仍受限于LLM的上下文长…

爬虫项目基础知识详解

文章目录 Python爬虫项目基础知识一、爬虫与数据分析1.1 Python中的requests库Requests 库的安装Requests 库的 get() 方法爬取网页的通用代码框架HTTP 协议及 Requests 库方法Requests 库主要方法解析 1.2 python中的json库1.3 xpath学习之python中lxml库html了解html结构html…

LeetCode - #152 乘积最大子数组(Top 100)

文章目录 前言1. 描述2. 示例3. 答案关于我们 前言 本题为 LeetCode 前 100 高频题 我们社区陆续会将顾毅&#xff08;Netflix 增长黑客&#xff0c;《iOS 面试之道》作者&#xff0c;ACE 职业健身教练。&#xff09;的 Swift 算法题题解整理为文字版以方便大家学习与阅读。 …

记一次跑前端老项目的问题

记一次跑前端老项目的问题 一、前言二、过程1、下载依赖2、启动项目3、打包 一、前言 在一次跑前端老项目的时候&#xff0c;遇到了一些坑&#xff0c;这里记录一下。 二、过程 1、下载依赖 使用 npm install下载很久&#xff0c;然后给我报了个错 core-js2.6.12: core-js…

无约束最优化问题的求解算法

无约束最优化问题的求解算法 使用梯度下降法目的和原因 目的 梯度下降法(Gradient Descent)是一个算法&#xff0c;但不是像多元线性回归那样是一个具体做回归任务的算法&#xff0c;而是一个非常通用的优化算法来帮助一些机器学习算法求解出最优解的&#xff0c;所谓的通用就是…

VideoBooth: Diffusion-based Video Generation with Image Prompts

VideoBooth: Diffusion-based Video Generation with Image Prompts 概括 文章提出了一个视频生成模型VideoBooth&#xff0c;输入一张图片和一个文本提示词&#xff0c;即可输出保持图片中物体且符合文本提示词要求的视频。 方法 粗-细两阶段设计&#xff1a;1&#xff09;…

AndroidAutoSize实战教程:今日头条屏幕适配方案详解

如何在项目中结合 AndroidAutoSize 来进行今日头条屏幕适配&#xff0c;我会具体讲解如何用 AndroidAutoSize 实现屏幕适配&#xff0c;并结合 Kotlin 代码举例分析。 通过 AndroidAutoSize 库来实现屏幕适配&#xff0c;确保在不同的屏幕尺寸、分辨率、密度下&#xff0c;应用…

为什么使用3DMAX插件会出现系统崩溃?

使用3DMAX插件时出现系统崩溃&#xff0c;可能涉及多个方面的原因。以下是一些主要的原因及相应的解决方案&#xff1a; 一、插件兼容性问题 版本不兼容&#xff1a; 旧版插件可能无法与最新版本的3DMAX完全兼容&#xff0c;导致系统崩溃。解决方案&#xff1a;更新插件至最新…

[pdf,epub]228页《分析模式》漫谈合集01-45提供下载

《分析模式》漫谈合集01-45的pdf、epub文件提供下载。已上传至本号的CSDN资源。 如果CSDN资源下载有问题&#xff0c;可到umlchina.com/url/ap.html。 已排版成适合手机阅读&#xff0c;pdf的排版更好一些。 ★UMLChina为什么叒要翻译《分析模式》&#xff1f; ★[缝合故事]…

Python 【图像分类】之 PyTorch 进行猫狗分类功能的实现(Swanlab训练可视化/ Gradio 实现猫狗分类 Demo)

Python 【图像分类】之 PyTorch 进行猫狗分类功能的实现(Swanlab训练可视化/ Gradio 实现猫狗分类 Demo) 目录 Python 【图像分类】之 PyTorch 进行猫狗分类功能的实现(Swanlab训练可视化/ Gradio 实现猫狗分类 Demo) 一、简单介绍 二、PyTorch 三、CNN 1、神经网络 2、卷…

HarmonyOS开发:关于签名信息配置详解

目录 前言 签名信息的重要性 签名的方式 自动化签名 1、连接真机 2、选择 手动签名 &#xff08;一&#xff09;生成密钥和证书请求文件 &#xff08;二&#xff09;申请调试证书 &#xff08;三&#xff09;注册调试设备 &#xff08;四&#xff09;申请调试Profil…

Java的Stirng、StringBuilder、StringJoiner

黑马程序员Java个人笔记 目录 字符串比较 比较 boolean equals boolean equalsIgnoreCase 键盘录入和定义的字符串的比较 StringBuilder 打印 ​编辑 添加元素 反转 获取长度 toString 练习 对称字符串 拼接字符串 StringJoiner 概述 ​编辑 构造方法 只有…

fastadmin 后台插件制作方法

目录 一&#xff1a;开发流程 二&#xff1a;开发过程 &#xff08;一&#xff09;&#xff1a;后台功能开发 &#xff08;二&#xff09;&#xff1a;功能打包到插件目录 &#xff08;三&#xff09;&#xff1a;打包插件 &#xff08;四&#xff09;&#xff1a;安装插件…

视频自学笔记

一、视频技术基本框架 二、视频信号分类 2.1信号形式 2.1.1模拟视频 模拟视频是指由连续的模拟信号组成的视频图像&#xff0c;以前所接触的电影、电视都是模拟信号&#xff0c;之所以将它们称为模拟信号&#xff0c;是因为它们模拟了表示声音、图像信息的物理量。摄像机是获…

Docker单机网络:解锁本地开发环境的无限潜能

作者简介&#xff1a;我是团团儿&#xff0c;是一名专注于云计算领域的专业创作者&#xff0c;感谢大家的关注 座右铭&#xff1a; 云端筑梦&#xff0c;数据为翼&#xff0c;探索无限可能&#xff0c;引领云计算新纪元 个人主页&#xff1a;团儿.-CSDN博客 目录 前言&#…

Golang 八股(持续补充...)

目录 进程、线程、协程 Go语言——垃圾回收 GC的触发条件 GC调优 GMP调度和CSP模型 Groutine的切换时机 Goroutine调度原理 Goroutine的抢占式调度 Context结构原理 Context原理 Golang内存分配机制 竞态、内存逃逸 golang内存对齐机制 golang中new和make的区别&a…

手机中的核心SOC是什么?

大家好&#xff0c;我是山羊君Goat。 常常听说CPU&#xff0c;中央处理器等等的&#xff0c;它是一个电脑或单片机系统的核心&#xff0c;但是对于SOC可能相比于CPU了解的人没有那么广泛。 所以SOC是什么&#xff1f; SOC全称是System on Chip&#xff0c;就是片上系统&#…

绿虫光伏设计系统:清洁能源的未来

煤炭、石油、天然气是我们现在依赖的重要能源&#xff0c;但这些能源难以再生&#xff0c;而且开采过程中会产生污染。太阳能发电作为清洁能源的一种重要形式&#xff0c;受到了越来越多的关注。绿虫光伏发电系统&#xff0c;不仅考虑到其发电效率&#xff0c;还可以考虑其经济…

R语言 | ComplexHeatmap 画注释若干基因的热图 //todo

一般顺序&#xff1a; 先用 pheatmap 聚类再用 ComplexHeatmap 做可视化&#xff1a;添加顶部、左侧聚类颜色&#xff0c;显示若干代表性基因 genec("Gene18", "Gene19", "Gene7","Gene3", "Gene9", "Gene15") …

springmvc的简介

SpringMVC的介绍与第一个程序的开发步骤 1. 介绍 SpringMVC是一个实现了MVC架构模式的Web框架&#xff0c;底层基于Servlet实现。 SpringMVC已经将MVC架构模式实现了&#xff0c;因此只要我们是基于SpringMVC框架写代码&#xff0c;编写的程序就是符合MVC架构模式的。&#x…