Linux线程控制

目录

一、线程的简单控制

1.多线程并行

2.线程结束

3.线程等待

(1)系统调用

(2)返回值

4.线程取消

5.线程分离

二、C++多线程小组件

三、线程库TCB

1.tid

2.局部储存


一、线程的简单控制

1.多线程并行

我们之前学过pthread_create可以创建线程,而且要使用原生线程库必须在编译时加上-lpthread。

我们用这个接口一次性创建五个进程。

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

#define NUM 5

void* start_routine(void* args)
{
    string s;
    s += "running->";
    s += (char*)args;
    while(1)
    {
        cout << "running->" << s;
        sleep(1);
    }
}

int main()
{
    for(int i = 0; i<NUM; ++i)
    {
        pthread_t tid;
        char buffer[64];
        snprintf(buffer, sizeof(buffer), "new thread:%d\n",i+1);
        pthread_create(&tid, nullptr, start_routine, (void*)buffer);
    }

    while(1)
    {
        cout << "main pthread running.\n";
        sleep(1);
    }
    return 0;
}

我们运行三次程序:

我们可能会想,按照代码的逻辑,我们想要看到的结果是:

running->new thread:1

running->new thread:2

running->new thread:3

running->new thread:4

running->new thread:5

main pthread running.

可是为什么运行两次,main pthread running.打印的位置不断变化,而且后面的数字都是5。

首先解决第一个问题,main pthread running.打印的位置为什么不断变化?

这是因为线程的运行顺序是由调度器决定的,各个线程的执行进度不同,主线程不一定在最后才能打印main pthread running.

第二个问题,为什么后面的数字都是5?

这是因为,三次执行中主线程执行顺序靠前。主线程将5次创建线程的代码跑完了,每循环一次sprintf就会将上一次的buffer内容覆盖掉,循环5次,i+1变为5。此时,又因为包括主线程的6个线程共享同一个地址空间,所以它们同时能看到buffer。五个新线程通过参数传递的地址找到buffer并打印出来。

既然你说buffer作为缓冲区被覆盖掉了,那我们为每一个线程构建一个自己的缓冲区不就解决问题了?

这样的思想是没有问题的,我们可以使用一个pthread_data类管理线程,内部包含线程标识符tid和缓冲区buffer。

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

#define NUM 5

class pthread_data
{
public:
    pthread_t tid;
    char buffer[64];
};

void* start_routine(void* args)
{
    pthread_data* p = (pthread_data*)args;
    string s;
    s += "running->";
    s += p->buffer;
    while(1)
    {
        cout << s;
        sleep(1);
    }
}

int main()
{
    for(int i = 0; i<NUM; ++i)
    {
        pthread_data* pd = new  pthread_data;
        snprintf(pd->buffer, sizeof(pd->buffer), "new thread:%d\n",i+1);
        pthread_create(&(pd->tid), nullptr, start_routine, (void*)pd);
    }

    while(1)
    {
        cout << "main pthread running.\n";
        sleep(1);
    }
    return 0;
}

虽然确实1到5都出现了,但是因为我们控制不了线程运行的顺序,所以还是不能保证按数字顺序打印。

2.线程结束

线程执行的函数有一个void*返回值,我们返回空指针就能终止该线程。

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

#define NUM 5

class pthread_data
{
public:
    pthread_t tid;
    char buffer[64];
};

void* start_routine(void* args)
{
    pthread_data* p = (pthread_data*)args;
    string s;
    s += "running->";
    s += p->buffer;
    int j = 1;
    while(1)
    {
        cout << s;
        sleep(1);
        if(j++ == 5)
            return nullptr;
    }
}

int main()
{
    for(int i = 0; i<NUM; ++i)
    {
        pthread_data* pd = new  pthread_data;
        snprintf(pd->buffer, sizeof(pd->buffer), "new thread:%d\n",i+1);
        pthread_create(&(pd->tid), nullptr, start_routine, (void*)(pd->buffer));
    }

    while(1)
    {
        cout << "main pthread running.\n";
        sleep(1);
    }
    return 0;
}

五个线程都能正常退出,最后只剩下主线程运行。

POSIX线程库也提供了一个接口用于结束线程

void pthread_exit(void* retval);

头文件:pthread.h

功能:终止当前线程。

参数:void* retval是线程的结束信息,设置为空指针就可以了。

void* start_routine(void* args)
{
    pthread_data* p = (pthread_data*)args;
    string s;
    s += "running->";
    s += p->buffer;
    int j = 1;
    while(1)
    {
        cout << s;
        sleep(1);
        if(j++ == 5)
            pthread_exit(nullptr);
    }
}

将return nullptr替换成该函数也能实现线程退出。

3.线程等待

(1)系统调用

和进程一样,线程在执行完毕时如果task_struct结构体不回收,就会导致内存泄漏(类似未被回收的僵尸进程)。所以我们需要使用pthread_join函数将线程加入等待队列,加入等待队列的线程会被回收,但是回收的现象我们是看不到的。

int pthread_join(pthread_t thread, void** retval);

头文件:pthread.h

功能:将标识符为tid的线程加入等待队列。

参数:pthread_t thread是需要等待的线程标识符,void** retval是线程结束返回的信息,是一个输出型参数。

返回值:等待成功返回0,等待失败返回错误码。

实际上,这个加入等待队列和我们之前的进程等待现象差不多,我们写一段代码,让五个新线程运行3秒终止,主线程负责回收五个新线程,观察一下结果。

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

#define NUM 5

class pthread_data
{
public:
    pthread_t tid;
    char buffer[64];
};

void* start_routine(void* args)
{
    pthread_data* p = (pthread_data*)args;
    string s;
    s += "running->";
    s += p->buffer;
    int j = 1;
    while(1)
    {
        cout << s;
        sleep(1);
        if(j++ == 3)
            return nullptr;
    }
}

int main()
{
    vector<pthread_data*> vpd;
    for(int i = 0; i<NUM; ++i)
    {
        pthread_data* pd = new  pthread_data;
        snprintf(pd->buffer, sizeof(pd->buffer), "new thread:%d\n",i+1);
        pthread_create(&(pd->tid), nullptr, start_routine, (void*)pd);
        vpd.push_back(pd);
    }

    for(auto& e : vpd)
    {
        pthread_join(e->tid, nullptr);
        printf("wait success:%d\n", e->tid);
    }

    return 0;
}

运行结果:

主线程在执行至线程等待代码时,主线程并不会继续往下执行。这也证明了,线程等待是阻塞式等待。

(2)返回值

在之前我们写的start_routine线程函数有一个void*的返回值,它可以返回线程退出相关的信息。

比如说,下图的最后一行就可以以void*的格式返回1。

不过这里有个问题,它虽然能返回结束的信息,但是这个变量要怎么让主线程接收到呢?

pthread_join函数有一个输出型参数void** retval,我们在主线程内定义一个void*类型的ret指针变量。当一个线程被回收的时候,将ret传参,它的返回值就会被放进这个ret里。

又因为返回值类型为void*,如果只将ret传参,那么只会将ret的临时拷贝改变。所以参数必须为void**,这也是使用二级指针的原因。

我让每一个线程都返回1,运行代码。

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

#define NUM 5

class pthread_data
{
public:
    pthread_t tid;
    char buffer[64];
};

void* start_routine(void* args)
{
    pthread_data* p = (pthread_data*)args;
    string s;
    s += "running->";
    s += p->buffer;
    int j = 1;
    while(1)
    {
        cout << s;
        sleep(1);
        if(j++ == 3)
            break;
    }
    return (void*)1;
}

int main()
{
    vector<pthread_data*> vpd;
    for(int i = 0; i<NUM; ++i)
    {
        pthread_data* pd = new  pthread_data;
        snprintf(pd->buffer, sizeof(pd->buffer), "new thread:%d\n",i+1);
        pthread_create(&(pd->tid), nullptr, start_routine, (void*)pd);
        vpd.push_back(pd);
    }

    for(auto& e : vpd)
    {
        void* ret = nullptr;
        pthread_join(e->tid, &ret);
        printf("wait success:%d,exit code:%d\n", e->tid, (long long)ret);
    }

    return 0;
}

运行结果:

把return换成只有一个参数void* retval的pthread_exit,它也可以将结果输出到变量中。

结果与上面的一致。

如果想要让每一个线程都返回各自的错误码,我们可以在pthread_data类中增加一个储存返回值的变量。

我下面就修改代码让每一个线程返回自己的编号。

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

#define NUM 5

class pthread_data
{
public:
    pthread_t tid;
    char buffer[64];
    int num;
};

void* start_routine(void* args)
{
    pthread_data* p = (pthread_data*)args;
    string s;
    s += "running->";
    s += p->buffer;
    int j = 1;
    while(1)
    {
        cout << s;
        sleep(1);
        if(j++ == 3)
            break;
    }
    //return (void*)1;
    pthread_exit((void*)(p->num));
}

int main()
{
    vector<pthread_data*> vpd;
    for(int i = 0; i<NUM; ++i)
    {
        pthread_data* pd = new pthread_data;
        snprintf(pd->buffer, sizeof(pd->buffer), "new thread:%d\n",i+1);
        pd->num = i+1;
        pthread_create(&(pd->tid), nullptr, start_routine, (void*)pd);
        vpd.push_back(pd);
    }

    for(auto& e : vpd)
    {
        void* ret = nullptr;
        pthread_join(e->tid, &ret);
        printf("wait success:%d,exit code:%d\n", e->tid, (long long)ret);
    }

    return 0;
}

运行结果:

4.线程取消

线程取消也是终止线程的一种方式,可使用下面的系统调用。

int pthread_cancel(pthread_t thread);

头文件:pthread.h

功能:取消标识符为thread的线程。

参数:pthread_t thread是需要取消的线程标识符。

返回值:取消成功返回0,取消失败返回错误码。

我们创建10个线程,在中途取消前五个线程,查看具体的现象。

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

#define NUM 10

class pthread_data
{
public:
    pthread_t tid;
    char buffer[64];
    int num;
};

void* start_routine(void* args)
{
    pthread_data* p = (pthread_data*)args;
    int j = 1;
    while(1)
    {
        sleep(1);
        if(j++ == 3)
            break;
    }
    //return (void*)1;
    pthread_exit((void*)(p->num));
}

int main()
{
    vector<pthread_data*> vpd;
    for(int i = 0; i<NUM; ++i)
    {
        pthread_data* pd = new pthread_data;
        snprintf(pd->buffer, sizeof(pd->buffer), "new thread:%d\n",i+1);
        pd->num = i+1;
        pthread_create(&(pd->tid), nullptr, start_routine, (void*)pd);
        vpd.push_back(pd);
    }
    
    for(int i = 0; i<NUM; ++i)
    {
        printf("new thread:%d,address:%p\n", i+1, vpd[i]);
    }

    for(int i = 0; i<vpd.size()/2; ++i)
    {
        pthread_cancel(vpd[i]->tid);
    }

    for(auto& e : vpd)
    {
        void* ret = nullptr;
        pthread_join(e->tid, &ret);
        printf("join success:%d,exit code:%d\n", e->tid, (long long)ret);
    }

    return 0;
}

运行结果:

对于取消的前五个线程,等待会直接成功,返回值是-1。

未被取消的后五个线程,仍然阻塞等待,等待成功后返回的是设定的线程编号。

如果一个线程是被取消结束的,退出码就是-1,是一个宏PTHREAD_CANCELED。

5.线程分离

默认情况下,在线程退出后,需要使用pthread_join将它加入等待队列,否则就会造成内存泄漏。

但是主线程只能阻塞式线程,阻塞时主线程只能干等着。而且我们有时根本不关心线程的返回值,那阻塞式等待就是巨大的负担。

那么,能不能模仿之前的轮询检测让主线程也继续干活吗?

可以,我们可以将需要释放的线程变为分离状态。我们将一个进程的所有线程做成一个组,如果将一个进程移出这个组,我们就说该线程处于分离状态。此时,主线程不用再关心该线程的状态,它会由系统自动释放。

我在这里也在说明一下,只要是让线程加入等待队列,那就必须要阻塞式等待;只要是分离状态,就不能再加入等待队列。所以说,可加入阻塞队列和分离状态是相互矛盾的,这也解释了为什么不能轮询非阻塞等待。

int pthread_detach(pthread_t thread);

头文件:pthread.h

功能:设置标识符为thread的线程分离状态。

参数:pthread_t thread是需要分离的线程标识符。

返回值:取消成功返回0,取消失败返回错误码。

这个函数既可以分离线程组内的其他线程,也可以分离自己,但分离自己就需要用到自己的线程标识符tid,线程自己的tid可以由下面的函数获取。

pthread_t pthread_self(void);

头文件:pthread.h

功能:获取线程自己的tid。

返回值:返回自己的tid。

我们创建一个新线程,让新进程在第一步就分离执行,观察主线程能否回收它。

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

#define NUM 10

void* start_routine(void* args)
{
    pthread_detach(pthread_self());
    string s = static_cast<const char*>(args);
    for(int i = 0; i<5; ++i)
    {
        cout << s;
        sleep(1);
    }
    return nullptr;
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, start_routine, (void*)"new thread running\n");

    cout<<"main thread tid: 0x"<<(void*)pthread_self()<<endl;
    int n = pthread_join(tid, nullptr);
    cout << "error: " << n << ":" << strerror(n) << endl;

    return 0;
}

线程只要分离,主线程就管不了它了,而且我们发现确实不能回收该分离的线程了,与预期效果一致。

我们把start_routine的前两个语句调换位置。让线程先去构造string对象,然后将其分离。

此时运行观察,我们发现主线程把新线程回收了。这又是怎么回事呢?

这是因为主线程优先运行,在新线程开始运行时,主线程已经在阻塞等待新线程了,这样设置分离也就没有意义了。

既然在新线程中分离线程并不保险,我们就将分离操作全部放在主线程中。

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

#define NUM 10

void* start_routine(void* args)
{
    string s = static_cast<const char*>(args);  
    for(int i = 0; i<5; ++i)
    {
        cout << s;
        sleep(1);
    }
    return nullptr;
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, start_routine, (void*)"new thread running\n");
    //在主线程分离新线程
    pthread_detach(tid);
    cout<<"main thread tid: 0x"<<(void*)pthread_self()<<endl;
    int n = pthread_join(tid, nullptr);
    cout << "error: " << n << ":" << strerror(n) << endl;

    return 0;
}

最终结果就不会出错了。

二、C++多线程小组件

既然我们也学习了一些系统调用了,我们可以试着写一个小组件。这个小组件将线程的创建、执行和等待等都封装起来。我们在程序中指定一个函数,让多个线程不断地执行该函数。

thread_handler.h

#include<pthread.h>
#include<assert.h>
#define NUM 64
class Thread;//前置声明

class Context
{
public:
    Context()
        :_this(nullptr)
        ,_args(nullptr)
    {}
    Thread* _this;//线程this指针
    void* _args;//pthread_create需要传递的args
};

class Thread
{
    typedef std::function<void*(void*)> func_t;
public:
    //构造函数创建线程
    Thread(func_t func, void* args, int number = 0)
        :_func(func)
        ,_args(args)
    {
        //对线程进行规范化命名
        char buffer[NUM];
        snprintf(buffer, sizeof(buffer), "thread-%d", number);
        _name = buffer;
        //将线程信息保存到Context变量
        Context* p = new Context;
        p->_this = this;
        p->_args = args;
        int n = pthread_create(&_tid, nullptr, start_routine, (void*)p);
    }

    //运行函数
    void* run(void* args)
    {
        return _func(args);
    }

    //线程执行start_routine,但是它如果是普通成员函数,则参数多了一个this指针
    //将其变为static成员函数就能消除this指针
    //但同时我们又需要执行线程内部成员_func函数,此时既不能访问this指针的内容,之前创建线程传递的void* args也传递不到
    //我们再次构建一个类Context,将这些内容包含进去,将它们通过一个类指针传过去就好了
    static void* start_routine(void* args)
    {
        Context* cp = static_cast<Context*>(args);
        void* ret = cp->_this->run(cp->_args);
    }

    //将加入等待队列
    void join()
    {
        int n = pthread_join(this->_tid, nullptr);
        assert(n == 0);
    }

private:
    std::string _name;
    pthread_t _tid;
    func_t _func; 
    void* _args;
};

test.cc

#include<iostream>
#include<memory>
#include<unistd.h>
#include"thread_handler.h"
using namespace std;

void* handler(void* args)
{
    string s = "new thread:";
    s += static_cast<const char*>(args);
    s += '\n';
    while(1)
    {
        cout << s;
        sleep(1);
    }
}

int main()
{
    unique_ptr<Thread> t1(new Thread(handler, (void*)"thread1", 1));
    unique_ptr<Thread> t2(new Thread(handler, (void*)"thread2", 2));
    unique_ptr<Thread> t3(new Thread(handler, (void*)"thread3", 3));

    t1->join();
    t2->join();
    t3->join();

    return 0;
}

可以看到三个线程在不停地执行我们的函数。

三、线程库TCB

1.tid

我们编写代码让新线程打印自己的 tid,主线程打印自己和新线程的tid。

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

void* start_routine(void* args)
{
    string s = (char*)args;
    while(1)
    {
        cout << s << " tid:0x" << pthread_self() << endl;
        sleep(1);
    }
}

int main()
{
    pthread_t tid;
    int n = pthread_create(&tid, nullptr, start_routine, (void*)"new pthread");
    assert(n == 0);
    while(1)
    {
        printf("main thread tid:0x%p,new thread tid:0x%p\n", pthread_self(), tid);
        sleep(1);
    }
    return 0;
}

我们发现主线程和新线程打印的新线程tid相同,而且它们是一个地址。

我们一开始就强调,Linux内核中没有线程概念,只有轻量级进程的PCB,更没有TCB这样的数据结构。我们宏观看到的线程是系统的通过clone创造出的轻量级进程,只是这些轻量级进程共用了地址空间等资源。然后这些轻量级进程再通过POSIX原生线程库模拟出我们看到的线程。

既然原生线程库可以保证大量的线程同时工作,那么原生线程库中就必定有能管理这些线程的数据结构,换句话说,TCB结构一定在线程库中。

由于存储线程属性的TCB不在内核中,所以Linux的TCB也叫做用户级线程。

结论:Linux内核只负责调度执行流,用户关心的线程及其属性都由原生线程库维护。Linux的用户级线程和内核轻量级进程都保存着线程的属性,二者基本做到一一对应。

我们将视线转向地址空间:

原生线程库加载到内存后,页表会对虚拟地址空间和其内存物理地址建立映射关系。根据地址空间的分区,线程库会映射在虚拟地址空间中的共享区中,其中TCB等结构和数据当然也在共享区。

看下面的图片

我这里严谨一点:

由于局部储存和栈结构空间都只是在TCB存储指针,虽然这些都是TCB拥有的数据,但它们的内容肯定不会直接保存在TCB里,所以我就把它们和TCB分开了。

你要是认为全部的数据就是TCB也可以接受。

我们能得到以下结论:

  • 共享区内的每个线程都有自己的TCB、局部储存和独立栈,它们由载入内存的原生线程库维护。
  • 主线程的栈就是地址空间的栈区,而新线程的独立栈结构都在共享区,所以线程间才能有独立栈结构。
  • tid是指针类型,TCB在地址空间中的虚拟地址就是tid的值。

2.局部储存

我们知道同一进程的线程相互共用地址空间和页表,所以它们都可以使用一个全局变量。

可如果新线程仍然想用这个变量名,但又不想影响其他线程。这时需要让这个全局变量在每个进程中独立使用,此时就可以使用线程的局部存储属性了。

不使用局部储存

 主线程和新线程都用了同一个g_val变量。

在int g_val = 0;前面加上__thread就能将该变量独立出去。

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

__thread int g_val = 0;

void* start_routine(void* args)
{
    string s = (char*)args;
    while(1)
    {
        ++g_val;
        printf("new thread g_val:%d,address:%p\n", g_val, &g_val);
        sleep(1);
    }
}

int main()
{
    pthread_t tid;
    int n = pthread_create(&tid, nullptr, start_routine, (void*)"new pthread");
    assert(n == 0);
    while(1)
    {
        ++g_val;
        printf("main thread g_val:%d,address:%p\n", g_val, &g_val);
        sleep(1);
    }
    return 0;
}

主线程和新线程使用的不再是同一个g_val变量,互相之间也不会受到影响。

结论:在全局变量或static变量前添加 __thread,可以让每个线程的TCB中都有一份独立的变量,不会互相影响。

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

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

相关文章

代码随想录笔记--哈希表篇

目录 1--有效的字母异位词 2--两个数组的交集 3--两数之和 4--四数相加II 5--三数之和 6--四数之和 1--有效的字母异位词 利用哈希表存储每个字母的出现次数&#xff0c;比较两个字符串各个字母出现次数是否相等即可&#xff1b; #include <iostream> #include <…

QT基础教程之七Qt消息机制和事件

QT基础教程之七Qt消息机制和事件 事件 事件&#xff08;event&#xff09;是由系统或者 Qt 本身在不同的时刻发出的。当用户按下鼠标、敲下键盘&#xff0c;或者是窗口需要重新绘制的时候&#xff0c;都会发出一个相应的事件。一些事件在对用户操作做出响应时发出&#xff0c…

CRM通过哪四个特点赢得不同类型的客户

1.设置正确的目标 首先&#xff0c;在CRM系统中设置正确的目标是非常重要的。不同类型的客户有不同的需求和预期&#xff0c;需要使用不同的方法去处理。如果企业想吸引新客户&#xff0c;那么企业需要更加侧重于建立品牌形象和提供相关的信息。如果企业想留住老客户&#xff…

Socks5代理 vs. Socks4代理:特点和区别解析

在网络通信中&#xff0c;使用代理服务器可以提供更安全、匿名的连接。其中&#xff0c;Socks5和Socks4是两种常见的代理协议。本文将深入探讨它们之间的特点和区别&#xff0c;帮助您选择适合自己需求的代理类型。 1.特点概述 -Socks5&#xff08;Socket Secure 5&#xff0…

MP中的字段还可以利用函数来查询拼接sql

//根据value查询GetMapping("getTest")public List<HashMap> getTest() {QueryWrapper<TTest> queryWrapper new QueryWrapper<>();queryWrapper.eq("substr(name,1,2)","99999");List<TTest> list1 testService.list…

Linux网络编程:线程池并发服务器 _UDP客户端和服务器_本地和网络套接字

文章目录&#xff1a; 一&#xff1a;线程池模块分析 threadpool.c 二&#xff1a;UDP通信 1.TCP通信和UDP通信各自的优缺点 2.UDP实现的C/S模型 server.c client.c 三&#xff1a;套接字 1.本地套接字 2.本地套 和 网络套对比 server.c client.c 一&#xff1a;线…

ogg怎么转mp3格式?让我们一起来学习吧

ogg怎么转mp3格式&#xff1f;如今&#xff0c;有许多种音频格式可供选择&#xff0c;其中包括了很多小伙伴可能并不熟悉的OGG音频格式。OGG的全称是OGG Vorbis&#xff0c;它是一种免费开放且没有使用限制的音频格式&#xff0c;因此备受许多小伙伴的喜爱。然而&#xff0c;OG…

打破数据孤岛,实现文档数据互通

随着数字经济加速发展&#xff0c;企业数字化转型正向更深层次推进。非结构化数据量也正在飞速增长&#xff0c;这些数据以文档、图片、音频等形式散落在组织内部&#xff0c;这给数据的整理和统一利用增加了难度。由于部门、应用、框架、多云环境等原因形成非结构化数据孤岛。…

【React源码实现】元素渲染的实现原理

前言 本文将结合React的设计思想来实现元素的渲染&#xff0c;即通过JSX语法的方式是如何创建为真实dom渲染到页面上&#xff0c;本文基本不涉及React的源码&#xff0c;但与React的实现思路是一致的&#xff0c;所以非常适合小白学习&#xff0c;建议跟着步骤敲代码&#xff…

csp认证真题——重复局面——Java题解

目录 题目背景 问题描述 输入格式 输出格式 样例输入 样例输出 样例说明 子任务 提示 【思路解析】 【代码实现】 题目背景 国际象棋在对局时&#xff0c;同一局面连续或间断出现3次或3次以上&#xff0c;可由任意一方提出和棋。 问题描述 国际象棋每一个局面可以…

01JVM_内存结构

一、什么是JVM 1.JVM的定义 Java程序的运行环境&#xff0c;java二进制字节码的运行环境 2.JVM的好处 ①一次编写&#xff0c;到处运行 ②自动内存管理&#xff0c;垃圾回收功能 ③数组下标越界检查 ④多态 3.jvm&#xff0c;jre&#xff0c;jdk的比较 3.常见的JVM 主…

Mac下Docker Desktop安装命令行工具、开启本地远程访问

Mac系统下&#xff0c;为了方便在terminal和idea里使用docker&#xff0c;需要安装docker命令行工具&#xff0c;和开启Docker Desktop本地远程访问。 具体方法是在设置-高级下&#xff0c; 1.将勾选的User调整为System&#xff0c;这样不用手动配置PATH即可使用docker命令 …

说说我最近筛简历和面试的感受。。

大家好&#xff0c;我是鱼皮。 都说现在行情不好、找工作难&#xff0c;但招人又谈何容易&#xff1f;&#xff01; 最近我们公司在招开发&#xff0c;实习社招都有。我收到的简历很多&#xff0c;但认真投递的、符合要求的却寥寥无几&#xff0c;而且都是我自己看简历、选人…

对于uts namespace共享的测试

前言 单单以下列命令运行虽然是root&#xff0c;还不行&#xff0c;我们需要加--privileged&#xff0c;不然会报 hostname: you must be root to change the host name docker run -it --utshost ubuntu:latest /bin/bash 如果加上--privileged后 docker run -it --priv…

基于AI智能分析网关EasyCVR视频汇聚平台关于能源行业一体化监控平台可实施应用方案

随着数字经济时代的到来&#xff0c;实体经济和数字技术深度融合已成为经济发展的主流思路。传统能源行业在运营管理方面也迎来了新的考验和机遇。许多大型能源企业已开始抓住机遇&#xff0c;逐步将视频监控、云计算、大数据和人工智能技术广泛应用于生产、维护、运输、配送等…

hp惠普光影精灵5笔记本HP Pavilion Gaming-15-dk0135tx原装出厂Win10系统

原厂系统自带所有驱动、出厂主题壁纸LOGO、Office办公软件、惠普电脑管家等预装程序 适用型号&#xff1a; 15-dk0011tx,15-dk0018tx,15-dk0019tx,15-dk0020tx,15-dk0021tx,15-dk0038tx 15-dk0039tx,15-dk0040tx,15-dk0041tx,15-dk0125tx,15-dk0126tx,15-dk0127tx 15-dk012…

排序 Bubble Sort(提取函数)

C自学精简教程 目录(必读) 1 前驱知识点 3.5 for循环语句 3.6 if语句 3.7 函数 3.8 动态内存 2 排序 是将元素按照从小到大的顺序存放的方法。 一开始元素可能并不是按照从小到大的顺序存放的。 这时候我们需要找到需要调整的元素对&#xff0c;并交换这两个元素的值&…

Flutter实现动画列表AnimateListView

由于业务需要&#xff0c;在打开列表时&#xff0c;列表项需要一个从右边飞入的动画效果&#xff0c;故封装一个专门可以执行动画的列表组件&#xff0c;可以自定义自己的动画&#xff0c;内置有水平滑动&#xff0c;缩放等简单动画。花里胡哨的动画效果由你自己来定制吧。 功…

springboot实战(二)之将项目上传至远程仓库

目录 环境&#xff1a; 背景&#xff1a; 操作&#xff1a; 1.注册码云账号 2.创建仓库 步骤&#xff1a; 1.注册完码云账号后&#xff0c;点击加号&#xff0c;新建仓库 2.输入项目名称和介绍&#xff0c;点击创建 3.复制仓库地址&#xff0c;你可以选择https协议或者…

VSAN硬盘出现resetremoved

原创作者&#xff1a;运维工程师 谢晋 VSAN硬盘出现reset&removed 客户环境有8台服务器dell R740和R740XD服务器组成了一套VSAN集群&#xff0c;但R740那四台的物理机老是出现硬盘故障需进行硬盘更换&#xff0c;后发现刚换完的硬盘没过几天又坏了&#xff0c;先开始怀疑…
最新文章