Linux系统编程——线程控制

news2024/9/21 20:40:47

目录

一,关于线程控制

二,线程创建

2.1 pthread_create函数

2.2 ps命令查看线程信息

三,线程等待

3.1 pthread_join函数

 3.2 创建多个线程

3.3 pthread_join第二个参数 

四,线程终止

4.1 关于线程终止

4.2 pthread_exit线程退出

4.3 pthread_cancel线程取消

五,线程分离

七,线程ID和进程地址空间布局

7.1 线程ID与LWP

7.2 地址空间共享区中的线程栈

7.3 代码验证几个问题

八,pthread_create第四个参数传类的对象


一,关于线程控制

  1. 在线程概念章节,我们已经说过:Linux内核中没有明确的线程的概念,只有轻量级进程的概念,所以Linux没有给我们提供线程的系统调用,只有轻量级进程的系统调用
  2. 但又因为没有系统接口,用户用起来就比较犯难,所以他也必须给我们提供一套完整的线程接口。于是Linux设计者在用户层实现了一套线程方案,以动静态库的方式提供给用户进行使用 --> pthread线程库(也叫POSIX线程库或者原生线程库)
  3. 原生指的是大部分的Linux系统默认内置该线程库,使用这些库函数需要包含头文件"#include<pthread.h>",同时在编译链接时需要加上"-lpthread"选项
  4. 并且由于原生线程库是第三方库,不属于系统接口,那么pthread函数出错时不会设置errno全局变量,而是直接用返回值来告诉用户执行结果

 下面是常规情况下使用原生线程库的makefile内容:

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

二,线程创建

2.1 pthread_create函数

pthread_create就是的创建线程的函数了,在man手册中函数声明如下:

 解释下四个参数:

  1. 第一个参数是输出型参数,负责返回成功创建的新线程的id,pthread_t也是一个unsigned long int
  2. attr表示设置创建线程的属性,我们目前只要传入nullptr即可,表示不添加其它属性
  3. 第三个参数很明显是一个函数指针,该指针指向一个参数为void*,返回值为void*的函数
  4. 第四个参数arg表示要传入第三个函数指针参数的值,这个arg不仅仅可以传内置类型参数,也可以传C++类的对象,后面有代码

下面是用pthread_create函数创建线程,然后打印pid的代码:

#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;

void show(const string &name)
{
    cout << name << ", pid: " << getpid() << "\n" << endl; // 在一个进程里面获取pid
}

void *threadRun(void *args)
{
    const string name = (char *)args;
    while (true)
    {
        show(name);
        sleep(1);
    }
}

int main()
{
    pthread_t tid;
    char name[64];
    snprintf(name, sizeof name, "new thread");              // 以特定格式化,把内容搞到字符串里去,然后创建进程时把该字符串传过去
    pthread_create(&tid, nullptr, threadRun, (void *)name); // 新线程就跳转过去执行这个函数,主线程就继续向下运行
    while (true)
    {
        cout << "I am main thread" << ", pid: " << getpid() << endl;
        sleep(1);
    }
}

 打印结果如下:

 

可以发现,两个执行流打印的进程pid一样,可以证明确实创建了线程 

2.2 ps命令查看线程信息

先让上面的进程挂着,可以另起一个窗口,先查看进程的

ps -ajx | head -1 && ps ajx | grep mythread

发现只有一条,正常,因为我们创建的是线程不是进程,要查看线程信息需要使用ps -aL 

ps -aL | head -1 && ps -aL | grep mythread

 其中PID就是进程的PID,LWP(Light Weight Process)就是线程的ID了,其中最小的就是主线程,主线程的LWP值和PID一样,LWP也存在task_struct里。

所以CPU调度时本质是看的LWP,由于我们前面说的是看的PID,那其实是一个进程只有一个线程的这种特殊情况,所以在CPU看来,LWP和PID其实是一样滴

三,线程等待

3.1 pthread_join函数

线程和进程一样,线程也是需要进行等待的,如果主线程不等待新线程,就会引起类似僵尸进程得问题,导致内存泄漏。线程等待得函数为pthread_join,下面是函数声明

 参数解释:

  1. 第一个参数thread表示要等待得线程得ID
  2. 第二个参数retval表示线程退出时得退出码信息

 3.2 创建多个线程

和进程一样,我么也可以创建多个线程并进行等待,如下代码:

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

void show(const string &name)
{
    cout << name << ", pid: " << getpid() << endl; // 在一个进程里面获取pid
}

void *threadRun(void *args)
{
    const string name = (char *)args;
    for (int i = 0; i < 5; i++)
    {
        show(name);
        sleep(1);
    }
    return nullptr;
}

int main()
{
    pthread_t tid[5]; // 创建5个线程,这个pthread_t类型也是一个unsigned long int
    char name[64];
    for (int i = 1; i <= 5; i++)
    {
        snprintf(name, sizeof name, "%s-%d", "thread", i);         // 以特定格式化,把内容搞到字符串里去,然后创建进程时把该字符串传过去
        pthread_create(tid + i, nullptr, threadRun, (void *)name); // 新线程就跳转过去执行这个函数,主线程就继续向下运行
    }
    for (int i = 1; i <= 5; i++)
    {
        int a = pthread_join(tid[i], nullptr);
        if (a == 0)
        {
            cout << "thread-" << i << " quit" << endl;
        }
        else
        {
            cout << "thread-" << i << " quit error: " << a << endl;
        }
    }
}

上面代码的逻辑大致是:一次性创建5个线程,每个线程各自打印部分内容,主线程等待,5秒过后,5个线程全部退出,和进程等待一样 

3.3 pthread_join第二个参数 

前面提到过:①原生线程库函数是通过返回值来告诉用户退出结果的    ②pthread_join的第二个参数可以获取线程退出码

但其实,第二个参数说明白点其实是获取线程退出信息,“获取退出码”其实是代表退出信号恰好是数字,所以我们可以用第二个参数获取其它信息,如下代码:

#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;
// 1,线程谁先运行与调度器有关
// 2,线程一旦异常, 就可能导致整个进程崩溃:所以,所有线程共用一个寄存器的标记位
// 3,线程的输入值和返回值问题
// 4,线程异常退出的理解

void *threadRoutine(void *args)
{
    int *data = new int[10];
    for (int i = 0; i < 10; i++)
    {
        cout << "新线程:" << (char *)args << " running " << i << endl;
        sleep(1);
        data[i] = i;
        // int a = 100;
        // a /= 0;
        // 线程出现除0异常,进程也随之终止
    }
    // exit(10); exit是进程退出,所以不要在线程里轻易调用exit
    //  return (void *)10; // 这个返回值一般返回给主线程,返回给pthread_join的第二个参数,直接保存在ret里
    return (void *)data; // data保存i的值,返回给主线程
}

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

    int *ret = nullptr;               // 这里的ret属于指针变量。可以存值
    pthread_join(tid, (void **)&ret); // 默认是阻塞等待新线程退出,不关心线程异常退出,所以线程的健壮性会差一些
    // cout << "main thread wait done, main quit, " << (long long)ret << endl;    //64位下指针是8字节,指针是四字节,所以用long long
    for (int i = 0; i < 10; i++) // 打印线程返回给主线程的执行结果
    {
        cout << ret[i] << " ";
    }
    cout << endl;
    return 0;
}

上面代码大致逻辑是: 创建一个线程,然后在线程里new一个数组,然后循环打印,并且在每次循环就往数组里写一个数组,最后线程执行完后,把数组的指针返回给主线的pthread_join的第二个参数,然后打印这个数组,结果如下:

所以我们可以感觉到这些传来传去的值,其实自由度很高,而这都是void*这个特殊类型的优势所在,后面会专门讲下void*相比其它类型指针的优势

四,线程终止

4.1 关于线程终止

  1. 线程执行进程代码的一部分,换到代码上就是执行一个函数,而一个函数常见的退出方式有三种:return,exit,信号退出。
  2. return是线程的正常退出,对其它线程没有影响,但是如果是exit,那么就是进程退出,会把所有的线程全部给退出掉,而且发生异常例如除0错误,是信号退出,信号退出也是进程退出
  3. 所以主线的pthread_join等待线程函数不关心线程的异常退出信息,因为线程如果出异常,那么全没了,再等待也没有意义了;而且主线程return时,其它线程也会退出,因为主线程一旦退出,其它线程赖以生存的地址空间资源也就全释放了,所以也会退出
  4. 所以要想线程退出并且不影响其它线程,有三种方法:return,pthread_exit和pthread_cancel,就是我们接下来要讲的

4.2 pthread_exit线程退出

使用起来很简单,功能就是终止当前,但是由于线程结时无法返回退出信息给它的调用者(因为是干掉自身,所以返回值无意义),所以pthread_exit和return返回的指针必须是全局的或者是malloc从堆上申请的,不能返回线程的独立栈上的数据,因为线程退出也会销毁栈

如下代码:

#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;

void *threadRun(void *args)
{
    cout << "I am new thread, my quit number is: 666" << endl;
    pthread_exit((void *)666);
}

int main()
{
    pthread_t tid;
    void *ret = nullptr;
    pthread_create(&tid, nullptr, threadRun, nullptr);
    pthread_join(tid, &ret);
    cout << (long long)ret << endl;
}

代码的逻辑很简单,线程函数直接调用pthread_exit退出,然后将666作为返回值返回,然后主线程的pthread_join捕捉到,就拿到了线程的返回值“666”

4.3 pthread_cancel线程取消

上面的pthread_exit是线程自己退出自己,其实使用起来比较别扭,而且容易出错,所以我们可以用pthread_cancel线程取消函数,这个函数可以指定线程ID取消,相比上面线程退出,线程取消灵活性更高,我们可以直接在主线程像类似“远程操控”一样控制线程退出

如下代码:

#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;

void *threadRoutine(void *args)
{
    int i = 0;
    // int *data = new int[10];
    cout << "新线程:" << (char *)args << " running " << endl;
    while (true)
    {
        sleep(1);
    }
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, threadRoutine, (void *)"thread 1");
    for (int i = 5; i > 0; i--)
    {
        cout << "线程取消倒计时: " << i << endl;
        sleep(1);
    }
    pthread_cancel(tid); // 直接取消线程
    int *ret = nullptr;
    pthread_join(tid, (void **)&ret); // 线程被取消,join的时候,退出码是-1 --> #define PTHREAD_CANCELED ((void *)-1)
    cout << "新线程退出,退出码为:  " << (long long)ret << endl;
}

五,线程分离

  1. 其实线程分离的概念和进程分离的概念是高度重合的,都是“如果不关心线程退出情况”,就不再需要pthread_join等待了,因为在不关心线程退出的情况下,join反而是一种负担,会降低整体效率,毕竟是阻塞式等待的
  2. 分离线程后,该线程依旧要使用进程的资源,崩溃时也要退出,只是该线程不再需要join等待了,线程分离后也不会造成“僵尸线程”,系统会自动回收该线程的PAB资源
  3. 分离和join是冲突的,只能存在一个

 

 下面是线程分离的样例代码:

#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;

void *threadRoutine(void *args)
{
    pthread_detach(pthread_self()); // pthread_self()返回自己线程的id
    cout << "新线程:" << (char *)args << " running " << endl;
    sleep(3);
    cout << "新线程退出" << endl;
    pthread_exit((void *)666);
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, threadRoutine, (void *)"thread 1");
    sleep(5);
    cout << "主线程退出" << endl;
}

七,线程ID和进程地址空间布局

7.1 线程ID与LWP

先阐明一个事实:线程ID和LWP没关系

下面是创建一个线程,然后获取线程ID,再以16进制打印出来,如下代码:

#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;

std::string toHex(pthread_t tid) // 把tid转16进制
{
    char hex[64];
    snprintf(hex, sizeof(hex), "%p", tid);
    return hex;
}

void *threadRoutine(void *args)
{
    while (true)
    {
        cout << "thread id: " << toHex(pthread_self()) << endl;
        sleep(1);
    }
}

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

 

  • pthread_create函数会产生一个线程ID,存放在第一个参数指向的空间中
  • 这个ID与LWP是两回事,LWP属于进程调度的范畴,因为线程是操作系统调度的最基本单位,所以需要一个数值来唯一表示,类似线程PID
  • pthread_create函数的第一个参数获取的数值与pthread_self()函数返回的数值是一样的

7.2 地址空间共享区中的线程栈

谈论这个要结合上面提到的线程ID

  1. 因为我们用的不是Linux自带的创建线程的接口,我们用的是pthread库中的接口,所以线程的概念是库给我们维护的,而且线程库不用维护线程的执行流,我们用的原生线程库,也是要加载到内存里的,加载到地址空间里
  2. 库的加载,默认是动态链接的,先把磁盘上的pthread库加载到内存里,然后通过页表映射到共享区,当我想调用库,就直接从代码区跳转到共享区,(如果想访问系统接口,就直接跳转到内核区然后通过内核级页表找到内核代码)
  3. 由于OS没有具体的关于线程的概念,所以关于线程的各种属性要由库来管理,而要管理,就要“先描述,再组织”,所以库就在共享区中为每一个线程都创建了对应的用户层线程的数据集合,这个集合里就包含了线程栈,而由于地址空间是线性的,所以为了让每个线程都能快速找到自己的属性集合,就把每个描述线程的结构体的的起始地址充当线程id了
  4. 主线程就用的是内核级栈结构,新线程用的就是共享区内部提供的私有栈结构
  5. 所以,本质上我们说的进程ID,其实就是一个虚拟地址,每一个进程的虚拟地址都是不同的,因此可以用它来区分每一个线程

问题:如何保证栈区是每一个线程独占的呢?
解答:用户要线程,但是OS只提供轻量级进程,所以在中间加了个线程库作为中间软件层,一样的,线程也要被管理起来,所以这个管理OS承担一部分,库承担一部分,OS承担的主要是对轻量级进程的调度和它的内核数据结构的管理,库主要是要为用户提供线程相关的属性字段。(线程也需要又自己私有的一部分属性,而这部分属性有可能无法和进程一样完整地表示线程,所以这部分工作就由库来完成。)

7.3 代码验证几个问题

通过下面的代码可以验证两个线程的周边结论:

①验证每个线程都有独立的栈空间

②主线程能访问每一个线程栈里的数据

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

#define NUM 3
int *p = NULL;

struct threadData
{
    string threadname;
};

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

void InitThreadData(threadData *td, int number)
{
    td->threadname = "thread-" + to_string(number);
}

void *threadRoutine(void *args)
{

    int test_i = 0;
    threadData *td = static_cast<threadData *>(args);
    if (td->threadname == "thread-2")
        p = &test_i; // 验证主线程能访问其它线程的栈数据
    int i = 0;
    while (i < 10)
    {
        cout << "pid: " << getpid() << ", tid: " << toHex(pthread_self()) << ", threadname: " << td->threadname
             << ", test_t: " << test_i << ", &test_i: " << &test_i << endl;
        sleep(1);
        i++;
        test_i++;
    }

    delete td;
    return nullptr;
}

int main()
{
    vector<pthread_t> tids;
    for (int i = 0; i < NUM; i++)
    {
        pthread_t tid;
        threadData *td = new threadData;
        InitThreadData(td, i);

        pthread_create(&tid, nullptr, threadRoutine, td);
        tids.push_back(tid);
    }
    sleep(1);
    cout << "main thread get a thread local value, val: " << *p << ", &p: " << p << endl;
    for (int i = 0; i < tids.size(); i++)
    {
        pthread_join(tids[i], nullptr);
    }

    return 0;
}

 打印结果乱序很正常,比较线程本来就是并发执行的,从上面的信息我们看到下面几点:

  1. 每个线程打印的pid是一样的,每个线程的tid也就是地址不同,表示每个线程都有独立的栈结构
  2. 主线程定义全局指针p,p能访问2号线程的test_i值,说明主线程能访问每个线程的栈数据
  3. 每个线程都对test_i++,打印的值一样,再次证明线程有独立的栈结构
  4. 在线程当中,线程有独立的栈结构,但是没有私有的栈结构,其它线程仍然能访问,但是以后编写代码时,非常不建议这样搞

八,pthread_create第四个参数传类的对象

pthread_create第四个参数不仅可以传整数或字符串,也可以传类的对象,这就是void*的好处,如下代码:

#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;

class Request
{
public:
    Request(int start, int end, const string &threadname)
        : _start(start), _end(end), _threadname(threadname)
    {
    }

public:
    int _start;
    int _end;
    string _threadname;
};

class Response
{
public:
    Response(int result, int exitcode)
        : _result(result), _exitcode(exitcode)
    {
    }

public:
    int _result;
    int _exitcode;
};

void *sumCount(void *args)
{
    Request *rq = static_cast<Request *>(args); // 类似于Request *eq = (Request*)args
    int sum = 0;
    for (int i = rq->_start; i <= rq->_end; i++) // 求start到end的数的和
    {
        cout << rq->_threadname << " is runing... " << i << endl;
        sum += i;
        usleep(60000);
    }
    Response *rsq = new Response(sum, 0);
    delete rq;
    return rsq;
}

int main()
{
    pthread_t tid;
    Request *rq = new Request(1, 100, "thread 1:");
    pthread_create(&tid, nullptr, sumCount, rq); // 这个参数不仅仅只能传整数或字符串,我还可以传类的对象

    void *ret;
    pthread_join(tid, &ret);
    Response *rsp = static_cast<Response *>(ret);
    cout << "rsp->result: " << rsp->_result << endl;
    delete rsp;
    return 0;
}


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

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

相关文章

为什么KV Cache只需缓存K矩阵和V矩阵,无需缓存Q矩阵?

大家都知道大模型是通过语言序列预测下一个词的概率。假定{ x 1 x_1 x1​&#xff0c; x 2 x_2 x2​&#xff0c; x 3 x_3 x3​&#xff0c;…&#xff0c; x n − 1 x_{n-1} xn−1​}为已知序列&#xff0c;其中 x 1 x_1 x1​&#xff0c; x 2 x_2 x2​&#xff0c; x 3 x_3 x…

MathType7.4.6.8最新免费下载,数学表达神器来袭!

大家好啊&#xff0c;我是爱分享的小能手&#xff01;今天要给大家安利一款神奇的工具——MathType7.4免费版本。这不仅仅是一个简单的数学公式编辑器&#xff0c;而是你学术写作和数学研究的强大助手&#xff0c;简直是数学爱好者的“瑞士军刀”&#xff01; MathType最新mac…

前端面试题17(js快速检索方法详解)

在前端JavaScript中&#xff0c;快速检索数据通常涉及到数组或对象的搜索。这里我会介绍几种常见的快速检索方法&#xff0c;并提供相应的代码示例。 1. 数组的find和findIndex方法 find: 返回数组中满足条件的第一个元素的值。findIndex: 返回数组中满足条件的第一个元素的索…

ASAN排查程序中内存问题使用总结

简介 谷歌有一系列Sanitizer工具&#xff0c;可用于排查程序中内存相关的问题。常用的Sanitizer工具包括&#xff1a; Address Sanitizer&#xff08;ASan&#xff09;&#xff1a;用于检测内存使用错误。Leak Sanitizer&#xff08;LSan&#xff09;&#xff1a;用于检测内存…

2-28 基于matlab提取出频域和时域信号的29个特征

基于matlab提取出频域和时域信号的29个特征&#xff0c;主运行文件feature_extraction&#xff0c;fre_statistical_compute和time_statistical_compute分别提取频域和时域的特征&#xff0c;生成的29个特征保存在生成的feature矩阵中。程序已调通&#xff0c;可直接运行。 2-2…

mp4视频太大怎么压缩不影响画质,mp4文件太大怎么变小且清晰度高

在数字化时代&#xff0c;我们常常面临视频文件过大的问题。尤其是mp4格式的视频&#xff0c;文件大小往往令人望而却步。那么&#xff0c;如何在不影响画质的前提下&#xff0c;有效地压缩mp4视频呢&#xff1f;本文将为您揭秘几种简单实用的压缩技巧。 在分享和存储视频时&am…

入门 Vue Router

Vue Router Vue Router插件做了什么&#xff1f; 全局注册 RouterView 和 RouterLink 组件。添加全局 $router 和 $route 属性。启用 useRouter() 和 useRoute() 组合式函数。触发路由器解析初始路由。 标签介绍 RouterView 加载指定页面 <RouterLink to"/home"…

OpenAI Gym Atari on Windows

题意&#xff1a;在Windows系统上使用OpenAI Gym的Atari环境 问题背景&#xff1a; Im having issues installing OpenAI Gym Atari environment on Windows 10. I have successfully installed and used OpenAI Gym already on the same system. It keeps tripping up when t…

Java 静态变量、静态代码块、普通代码块、构造方法的执行顺序

今天碰到这个问题&#xff0c;看了课程以及资料&#xff0c;做出解答。这是我自己绘制的图&#xff0c;按从上到下&#xff0c;从左到右的顺序执行。如有问题请联系我修正。 要点&#xff1a; 1、执行顺序分为两步&#xff0c;类加载和初始化阶段。 2、因为静态变量和静态代码块…

如何使用Selenium处理Cookie,今天彻底学会了!_selenium获取cookies(1)

02、session介绍 session是另一种记录客户状态的机制&#xff0c;不同的是cookie保存在客户端浏览器中&#xff0c;而session保存在服务器上。客户端浏览器访问服务器的时候&#xff0c;服务器把客户端信息以某种形式记录在服务器上。存储在服务器的数据会更加的安全&#xff…

常见的认证方式

认证机制是一种用户确定用户身份或者权限的安全措施&#xff0c;比如用来验证某个用户是否有权限访问一个资源&#xff0c;如果认证通过&#xff0c;用户就可以成功访问&#xff0c;反之则会访问失败 常见的认证方式有四种&#xff0c;分别是 Basic、Digest、OAuth 和 Bearer …

【分布式系统】Filebeat+Kafka+ELK 的服务部署

目录 一.实验准备 二.配置部署 Filebeat 三.配置Logstash 四.验证 一.实验准备 结合之前的博客中的实验 主机名ip地址主要软件es01192.168.80.101ElasticSearches02192.168.80.102ElasticSearches03192.168.80.103ElasticSearch、Kibananginx01192.168.80.104nginx、Logs…

影视灯方案 H5228升降压恒流芯片 48V60V80V100V共阳0.01%深度调光无频闪

H5228 升降压恒流芯片是一款为 LED 照明应用设计的驱动解决方案&#xff0c;具有宽电压输入范围、高精度的恒流控制和多样的调光功能&#xff0c;适用于智能照明、摄影灯照明以及补光灯照明等领域。以下是对其特点和应用的详细分析&#xff1a; 产品特点 拓扑结构支持&#xf…

【2024华为HCIP831 | 高级网络工程师之路】刷题日记(BGP)

个人名片:🪪 🐼作者简介:一名大三在校生,喜欢AI编程🎋 🐻‍❄️个人主页🥇:落798. 🐼个人WeChat:hmmwx53 🕊️系列专栏:🖼️ 零基础学Java——小白入门必备🔥重识C语言——复习回顾🔥计算机网络体系———深度详讲HCIP数通工程师-刷题与实战🔥🔥

深度解析移动硬盘“函数不正确”错误及高效恢复策略

在数据密集型的社会中&#xff0c;移动硬盘作为移动存储的重要载体&#xff0c;承载着无数用户的个人信息、工作资料及珍贵回忆。然而&#xff0c;当遭遇“函数不正确”的错误时&#xff0c;这些宝贵的数据仿佛被一层无形的屏障所阻隔&#xff0c;让人束手无策。本文将深入探讨…

初学SpringMVC之执行原理

Spring MVC 是基于 Java 实现 MVC 的轻量级 Web 框架 导入 jar 包 pom.xml 文件导入依赖&#xff1a; <dependency><groupId>junit</groupId><artifactId>junit</artifactId><version>4.13.2</version></dependency><dep…

基于全志Orangepi Zero 2 的语音刷抖音项目

目录 一、硬件连接 二、项目实现功能 三、SU-03T语音模块的配置和烧录 3.1 创建产品&#xff1a; 3.2 设置PIN引脚为串口模式&#xff1a; 3.3 设置唤醒词&#xff1a; 3.4 设置指令语句&#xff1a; 3.5 设置控制详情&#xff1a; 3.6 发音人配置&#xff1a; 3.7 其…

第二证券股市知识:股票填权是怎么回事?利好还是利空?

1、股票填权的含义 股票填权是指在除权除息之后的一段时刻内&#xff0c;假设多数投资者看好该个股&#xff0c;股票的价格超过除权除息的基准价就叫做填权。上市公司假设能持续分红&#xff0c;就会向市场传递积极信号&#xff0c;招引更多投资者买入&#xff0c;越来越多的投…

我“硬刚”mmkv开源库对于版本号的定义赢啦!

我“硬刚”mmkv开源库胜利啦&#xff01; 前情是这个帖子https://blog.csdn.net/jzlhll123/article/details/139917169 之前项目中将mmkv1.3.4升级到1.3.5或者1.3.6&#xff0c;就从firebase后台上看到crash。 java.lang.UnsatisfiedLinkError: dlopen failed: library “libmm…

数据融合工具(3)国家基本比例尺地形图分幅计算

情景再现&#xff0c;呼叫小编 数据获取和使用过程中&#xff0c;经常听到一个名词“分幅图幅号”…… 你的数据是按多大比例尺分幅的&#xff1f;我不知道&#xff0c;就一些字母和数值。 你把G47E018018范围内的数据裁剪提供&#xff0c;这个范围是啥&#xff1f; 你把镶嵌…