Linux系统编程多线程(C++)

news2024/11/28 20:48:52

目录

【1】引入如何看待地址空间和列表

【2】什么是线程

【3】线程的优点

【4】线程的缺点

【5】线程异常

【6】线程用途

【7】线程VS进程

【8】Linux线程控制

【8.1】查看轻量级线程指令

【8.2】线程创建

【8.2.1】POSIX线程库

【8.2.2】创建线程

【8.2.3】一次性创建一个线程

【8.2.4】一次性创建多个线程

【8.2】线程终止

【8.3】线程取消

【8.4】线程等待

【8.5】分离线程

【9】线程ID及进程地址空间布局


【1】引入如何看待地址空间和列表

  • 地址空间是进程能看到的资源窗口。

  • 页表决定,进程真正拥有资源的情况。

  • 合理的对地址空间+页表进行资源划分,我们就可以对一个进程所有的资源进行分类

【2】什么是线程  

  • 在一个程序里的一个执行路线就叫做线程(thread)。更准确的定义是:线程是“一个进程内部的控制序列”。

  • 线程是进程内的一个执行流。

  • 一切进程至少都有一个执行线程。

  • 线程在进程内部运行,本质是在进程地址空间内运行。

  • 在Linux系统中,在CPU眼中,看到的PCB都要比传统的进程更加轻量化。

  • 透过进程虚拟地址空间,可以看到进程的大部分资源,将进程资源合理分配给每个执行流,就形成了线程执行流。

        因为我们可以通过虚拟地址空间+页表的方式对进程进行资源划分,单个“进程”执行力度,一定要比之前的进程要细!

【思考】如果我们OS真的要专门设计“线程”概念,OS未来要不要管理这个线程呢?

        一定要为线程设计专门的数据结构,表示线程对象---称为TCB(Windows是这样的)。

        单纯从线程调度角度,线程和进程有很多地方是重叠的,所有Linux工程师,不想给Linux“线程”专门设计对应的数据结构,而是直接复用PCB,用PCB用来表示Linux内部的“线程”。

        线程是在进程内部运行的,线程在进程的地址空间内运行,拥有进程的一部分资源!

思考:线程来了,问题也随之而来!

  • 什么是进程呢?

之前:进程 = 内核数据结构 + 进程对应的代码和数据。

现在:从内核角度,进程分配系统资源的实体。

  • 什么是线程呢?

现在:CPU调度的基本单位。

  • 如何看待我们之前学习进程时,对应的进程概念呢?和今天的线程冲突吗?

之前:承担系统资源的基本实体,只不过内部只有一个执行流!

现在:一个进程内部可以有多个执行流!

  • 站在CPU的角度

之前:调用进程

现在:调用进程里的一个分支

但是:CPU并不关心,今天喂给CPU的task_struct <= 历史的tesk_struct的含义

思考:综合以上所总结Linux内核中有没有真正意义的线程呢?

        没有!,Linux是用进程PCB来模拟线程,是一种完全属于自己的一套线程方案,站在CPU的视角,每一个PCB,都可以称之为叫做轻量级进程,Linux*线程是CPU调度的基本单位,而进程是承担分配系统资源的基本单位,进程用来整体申请资源,线程用来伸手向进程要资源,Linux中没有真正意义的线程,

【3】线程的优点

  • 创建一个新线程的代价要比创建一个新进程小得多。

  • 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多。

  • 进程:切换页表&&虚拟地址空间&&切换PCB&&上下文切换。

  • 线程:切换PCB&&上下文切换。

  • 线程切换cache不用

  • 线程占用的资源要比进程少很多。

  • 能充分利用多处理器的可并行数量。

  • 在等待慢速I/O操作结束的同时,程序可执行其他的计算任务。

  • 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现。

  • I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。

【4】线程的缺点

  • 性能损失:一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变。

  • 健壮性降低:编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。

  • 缺乏访问控制:进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。

  • 编程难度提高:编写与调试一个多线程程序比单线程程序困难得多。

由于Linux中没有真正意义的线程 -》而OS只认线程,用户(程序员)也只认线程,Linux边无法直接提供创建线程的系统调用接口,而只能给我们提供创建轻量级进程的接口!

【5】线程异常

  • 单个线程如果出现除零,野指针问题导致线程崩溃,进程也会随着崩溃。

  • 线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该进程内的所有线程也就随即退出。

【6】线程用途

  • 合理的使用多线程,能提高CPU密集型程序的执行效率。

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

【7】线程VS进程

  • 进程是资源分配的基本单位

  • 线程是调度的基本单位

  • 线程共享进程数据,但也拥有自己的一部分数据:

  • 线程ID

  • 一组寄存器

  • errno

  • 信号屏蔽字

  • 调度优先级

进程的多个线程共享 同一地址空间,因此Text Segment、Data Segment都是共享的,如果定义一个函数,在各线程中都可以调用,如果定义一个全局变量,在各线程中都可以访问到,除此之外,各线程还共享以下进程资源和环境:

  • 文件描述符表

  • 每种信号的处理方式(SIG_ IGN、SIG_ DFL或者自定义的信号处理函数)

  • 当前工作目录

  • 用户id和组id

【最理想的创建合理性】 CPU的核数决定线程的个数,CPU的个数决定进程的个数。

【8】Linux线程控制

【8.1】查看轻量级线程指令

// 查看轻量级进程指令
ps -aL

【8.2】线程创建

【8.2.1】POSIX线程库

  • 与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都是以“pthread_”打头的。

  • 要使用这些函数库,要通过引入头文<phtread.h>

  • 链接这些线程函数库时要使用编译器命令的“-lpthread”选项。

【8.2.2】创建线程

// 功能:创建一个新的线程
// 原型
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *
(*start_routine)(void*), void *arg);

// 参数
// thread:返回线程ID
// attr:设置线程的属性,attr为NULL表示使用默认属性
// start_routine:是个函数地址,线程启动后要执行的函数
// arg:传给线程启动函数的参数
// 返回值:成功返回0;失败返回错误码

【错误检查】

  • 传统的一些函数是,成功返回0,失败返回-1,并且对全局变量errno赋值以指示错误。

  • pthreads函数出错时不会设置全局变量errno(而大部分其他POSIX函数会这样做)。而是将错误代码通过返回值返回。

  • pthreads同样也提供了线程内的errno变量,以支持其它使用errno的代码。对于pthreads函数的错误,建议通过返回值业判定,因为读取返回值要比读取线程内的errno变量的开销更小。

【实例代码】

// Makefile 文件》》》》》》》》》》》》》》》》
cc=g++
standard=-std=c++11

myThread:Thread.cc
	$(cc) -o $@ $^ $(standard) -lpthread

.PHONY:clean
clean:
	rm -rf myThread 

// Thread.cc文件》》》》》》》》》》》》》》》》
#include <iostream>
#include <cstdio>
#include <unistd.h>
#include <pthread.h>
using namespace std;

/* 线程函数 */
void* thread_rotine(void* args) {
    const char* buffer = (const char*)args;

    while(true) {
        cout << "我是新线程,我正在运行,我的名字叫做:" << buffer << endl;
        sleep(1);
    }
}

/* 入口函数 */
int main() {
    // 线程的描述符.
    pthread_t tid;
    // 参数传递依次是:描述符 nullptr 线程函数 传送的参数
    pthread_create(&tid, nullptr, thread_rotine, (void*)"thread_one");

    // 主线程:
    while(true) {
        char buffer[1024];
        snprintf(buffer, sizeof(buffer), "0x%x", tid);

        cout << "我是主线程,我正在运行,我的tid是:" << buffer << endl; 
        sleep(1);
    }

    return 0;
}

// 打印结果:
我是主线程,我正在运行,我的tid是:0x33d67700
我是新线程,我正在运行,我的名字叫做:thread_one

我是主线程,我正在运行,我的tid是:0x33d67700
我是新线程,我正在运行,我的名字叫做:thread_one

我是主线程,我正在运行,我的tid是:0x33d67700
我是新线程,我正在运行,我的名字叫做:thread_one

我是主线程,我正在运行,我的tid是:0x33d67700
我是新线程,我正在运行,我的名字叫做:thread_one

 

【概念】 线程一旦被创建,几乎所有的资源都是被所有线程共享的!

// Makefile 文件》》》》》》》》》》》》》》》》
cc=g++
standard=-std=c++11

myThread:Thread.cc
	$(cc) -o $@ $^ $(standard) -lpthread

.PHONY:clean
clean:
	rm -rf myThread

// Thread.cc文件》》》》》》》》》》》》》》》》
#include <iostream>
#include <string>
#include <cstdio>
#include <unistd.h>
#include <pthread.h>
using namespace std;

/* 测试共享的全局变量和函数 */ 
int g_num = 0;

const string Func() {
    return "我是一个独立的函数!";
}

/* 线程函数 */
void* thread_rotine(void* args) {
    const char* buffer = (const char*)args;

    while(true) {
        cout << "我是新线程,我正在运行,我的名字叫做:" << buffer << " " << Func() << " " << g_num++ << endl;
        sleep(1);
    }
}

/* 入口函数 */
int main() {
    // 线程的描述符.
    pthread_t tid;
    // 参数传递依次是:描述符 nullptr 线程函数 传送的参数
    pthread_create(&tid, nullptr, thread_rotine, (void*)"thread_one");

    // 主线程:
    while(true) {
        char buffer[1024];
        snprintf(buffer, sizeof(buffer), "0x%x", tid);

        cout << "我是主线程,我正在运行,我的tid是:" << buffer << " " << Func() << " " << g_num << endl;
        sleep(1);
    }

    return 0;
}

// 打印结果:现象就是线程在进程中的资源共享
我是新线程,我正在运行,我的名字叫做:thread_one 我是一个独立的函数! 2
我是主线程,我正在运行,我的tid是:0x78002700 我是一个独立的函数! 3
我是新线程,我正在运行,我的名字叫做:thread_one 我是一个独立的函数! 3
我是主线程,我正在运行,我的tid是:0x78002700 我是一个独立的函数! 4
我是主线程,我正在运行,我的tid是:0x78002700 我是一个独立的函数! 4
我是新线程,我正在运行,我的名字叫做:thread_one 我是一个独立的函数! 4
我是主线程,我正在运行,我的tid是:0x78002700 我是一个独立的函数! 5
我是新线程,我正在运行,我的名字叫做:thread_one 我是一个独立的函数! 5
我是主线程,我正在运行,我的tid是:0x78002700 我是一个独立的函数! 6
我是新线程,我正在运行,我的名字叫做:thread_one 我是一个独立的函数! 6
我是主线程,我正在运行,我的tid是:0x78002700 我是一个独立的函数! 7
我是新线程,我正在运行,我的名字叫做:thread_one 我是一个独立的函数! 7
我是主线程,我正在运行,我的tid是:0x78002700 我是一个独立的函数! 8
我是新线程,我正在运行,我的名字叫做:thread_one 我是一个独立的函数! 8

【概念】线程也一定有自己私有的资源,什么资源应该是线程私有的呢?

  • PCB属性私有。

  • 要有一定私有上下文结构。

  • 每一个线程都要有自己独立的栈结构。

【概念】线程与进程之间相对比,线程之间的切换需要操作系统做的工作要少很多。

  • 进程:切换页表 && 虚拟地址空间 && 切换PCB && 上下文切换。

  • 线程:切换PCB && 上下文切换。

【8.2.3】一次性创建一个线程

// Makefile 文件》》》》》》》》》》》》》》》》
cc=g++
standard=-std=c++11

myThread:Thread.cc
	$(cc) -o $@ $^ $(standard) -lpthread

.PHONY:clean
clean:
	rm -rf myThread

// Thread.cc文件》》》》》》》》》》》》》》》》
#include <iostream>
#include <string>
#include <cstdio>
#include <unistd.h>
#include <pthread.h>
using namespace std;

/* 创建的新线程 */
void* Start_Routine(void* args) {
    // 将参数转换为有效参数
    const string paramName = static_cast<const char*>(args);

    // 创建的线程执行
    while(true) {
        cout << "new thread create success, name:Start_Routine " << endl;
        sleep(1);
    }
}

/* 程序入口函数 */
int main() {
    // 创建线程
    pthread_t tid;
    pthread_create(&tid, nullptr, Start_Routine, (void*)"Thread One");

    // 主线程执行
    while(true) {
        cout << "new thread create success, name:main thread" << endl;
        sleep(1);
    }
    return 0;
}

// 打印结果:
new thread create success, name:Start_Routine 
new thread create success, name:main thread
new thread create success, name:Start_Routine 
new thread create success, name:main thread
new thread create success, name:Start_Routine 
new thread create success, name:main thread

【8.2.4】一次性创建多个线程

// Makefile 文件》》》》》》》》》》》》》》》》
cc=g++
standard=-std=c++11

myThread:Thread.cc
	$(cc) -o $@ $^ $(standard) -lpthread

.PHONY:clean
clean:
	rm -rf myThread

// Thread.cc文件》》》》》》》》》》》》》》》》
// 监控脚本 while :; do ps -aL | head -1 && ps -aL | grep myThread; sleep 1; done
#include <iostream>
#include <string>
#include <vector>
#include <cstdio>
#include <unistd.h>
#include <pthread.h>
using namespace std;

class ThreadData {
public:
    pthread_t tid;          // 线程的描述符
    char nameBuffer[64];    // 线程的名称
};

/* 抽象的线程函数 */
// start_routine函数现在是被几个线程执行呢? 10 
// 这个函数现在是什么状态? 可重入状态
// 在函数内定义的变量,都叫做局部变量,具有临时性,依旧适用,在多线程状态下,其实每一个线程都有自己独立的栈结构!
void*Start_Routine(void* args) {
    sleep(1);

    // 获取到传递的void*参数
    // 一个线程如何出现了异常,会影响到其他线程吗?(会的,健壮性或者鲁棒性较差)
    ThreadData* td = static_cast<ThreadData*>(args);

    // int cnt = 10; // 这里的操作是为了演示线程具有独立的栈结构!
    while(true) {
        // cout << "cnt:" << &cnt << endl;
        cout << "new thread create success,name:" << td->nameBuffer << " " << td->tid << endl;
        sleep(5);
    }

    delete td;
    return nullptr;
}

/* 入口函数 */
int main() {
    vector<ThreadData*> threads;

#define NUM 10 // 宏定义,默认一次性创建10个进程
    // 一次性创建多个进程
    for(int i = 0; i < NUM; i++) {
        // 在堆中创建线程的单集合
        ThreadData* td = new ThreadData();
        // 封装ThreadData类
        snprintf(td->nameBuffer, sizeof(td->nameBuffer), "%s,%d", "thread", i + 1);
        pthread_create(&td->tid, nullptr , Start_Routine, td);
        // 保留每个线程的信息
        threads.push_back(td);
    }

    // 打印已创建出来的线程清单
    for(auto v : threads) {
        cout << "create thread:" << v->nameBuffer << " " << v->tid << " success!" << endl;
    }

    // 主线程执行
    while(true) {
        cout << "new thread create success,name:main tread!" << endl;
        sleep(5);
    }

    return 0;
}
// 打印结果:
create thread:thread,1 139744236070656 success!
create thread:thread,2 139744227677952 success!
create thread:thread,3 139744219285248 success!
create thread:thread,4 139744210892544 success!
create thread:thread,5 139744202499840 success!
create thread:thread,6 139744194107136 success!
create thread:thread,7 139744185714432 success!
create thread:thread,8 139744177321728 success!
create thread:thread,9 139744168929024 success!
create thread:thread,10 139744160536320 success!
new thread create success,name:main tread!
new thread create success,name:thread,7 140342020540160
new thread create success,name:thread,5 140342037325568
new thread create success,name:thread,2 140342062503680
new thread create success,name:thread,1 140342070896384
new thread create success,name:thread,10 140341995362048
new thread create success,name:thread,6 140342028932864
new thread create success,name:thread,8 140342012147456
new thread create success,name:thread,9 140342003754752
new thread create success,name:thread,3 140342054110976
new thread create success,name:thread,4 140342045718272

【8.2】线程终止

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

  1. 从线程函数return。这种方法对主线程不适用,从main函数return相当于调用exit。

  2. 线程可以调用pthread_ exit终止自己。

  3. 一个线程可以调用pthread_ cancel终止同一进程中的另一个线程。

【pthread_exit函数】 |【return nullptr】

// 功能:线程终止
// 原型
void pthread_exit(void *value_ptr);
// 参数
// value_ptr:value_ptr不要指向一个局部变量。
// 返回值:无返回值,跟进程一样,线程结束的时候无法返回到它的调用者(自身)

// 【注意】需要注意,pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是用malloc
// 分配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了。

【测试代码】

// 监控脚本 
// [shaxiang@VM-8-14-centos threads]$ while :; do ps -aL | head -1 && ps -aL | grep myThread; sleep 1; done

#include <iostream>
#include <vector>
#include <string>
#include <cstdio>
#include <cassert>
#include <pthread.h>
#include <unistd.h>
using namespace std;

/* 线程数据类 */
class ThreadData{
public:
    pthread_t tid;
    char nameBuffer[64];
};

// start_routine函数现在是被几个线程执行呢? 10 
// 这个函数现在是什么状态? 可重入状态
// 在函数内定义的变量,都叫做局部变量,具有临时性,依旧适用,在多线程状态下,其实每一个线程都有自己独立的栈结构!
void *start_routine(void *args){
    // 演示打印!
    sleep(1);

    // 一个线程如何出现了异常,会影响到其他线程吗?(会的,健壮性或者鲁棒性较差)
    ThreadData *td = static_cast<ThreadData*>(args);  // 安全的进行强制类型转换。
    // exit(-1);    // exit是不能终止线程的,因为exit是终止进程的,任何一个执行流调用exit都会让整个进程退出。
    // return nullptr;      // ok
    pthread_exit(nullptr);  // oK

    int cnt = 10;
    char tempStr[64] = {'\0'};
    while(cnt){
        snprintf(tempStr, sizeof(tempStr), "new thread create success,name:%s cnt: %d\n", td->nameBuffer, cnt--);
        cout << tempStr;
        sleep(1);
    }

    // 释放空间
    delete td;
    return nullptr; // 线程函数结束,在函数return的时候线程就终止了。
} 

int main(){
    vector<ThreadData *> tids;
#define NUM 10
    // 一次性创建多个进程.
    for(int i = 1; i <= NUM; i++){
        // 这里没创建一个线程都会有一份独立的数据-> td传递的是指针.
        ThreadData* td = new ThreadData();
        snprintf(td->nameBuffer, sizeof(td->nameBuffer), "%s,%d", "thread", i);
        pthread_create(&td->tid, nullptr, start_routine, td);
        // 每个进程进行保存!
        tids.push_back(td);
    }

    // 打印线程清单
    for(auto& e : tids){
        cout << "create thread:" << e->nameBuffer << " " << e->tid << "success" << endl;
    }

    char tempStr[64] = {'\0'};
    while(true){
        snprintf(tempStr, sizeof(tempStr), "new thread create success,name:main tread\n");
        cout << tempStr;
        sleep(1);
    }
    return 0;
}

【8.3】线程取消

// 注意:线程可以被取消,线程要取消,前提是这个线程已经跑起来了!
// 功能:取消一个执行中的线程
// 原型
int pthread_cancel(pthread_t thread);
// 参数
// thread:线程ID
// 返回值:成功返回0;失败返回错误码

【测试代码】

// 监控脚本 
// [shaxiang@VM-8-14-centos threads]$ while :; do ps -aL | head -1 && ps -aL | grep myThread; sleep 1; done

#include <iostream>
#include <vector>
#include <string>
#include <cstdio>
#include <cassert>
#include <pthread.h>
#include <unistd.h>
using namespace std;

/* 线程数据类 */
class ThreadData{
public:
    long long number;
    pthread_t tid;
    char nameBuffer[64];
};

// start_routine函数现在是被几个线程执行呢? 10 
// 这个函数现在是什么状态? 可重入状态
// 在函数内定义的变量,都叫做局部变量,具有临时性,依旧适用,在多线程状态下,其实每一个线程都有自己独立的栈结构!
void *start_routine(void *args){
    // 演示打印!
    sleep(1);

    // 一个线程如何出现了异常,会影响到其他线程吗?(会的,健壮性或者鲁棒性较差)
    ThreadData *td = static_cast<ThreadData*>(args);  // 安全的进行强制类型转换。
    // exit(-1);    // exit是不能终止线程的,因为exit是终止进程的,任何一个执行流调用exit都会让整个进程退出。
    // return nullptr;      // ok
    

    int cnt = 10;
    char tempStr[64] = {'\0'};
    while(cnt){
        snprintf(tempStr, sizeof(tempStr), "new thread create success,name:%s cnt: %d\n", td->nameBuffer, cnt--);
        cout << tempStr;
        sleep(1);
    }

    pthread_exit((void*)td->number);  // oK
    // return nullptr; // 线程函数结束,在函数return的时候线程就终止了。
} 

int main(){
    vector<ThreadData *> tids;
#define NUM 10
    // 一次性创建多个进程.
    for(int i = 1; i <= NUM; i++){
        // 这里没创建一个线程都会有一份独立的数据-> td传递的是指针.
        ThreadData* td = new ThreadData();
        td->number = i;
        snprintf(td->nameBuffer, sizeof(td->nameBuffer), "%s,%d", "thread", i);
        pthread_create(&td->tid, nullptr, start_routine, td);
        // 每个进程进行保存!
        tids.push_back(td);
    }

    // 打印线程清单
    for(auto& e : tids){
        cout << "create thread:" << e->nameBuffer << " " << e->tid << "success" << endl;
    }

    // 等待所有的线程
    // for(auto& e : tids){
    //     int n = pthread_join(e->tid, (void **)&e->number);
    //     assert(n == 0);
    //     cout << "join:" << e->nameBuffer << " success!" << " number: " << (long long)e->number << endl;

    //     // 等待一个,就释放一个线程空概念
    //     delete e;
    // }
    // 取消线程
    // 线程如果是要被取消,退出码为-1(PTHREAD_CANCELED)
    // 5秒后取消一般的线程
    sleep(5);
    for(int i = 0; i<tids.size() / 2; i++){
        pthread_cancel(tids[i]->tid);
        cout << "pthread_cancel: " << tids[i]->number << " success!" << endl;
    }


    char tempStr[64] = {'\0'};
    while(true){
        snprintf(tempStr, sizeof(tempStr), "new thread create success,name:main tread\n");
        cout << tempStr;
        sleep(1);
    }
    return 0;
}

【8.4】线程等待

        线程也是需要等待的,线程为什么需要线程等待?

        线程也是需要等待的,如果不等待,会造成类似僵尸进程的问题 - 内存泄漏!

        已经退出的线程,其空间没有被释放,仍然在进程的地址空间内,创建新的线程不会复用刚才退出线程的地址空间。

// 功能:等待线程结束
// 原型
int pthread_join(pthread_t thread, void **value_ptr);
// 参数
// thread:线程ID
// value_ptr:它指向一个指针,后者指向线程的返回值,用来获取线程函数结束后,的执行见过
// 返回值:成功返回0;失败返回错误码

        调用该函数的线程将挂起等待,直到id为thread的线程终止。thread线程以不同的方法终止,通过pthread_join得到的终止状态是不同的,总结如下:

  • 如果thread线程通过return返回,value_ ptr所指向的单元里存放的是thread线程函数的返回值。

  • 如果thread线程被别的线程调用pthread_ cancel异常终掉,value_ ptr所指向的单元里存放的是常数PTHREAD_ CANCELED。

  • 如果thread线程是自己调用pthread_exit终止的,value_ptr所指向的单元存放的是传给pthread_exit的参数。

  • 如果对thread线程的终止状态不感兴趣,可以传NULL给value_ ptr参数。

// Makefile文件 》》》》》》》》》》》》》》》
myThread:myThread.cpp
    g++ -o $@ $^ -std=c++11 -lpthread

.PHONY:clean
clean:
    rm -f myThread  

// myThread.cpp文件》》》》》》》》》》》》》
// 监控脚本 
// [shaxiang@VM-8-14-centos threads]$ while :; do ps -aL | head -1 && ps -aL | grep myThread; sleep 1; done
#include <iostream>
#include <vector>
#include <string>
#include <cstdio>
#include <cassert>
#include <pthread.h>
#include <unistd.h>
using namespace std;

/* 线程数据类 */
class ThreadData{
public:
    pthread_t tid;
    char nameBuffer[64];
};

// start_routine函数现在是被几个线程执行呢? 10 
// 这个函数现在是什么状态? 可重入状态
// 在函数内定义的变量,都叫做局部变量,具有临时性,依旧适用,在多线程状态下,其实每一个线程都有自己独立的栈结构!
void *start_routine(void *args){
    // 演示打印!
    sleep(1);

    // 一个线程如何出现了异常,会影响到其他线程吗?(会的,健壮性或者鲁棒性较差)
    ThreadData *td = static_cast<ThreadData*>(args);  // 安全的进行强制类型转换。
    // exit(-1);    // exit是不能终止线程的,因为exit是终止进程的,任何一个执行流调用exit都会让整个进程退出。
    // return nullptr;      // ok
    

    int cnt = 10;
    char tempStr[64] = {'\0'};
    while(cnt){
        snprintf(tempStr, sizeof(tempStr), "new thread create success,name:%s cnt: %d\n", td->nameBuffer, cnt--);
        cout << tempStr;
        sleep(1);
    }

    pthread_exit(nullptr);  // oK
    // return nullptr; // 线程函数结束,在函数return的时候线程就终止了。
} 

int main(){
    vector<ThreadData *> tids;
#define NUM 10
    // 一次性创建多个进程.
    for(int i = 1; i <= NUM; i++){
        // 这里没创建一个线程都会有一份独立的数据-> td传递的是指针.
        ThreadData* td = new ThreadData();
        snprintf(td->nameBuffer, sizeof(td->nameBuffer), "%s,%d", "thread", i);
        pthread_create(&td->tid, nullptr, start_routine, td);
        // 每个进程进行保存!
        tids.push_back(td);
    }

    // 打印线程清单
    for(auto& e : tids){
        cout << "create thread:" << e->nameBuffer << " " << e->tid << "success" << endl;
    }

    // 等待所有的线程
    for(auto& e : tids){
        int n = pthread_join(e->tid, nullptr);
        assert(n == 0);
        cout << "join:" << e->nameBuffer << " success!" << endl;

        // 等待一个,就释放一个线程空概念
        delete e;
    }

    char tempStr[64] = {'\0'};
    while(true){
        snprintf(tempStr, sizeof(tempStr), "new thread create success,name:main tread\n");
        cout << tempStr;
        sleep(1);
    }
    return 0;
}

【如何获取线程自定义退出码】

// 监控脚本 
// [shaxiang@VM-8-14-centos threads]$ while :; do ps -aL | head -1 && ps -aL | grep myThread; sleep 1; done
#include <iostream>
#include <vector>
#include <string>
#include <cstdio>
#include <cassert>
#include <pthread.h>
#include <unistd.h>
using namespace std;

/* 线程数据类 */
class ThreadData{
public:
    long long number;
    pthread_t tid;
    char nameBuffer[64];
};

// start_routine函数现在是被几个线程执行呢? 10 
// 这个函数现在是什么状态? 可重入状态
// 在函数内定义的变量,都叫做局部变量,具有临时性,依旧适用,在多线程状态下,其实每一个线程都有自己独立的栈结构!
void *start_routine(void *args){
    // 演示打印!
    sleep(1);

    // 一个线程如何出现了异常,会影响到其他线程吗?(会的,健壮性或者鲁棒性较差)
    ThreadData *td = static_cast<ThreadData*>(args);  // 安全的进行强制类型转换。
    // exit(-1);    // exit是不能终止线程的,因为exit是终止进程的,任何一个执行流调用exit都会让整个进程退出。
    // return nullptr;      // ok
    

    int cnt = 10;
    char tempStr[64] = {'\0'};
    while(cnt){
        snprintf(tempStr, sizeof(tempStr), "new thread create success,name:%s cnt: %d\n", td->nameBuffer, cnt--);
        cout << tempStr;
        sleep(1);
    }

    pthread_exit((void*)td->number);  // oK
    // return nullptr; // 线程函数结束,在函数return的时候线程就终止了。
} 

int main(){
    vector<ThreadData *> tids;
#define NUM 10
    // 一次性创建多个进程.
    for(int i = 1; i <= NUM; i++){
        // 这里没创建一个线程都会有一份独立的数据-> td传递的是指针.
        ThreadData* td = new ThreadData();
        td->number = i;
        snprintf(td->nameBuffer, sizeof(td->nameBuffer), "%s,%d", "thread", i);
        pthread_create(&td->tid, nullptr, start_routine, td);
        // 每个进程进行保存!
        tids.push_back(td);
    }

    // 打印线程清单
    for(auto& e : tids){
        cout << "create thread:" << e->nameBuffer << " " << e->tid << "success" << endl;
    }

    // 等待所有的线程
    for(auto& e : tids){
        int n = pthread_join(e->tid, (void **)&e->number);
        assert(n == 0);
        cout << "join:" << e->nameBuffer << " success!" << " number: " << (long long)e->number << endl;

        // 等待一个,就释放一个线程空概念
        delete e;
    }

    char tempStr[64] = {'\0'};
    while(true){
        snprintf(tempStr, sizeof(tempStr), "new thread create success,name:main tread\n");
        cout << tempStr;
        sleep(1);
    }
    return 0;
}

【问题】为什么没有看到,线程退出的时候,对应的退出信号?

        线程退出,收到信号,整个进程都会退出,pthread_join:默认就认为函数会调用成功,不考虑异常的问题,异常问题就是你进程考虑的问题。

【8.5】分离线程

        默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄漏。

        如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源。

【引入函数】

#include <pthread.h>
// 功能:获取线程id
// 原型
pthread_t pthread_self(void);

【代码实例】

// Makefile文件 》》》》》》》》》》》》》》》》》》》》》》
myThread:myThread.cpp
    g++ -o $@ $^ -std=c++11 -l pthread

.PHONY:clean
clean:
    rm -f myThread

// myThread.cpp文件》》》》》》》》》》》》》》》》》》》
#include <iostream>
#include <cassert>
#include <cstdio>
#include <pthread.h>
#include <unistd.h>
using std::cout;
using std::cin;
using std::endl;

std::string change_id(const pthread_t &thread_id){
    char tid[128];
    snprintf(tid, sizeof(tid), "0x%x", thread_id);
    return tid;
}

// 线程方法调用.
void *start_routine(void *args){
    sleep(1);
    std::string threadName = static_cast<const char*>(args);
    while(true){
        cout << threadName << " running.... : " << change_id(pthread_self()) << endl;
        sleep(1);
    }
}

int main(){
    // 创建线程.
    pthread_t tid;
    int n = pthread_create(&tid, nullptr, start_routine, (void*)"thread One");
    assert(n == 0); (void)n;

    // 打印创建的pthread_t的线程id只
    cout << "main thread id : " << change_id(pthread_self()) << endl;
    cout << "main thread running ... new thread id: " <<  change_id(tid) << endl;

    // 等待线程.
    pthread_join(tid, nullptr);

    return 0;
}

[shaxiang@VM-8-14-centos threads_02]$ ./myThread 
main thread id : 0xaf078740
main thread running ... new thread id: 0xadfe3700
thread One running.... : 0xadfe3700
thread One running.... : 0xadfe3700
thread One running.... : 0xadfe3700
thread One running.... : 0xadfe3700
thread One running.... : 0xadfe3700
thread One running.... : 0xadfe3700
thread One running.... : 0xadfe3700

【可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离】

#include <pthread.h>
// 功能:线程分离
// 原型
int pthread_detach(pthread_t thread);

【代码实例】

#include <iostream>
#include <cassert>
#include <cstdio>
#include <cstring>
#include <pthread.h>
#include <unistd.h>
using std::cout;
using std::cin;
using std::endl;

std::string change_id(const pthread_t &thread_id){
    char tid[128];
    snprintf(tid, sizeof(tid), "0x%x", thread_id);
    return tid;
}

// 线程方法调用.
void *start_routine(void *args){
    sleep(1);
    std::string threadName = static_cast<const char*>(args);

    int cnt = 5;
    while(cnt--){
        cout << threadName << " running.... : " << change_id(pthread_self()) << endl;
        sleep(1);
    }

}

int main(){
    // 创建线程.
    pthread_t tid;
    int n = pthread_create(&tid, nullptr, start_routine, (void*)"thread One");
    assert(n == 0); (void)n;
    // 将线程进行分离
    pthread_detach(tid);

    // 打印创建的pthread_t的线程id只
    cout << "main thread id : " << change_id(pthread_self()) << endl;
    cout << "main thread running ... new thread id: " <<  change_id(tid) << endl;

    // 等待线程 -> 如果线程默认是joinable的,如果设置了分离状态,不能够进行等待了!
    int ret = pthread_join(tid, nullptr);
    cout << "result: " << ret << " : " << strerror(ret) << endl;    // ret失败了!

    return 0;
}

// 打印结果       
[shaxiang@VM-8-14-centos threads_02]$ ./myThread 
main thread id : 0xac09b740
main thread running ... new thread id: 0xab006700
result: 22 : Invalid argument     // 非法参数,因为线程被分离了!

【9】线程ID及进程地址空间布局

        pthread_ create函数会产生一个线程ID,存放在第一个参数指向的地址中。该线程ID和前面说的线程ID不是一回事。

        前面讲的线程ID属于进程调度的范畴。因为线程是轻量级进程,是操作系统调度器的最小单位,所以需要一个数值来唯一表示该线程。

        pthread_ create函数第一个参数指向一个虚拟内存单元,该内存单元的地址即为新创建线程的线程ID,属于NPTL线程库的范畴。线程库的后续操作,就是根据该线程ID来操作线程的。

        线程库NPTL提供了pthread_ self函数,可以获得线程自身的ID:

#include <pthread.h>
// 功能:获取线程id
// 原型
pthread_t pthread_self(void);

        pthread_t 到底是什么类型呢?取决于实现。对于Linux目前实现的NPTL实现而言,pthread_t类型的线程ID,本质就是一个进程地址空间上的一个地址。

【线程局部存储】

【局部存储测试 int g_value = 100】

// Makefile文件 》》》》》》》》》》》》》》》》》》》》》》
cc=g++
standard=-std=c++11

myThread:Thread.cc 
	$(cc) -o $@ $^ $(standard) -l pthread

.PHONY:clean
clean:
	rm -rf myThread
	
// Thread.cc文件》》》》》》》》》》》》》》》》》》》	
#include <iostream>
#include <string>
#include <cstdio>
#include <cassert>
#include <unistd.h>
#include <pthread.h>
using namespace std;

int g_value = 100;

string ChangeId(const pthread_t& tid) {
    char buffer[128];
    snprintf(buffer, sizeof(buffer), "0x%x", tid);
    return buffer;
}

void* StartRoutine(void* args) {
    sleep(2);
    const char* msg = static_cast<const char*>(args);

    while(true) {
        cout << msg << " Thread Id: " << ChangeId(pthread_self())\
            << "g_value: " << g_value << " &g_value: " << &g_value << endl;
        g_value++;
        sleep(1);
    }

    pthread_exit(nullptr);
}

int main() {
    // 创建线程,并且将线程分离
    pthread_t tid;
    int n = pthread_create(&tid, nullptr, StartRoutine, (void*)"One");
    assert(n == 0); (void)n;

    // 打印创建的pthread_t的线程id
    while(true) {
        cout << "Main Thread Id: " << ChangeId(pthread_self())\
            << "g_value: " << g_value << " &g_value: " << &g_value << endl;
        sleep(1);
    }
    
    return 0;
}

【局部存储测试 __threadint g_value = 100】

// Makefile文件 》》》》》》》》》》》》》》》》》》》》》》
cc=g++
standard=-std=c++11

myThread:Thread.cc 
	$(cc) -o $@ $^ $(standard) -l pthread

.PHONY:clean
clean:
	rm -rf myThread
	
// Thread.cc文件》》》》》》》》》》》》》》》》》》》
#include <iostream>
#include <string>
#include <cstdio>
#include <cassert>
#include <unistd.h>
#include <pthread.h>
using namespace std;

__thread int g_value = 100;

string ChangeId(const pthread_t& tid) {
    char buffer[128];
    snprintf(buffer, sizeof(buffer), "0x%x", tid);
    return buffer;
}

void* StartRoutine(void* args) {
    sleep(2);
    const char* msg = static_cast<const char*>(args);

    while(true) {
        cout << msg << " Thread Id: " << ChangeId(pthread_self())\
            << "g_value: " << g_value << " &g_value: " << &g_value << endl;
        g_value++;
        sleep(1);
    }

    pthread_exit(nullptr);
}

int main() {
    // 创建线程,并且将线程分离
    pthread_t tid;
    int n = pthread_create(&tid, nullptr, StartRoutine, (void*)"One");
    assert(n == 0); (void)n;

    // 打印创建的pthread_t的线程id
    while(true) {
        cout << "Main Thread Id: " << ChangeId(pthread_self())\
            << "g_value: " << g_value << " &g_value: " << &g_value << endl;
        sleep(1);
    }
    
    return 0;
}

 

【10】基于原声线程库封装线程类

【Makefile文件】

# 定义替代关系
cc=g++
standard=-std=c++11
​
# 定义myThread可执行依赖于Thread.cc文件
myThread:Thread.cc
    $(cc) -o $@ $^ $(standard) -l pthread
​
# 定义删除可执行命令
.PHONY:clean
clean: 
    rm -rf myThread

【Thread.hpp文件】

#pragma once
#include <iostream>
#include <functional>
​
#include <cstdio>
#include <cassert>
​
/* 连接上下文 */
class Thread;
class ConnectText {
public:
    /* 构造函数 */
    ConnectText() : _this(nullptr), _args(nullptr) {}
    /* 析构函数 */
    ~ConnectText() {} 
public:
    Thread* _this;
    void*   _args;
};
​
/* 线程类 */
class Thread {
public:
    using func_t = std::function<void*(void*)>; // // 从定义类似函数指针类型:返回值是:void*  参数是:void*
​
public: 
    /* 构造函数 */
    Thread(const func_t& func, void* args, const int number) 
        : _func(func), _args(args) 
    {
        // 创建线程的名称
        char buffer[64];
        snprintf(buffer, sizeof(buffer), "Thread-%d", number);
        _tName = buffer;
​
        // 创建线程后,启动
        ConnectText* cnt = new ConnectText();
        cnt->_this = this;
        cnt->_args = _args;
​
        int n = pthread_create(&_tid, nullptr, StartRoutine, (void*)cnt);
        assert(n == 0); (void)n;
        // 编译debug的方式时assert是存在的,release方式assert是不存在的,到时n就是定义了,但是没有被使用的变量。
        // 在有的编译器下会有warning。
    }
​
    /* 析构函数 */
    ~Thread(){}
​
public:
    /* 线程函数 */
    static void* StartRoutine(void* args) { // 在类内创建线程,想让线程执行对应的方法,需要将方法设置称为static
        ConnectText* cnt = static_cast<ConnectText*>(args);
        void* exRet = cnt->_this->Run(cnt->_args);
​
        delete cnt;
        return exRet;
    }
​
public:
    /* 线程等待 */
    void Join() {
        int n = pthread_join(_tid, nullptr);
        assert(n == 0); (void) n;
    }
​
private:
    void* Run(void* args) {
        return _func(args);
    }
​
private:
    pthread_t   _tid;   // 线程id
    std::string _tName; // 线程名称
    func_t      _func;  // 线程函数
    void*       _args;  // 线程参数
};

【Thread.cc文件】

#include <memory>
#include <unistd.h>
#include "Thread.hpp" 
using namespace std;
// 监控命令:while :; do ps -aL | head -1 && ps -aL | grep myThread; sleep 1; done
void* StartRoutine(void* args) {
    const char* msg = static_cast<const char*>(args);
​
    while(true) {
        cout << "我是一个线程,我正在执行任务:" << msg << endl;
        sleep(1);
    }
​
    return nullptr;
}
​
int main() {
    unique_ptr<Thread> thread1(new Thread(StartRoutine, (void*)"下载任务", 1));
    thread1->Join();
    return 0;
}

【打印结果】

[shaxiang@VM-8-14-centos Thread_Packaging]$ ./myThread 
我是一个线程,我正在执行任务:下载任务
我是一个线程,我正在执行任务:下载任务
我是一个线程,我正在执行任务:下载任务
我是一个线程,我正在执行任务:下载任务
我是一个线程,我正在执行任务:下载任务
我是一个线程,我正在执行任务:下载任务
我是一个线程,我正在执行任务:下载任务
我是一个线程,我正在执行任务:下载任务

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

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

相关文章

1054. 距离相等的条形码;823. 带因子的二叉树;1878. 矩阵中最大的三个菱形和

1054. 距离相等的条形码 核心思想&#xff1a;隔一个数填一个&#xff0c;优先填写出现次数多的数。注意点就是条形码的长度为奇数和偶数&#xff0c;但是我们遵循先优先填偶数就不会出错即可。 823. 带因子的二叉树 核心思想&#xff1a;递归。定义dfs(val)表示以值val作为根…

[UE虚幻引擎插件介绍] DTSQLite 插件说明 :蓝图操作SQLite3文件,执行SQL语句。

本插件可以在UE里面使用蓝图操作SQLite3文件&#xff0c;并且执行SQL语句&#xff0c;CREATE&#xff0c;SELECT&#xff0c;DELETE&#xff0c;INSERT&#xff0c;UPDATE。 直接操作数据库&#xff0c;并返回相应结果集&#xff0c;并可以把结果集转换为TArray<TMap<FSt…

Microsoft Excel 101 简介

什么是 Microsoft Excel&#xff1f; Microsoft Excel 是一个电子表格程序&#xff0c;用于记录和分析数值数据。 将电子表格想像成构成表格的列和行的集合。 字母通常分配给列&#xff0c;数字通常分配给行。 列和行相交的点称为像元。 单元格的地址由代表列的字母和代表行的…

华为云征文|华为云云耀云服务器L实例使用教学(一)

目录 国内免费云服务器&#xff08;体验&#xff09; 认识国内免费云服务器 如何开通国内免费云服务器 云耀云服务器 HECS HECS适用于哪些场景&#xff1f; 网站搭建 电商建设 开发测试环境 云端学习环境 为什么选择华为云耀云服务器 HECS 国内免费云服务器&#xff…

LeetCode 40. Combination Sum II【回溯,剪枝】中等

本文属于「征服LeetCode」系列文章之一&#xff0c;这一系列正式开始于2021/08/12。由于LeetCode上部分题目有锁&#xff0c;本系列将至少持续到刷完所有无锁题之日为止&#xff1b;由于LeetCode还在不断地创建新题&#xff0c;本系列的终止日期可能是永远。在这一系列刷题文章…

Loopback for Mac:专业级的音频处理能力

Loopback for Mac是一款功能强大的虚拟音频设备&#xff0c;它能够将应用程序的音频输出路由到其他应用程序的输入&#xff0c;从而实现音频数据的传输和交互。以下是Loopback for Mac的一些主要功能和特色介绍&#xff1a; 创建虚拟音频设备&#xff1a;Loopback可以创建虚拟…

基于Python开发的Excel数据分析系统(源码+可执行程序+程序配置说明书+程序使用说明书)

一、项目简介 本项目是一套基于Python开发的Excel数据分析系统&#xff0c;主要针对计算机相关专业的正在做毕设的学生与需要项目实战练习的Python学习者。 包含&#xff1a;项目源码、项目文档等&#xff0c;该项目附带全部源码可作为毕设使用。 项目都经过严格调试&#xff0…

源码编译risc-v虚拟机和编译器 riscv-gnu-toolchain 和 riscv-tools 在ubuntu 22.04

1. 编译 riscv-gnu-toolchain 1.1 预备环境 $ sudo apt-get install autoconf automake autotools-dev curl python3 libmpc-dev libmpfr-dev libgmp-dev gawk build-essential bison flex texinfo gperf libtool patchutils bc zlib1g-dev libexpat-dev 1.2 下载源代码 http…

Linux tcpdump抓包命令

1.tcpdump抓包命令 -c 指定抓取包的数量&#xff0c;即最后显示的数量 -i 指定tcpdump监听的端口。未指定&#xff0c;选择系统中最小的以配置端口。-i any:监听所有网络端口 -i lo:监听lookback接口。-nn 对监听地址以数字方式呈现&#xff0c;且对端口也以数字方式呈现。…

西门子S7-1200F或1500F系列安全PLC的组态步骤和基础编程(一)

西门子S7-1200F或1500F系列安全PLC的组态步骤和基础编程(一) 第一部分:组态配置 具体步骤可参考以下内容: 如下图所示,新建一个项目后,添加一个安全型PLC,这里以1516F-3 PN/DP为例进行说明, 如下图所示,添加CPU完成后,可以看到左侧的项目树中比普通的PLC多了几个选项…

el-date-picker 封装一个简单的日期组件, 主要是禁用日期

子组件 <template><div><el-date-pickerv-model"dateModel"type"datetimerange":picker-options"pickerOptions"range-separator"至"ref"picker"start-placeholder"开始日期"end-placeholder&quo…

深度学习-消融实验

深度学习中消融实验的目的 深度学习中&#xff0c;消融实验是一种用于理解和评估神经网络模型的技术。它的主要目的是通过逐步删除神经网络的某些组件或功能&#xff0c;来研究它们对模型性能的影响。通过这种方式&#xff0c;我们可以深入了解模型的工作原理、探索模型的鲁棒性…

第31章_瑞萨MCU零基础入门系列教程之WIFI蓝牙模块驱动实验

本教程基于韦东山百问网出的 DShanMCU-RA6M5开发板 进行编写&#xff0c;需要的同学可以在这里获取&#xff1a; https://item.taobao.com/item.htm?id728461040949 配套资料获取&#xff1a;https://renesas-docs.100ask.net 瑞萨MCU零基础入门系列教程汇总&#xff1a; ht…

【毕业设计】基于SSM的仓库进存销系统

前言 &#x1f525;本系统可以选作为毕业设计&#xff0c;运用了现在主流的SSM框架&#xff0c;采用Maven来帮助我们管理依赖&#xff0c;所选结构非常合适大学生所学的技术&#xff0c;本系统结构简单&#xff0c;容易理解&#xff01;本系统功能结构完整&#xff0c;非常高适…

下载HTMLTestRunner并修改

目录 一. 下载HTMLTestRunner 二. 修改HTMLTestRunner 1. 修改内容 2. 修改原因 一. 下载HTMLTestRunner 下载报告模板地址:http://tungwaiyip.info/software/HTMLTestRunner.html 下载模块&#xff1a; 二. 修改HTMLTestRunner 将修改后的模块放到python安装目录下的..…

【Mysql学习笔记】关键字顺序以及执行顺序

关键字顺序&#xff08;如上&#xff09;&#xff1a; SELECT ... FROM ... WHERE ... GROUP BY ... HAVING ... ORDER BY ...关键字执行顺序&#xff08;如上&#xff09;&#xff1a; FROM > WHERE > GROUP BY > HAVING > SELECT的字段 > DISTINCT > ORD…

解决java.io.IOException: Network error

解决java.io.IOException: Network error 解决java.io.IOException: Network error摘要引言正文1. 理解异常的根本原因2. 处理网络连接问题3. 处理连接超时4. 处理协议错误或不匹配5. 异常处理 总结参考资料 博主 默语带您 Go to New World. ✍ 个人主页—— 默语 的博客&#…

Redis从入门到精通(二:数据类型)

数据存储类型介绍 Redis 数据类型&#xff08;5种常用&#xff09; string hash list set sorted_set/zset&#xff08;应用性较低&#xff09; redis 数据存储格式 redis 自身是一个 Map&#xff0c;其中所有的数据都是采用 key : value 的形式存储 数据类型指的是存储的数据…

使用html展示中秋节快乐的脚本

#【中秋征文】程序人生&#xff0c;中秋共享# html代码如下所示&#xff1a; <!DOCTYPE html> <html> <head> <title>中秋节</title> <style> body { font-family: Arial, sans-serif; margin: 0; padding: 20px; } h1 { c…

AutoSAR配置与实践(实践篇)13.6 如何添加一个NVM BLOCK (PIM类型)

AutoSAR配置与实践&#xff08;实践篇&#xff09;13.1 如何添加一个NVM BLOCK 如何添加一个NVM BLOCK &#xff08;PIM类型&#xff09;一、PIM&#xff08;PerInstanceMemory&#xff09;简介二、PIM添加步骤2.1 总体思路2.2 DEV工程步骤2.3 CFG工程步骤 如何添加一个NVM BLO…