【Linux从入门到精通】多线程 | 线程介绍线程控制

news2024/9/24 1:18:52

 

  本篇文章主要对线程的概念和线程的控制进行了讲解。其中我们再次对进程概念理解。同时对比了进程和线程的区别。希望本篇文章会对你有所帮助。 

文章目录

一、线程概念

1、1 什么是线程

1、2 再次理解进程概念

1、3 轻量级进程

二、进程控制

2、1 创建线程 pthread_create

2、2 线程与进程资源

2、3 线程id

2、4 获得线程id pthread_ self

2、5 线程等待 pthread_join

2、6 线程终止 pthread_exit、pthread_cancel

2、6、1 pthread_exit

2、6、2 pthread_cancel

2、7 线程分离 pthread_detach

三、总结


🙋‍♂️ 作者:@Ggggggtm 🙋‍♂️

👀 专栏:Linux从入门到精通  👀

💥 标题:线程控制💥

 ❣️ 寄语:与其忙着诉苦,不如低头赶路,奋路前行,终将遇到一番好风景 ❣️  

一、线程概念

1、1 什么是线程

  在一个程序里的一个执行路线就叫做线程(thread),线程在进程内部运行。什么是执行线路呢?怎么是在进程内部运行的呢?下面我们通过进程进行理解。

1、2 再次理解进程概念

  我们之前学的进程是:进程就是内核数据结构+代码,同时每个进程都有自己独立的内核数据结构,以保持进程的独立性。具体如下图:

  现在我们用了特定的技术。只创建进程控制块PCB(task_struct),而不再创建对应的地址空间和页表进行映射。我们新创建的进程控制块PCB(task_struct)让它指向我们已经存在的进程的地址空间。具体如下图:

  正如上图所示,所有的进程控制块PCB(task_struct)共享了大部分资源。而这些资源均来自于我们第一个创建的进程控制块PCB(task_struct)

  上图的每个进程控制块PCB(task_struct)执行时,都是用的同一块进程地址空间。而每个进程控制块PCB(task_struct)可称之为线程。我们现在再来理解:在一个程序里的一个执行路线就叫做线程这个概念就不难理解了。其实就是一个 task_struct 所对应的运行起来后就是一个执行流。透过进程虚拟地址空间,可以看到进程的大部分资源,将进程资源合理分配给每个执行流,就形成了线程执行流。

  此时可能会有点疑惑:之前的进程和现在的线程有什么区别呢?

  进程是独立的执行单元,拥有独立的内存空间,包括代码段、数据段和堆栈,因此进程之间的数据不共享。而线程是进程内的执行单元,多个线程共享同一个进程的内存空间,包括代码和数据。具体也可结合下图理解:

  如上图所示,现在的进程是包含了多个进程控制块PCB(task_struct),和其对应的内数据结构。我们之前所学的进程里面只有一个执行流,而现在就不同了。 一个进程内最少有一个执行流,也就是一个进程内部最少有一个线程(主线程)。我们在主线程(最初的进程控制块PCB(task_struct),也可理解为第一个进程控制块PCB(task_struct))內部可创建多个新线程(也就是创建进程控制块PCB(task_struct))。

  站在用户的角度,我们理解进程:进程=内核数据结构+对应的代码和数据。站在内核的角度,我们理解进程:承担分配系统资源的实体

1、3 轻量级进程

  在CPU调度中,只会对进程控制块PCB(task_struct)进行调度。并不会关心是一个进程或者线程。我们也可以认为CPU进行调度的单位是线程。在Linux系统中,在CPU眼中,看到的PCB都要比传统的进程更加轻量化。为什么呢?

  它们与传统进程相比具有较小的资源占用和更快的创建、切换以及通信速度。轻量化进程具有以下特点:

  1. 共享地址空间:不同于传统进程拥有独立的地址空间,轻量化进程与父进程共享相同的地址空间。这样可以减少内存开销和减少上下文切换的开销。

  2. 轻量级创建和销毁:由于轻量化进程与父进程共享地址空间,创建和销毁的开销较小,因为无需分配新的地址空间和重复加载代码段等操作。

  3. 快速上下文切换:由于轻量化进程共享相同的地址空间,所以在进行线程之间的切换时,不需要切换页表,从而使得上下文切换速度更快。

  4. 共享资源:轻量化进程可以通过共享内存等机制方便地进行线程间通信和共享数据,避免了复杂的进程间通信机制带来的开销。

  5. 并发执行:轻量化进程可以在多个CPU核心上并发执行,充分利用多核处理器的性能。

  所以在Linux下,进程控制块又称之为轻量级进程(Lightweight Process)。

二、进程控制

  上面我们了解了进程的概念后,我们接下来看看在Linux怎么创建进程,和对进程的一系列操作。  

2、1 创建线程 pthread_create

  在学习pthread_create之前,我们先了解一下第三方库。本片文章不再讲解Linux操作系统提供了一种创建线程的接口。选择使用第三方库pthread来实现创建线程的一系列操作。pthread库提供了更多功能和跨平台的能力,使得多线程编程更加便捷和灵活。而且大部分语言底层封装的就是第三方库pthread。

  pthread_create函数是pthread库中用于创建线程的函数。下面是对pthread_create函数使用的详细解释:

int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
                   void *(*start_routine) (void *), void *arg);
  • thread:指向pthread_t类型变量的指针,用于存储新线程的标识符。在成功创建线程后,该指针将被填充。
  • attr:指向pthread_attr_t类型变量的指针,用于设置新线程的属性。可以为NULL,表示使用默认属性。
  • start_routine:指向一个返回类型为void*、接受一个void*参数的函数指针。该函数是新线程的起始点,线程将从这个函数开始执行。
  • arg:传递给start_routine函数的参数。它是一个void*类型的指针,可以传递任何类型的数据,通常用于向新线程传递参数。

返回值的含义:

  • 返回值为0:表示线程创建成功。
  • 返回值为正整数:表示线程创建失败,具体的返回值通常对应不同的错误情况,可以使用errno来获取错误码并查看具体错误信息。常见的错误码包括:
    • EAGAIN:当前系统资源不足,无法创建线程。
    • EINVAL:传递给pthread_create函数的参数无效。
    • EPERM:没有足够的权限来创建线程。

  下面我们看一个实际的例子:

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

using namespace std;

void* fun(void* name)
{
    cout << (char*)name << ", pid: " << getpid() << endl;
}

int main()
{
    pthread_t id;
    int n=pthread_create(&id,nullptr,fun,(void*)"new thread 1");
    sleep(1);
    cout << "main thread" << ", pid: " << getpid() <<  endl;
    return 0;
}

  我们上面就是简单创建了一个线程,然后进行打印不同的内容和进程id。运行结果如下图:

  我们发现,他们的进程id确实是相同的。也就我们上述所说的线程在进程内部运行。

  我们再看下一段代码:

int x = 100;

void show(const string &name)
{
    cout << name << ", pid: " << getpid() << " " << x << "\n"
         <<  endl;
}

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

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(1); // 缓解传参的bug
    }

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

   上述代码就是创建了多个线程,去执行同一个函数。同时打印一个全局变量和进程id。我们看运行结果:

  我们也不难发现,线程之间确实有共享的一部分数据。上述例子中的全局变量就被所有线程(执行流)共享。那么线程如何看待进程内部的资源呢?

2、2 线程与进程资源

  我们知道,线程大部分资源是与进程共享的。同时线程也是拥有属于自己的资源。那么到底有哪些资源共享,有哪些资源私有呢?

  在Linux下,线程与进程之间共享的资源有以下几种:

  1. 内存空间:线程和进程都可以访问相同的内存空间,包括代码段、数据段和堆栈段。这意味着线程可以读取和修改相同的变量和数据结构,而不需要进行显式的通信。

  2. 文件描述符:每个进程都有一张文件描述符表,用于跟踪它们打开的文件。当一个线程打开或关闭文件描述符时,其他线程也可以通过相同的文件描述符进行访问。

  3. 信号处理器:进程中的信号处理器对所有线程可见,当一个线程接收到信号时,所有线程都可以对其进行处理。

  4. 共享库和代码段:共享库和可执行文件的代码段可以被多个线程共享。这意味着不同的线程可以同时执行相同的函数或方法。

  5. 其他系统资源:还有一些其他资源如进程ID、进程组ID、用户ID等,在一个进程中创建的线程也会继承这些属性。

  每个线程私有的资源主要包括以下几种:

  1. :每个线程都有自己的栈空间,用于保存函数调用、局部变量和返回地址等信息。

  2. 寄存器:线程使用寄存器来保存当前执行的上下文信息,包括程序计数器、栈指针等。

  3. 线程特定数据:线程可以使用线程特定数据(Thread-Specific Data,TSD)来存储每个线程独有的数据。这些数据在同一进程的不同线程之间是隔离的。

  4. 线程ID:每个线程都有唯一的线程ID,用于标识线程的身份。

  5. 错误号变量:每个线程有自己的错误号变量,用于保存最近的系统调用错误码。

2、3 线程id

  细心的同学发现了,上述并没对线程创建的参数:pthread_t *thread 进行过多解释。那么pthread_t 是什么类型呢?其实 pthread_t 是一个 unsigned long int 类型的。又有什么用呢?

  我们在Linux下可通过指令:ps -aL,来查看进程和线程资源。具体如下图:

  其中我们看到有PID、LWP。LWP(Lightweight Process)所对应的就是线程的id。PID与LWP相等的就是主线程。这里的LWP与pthread_t *thread是一样的吗?我们不妨打印一下看看pthread_t *thread的值。如下图:

  事实上,这里所说的 thread 与我们上述将LWP的id值并不是相同的。pthread_t *thread到底指的是什么呢?取决于实现。对于Linux目前实现的NPTL实现而言,pthread_t类型的线程ID,本质就是一个进程地址空间上的一个地址。怎么是地址呢?我们接着往下看。

  我们知道,线程的执行过程中需要保存和管理各自的局部变量、函数调用以及其他线程执行时需要的临时数据。每个线程是必须有自己的栈空间。如果没有独立的栈空间,那么每个线程在压栈的时候,数据就会混乱。但是进程地址空间只有一个栈空间。怎么保证每个栈都有独立的占空间呢?具体如下图:

  为了保证每个线程有独立的栈空间,在每当创建一个线程的时候,都会在共享内存区为线程创建一个独立的的struct pthread,当中包含了对应线程的各种属性,包括栈空间。每个线程都有自己的线程局部存储,当中包含了对应线程被切换时的上下文数据。而这部分数据是有线程库给我们创建和维护的。
  每一个新线程在共享区都有这样一块区域对其进行描述,怎么找到这块空间呢?于是当创建成功后,就会把该块空间的起始地址进行返回,而pthread_t *thread就是接受的该地址!!!

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

  • 前面讲的线程ID属于进程调度的范畴。因为线程是轻量级进程,是操作系统调度器的最小单位,所以需要一个数值来唯一表示该线程。
  • pthread_ create函数第一个参数指向一个虚拟内存单元,该内存单元的地址即为新创建线程的线程ID,属于NPTL线程库的范畴。线程库的后续操作,就是根据该线程ID来操作线程的。
  • 线程库NPTL提供了pthread_ self函数,可以获得线程自身的ID。

2、4 获得线程id pthread_ self

  pthread_self()函数是POSIX线程库中的一个函数,它用于获取当前线程的线程ID。该函数的定义如下所示:

pthread_t pthread_self(void);

  在调用该函数时,它会返回一个用于表示当前线程的pthread_t类型的值。通常我们将这个值存储在一个变量中,以便后续使用。下面是一个使用pthread_self()函数的示例:

#include <stdio.h>
#include <pthread.h>

void* thread_func(void* arg) {
    pthread_t tid = pthread_self();
    printf("Thread ID: %lu\n", tid);
    // 执行其他操作...
    return NULL;
}

int main() {
    pthread_t tid;
    pthread_create(&tid, NULL, thread_func, NULL);
    
    pthread_join(tid, NULL); // 用来阻塞等待线程,回收资源    
    return 0;
}

  运行结果如下:

2、5 线程等待 pthread_join

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

函数原型如下:

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

参数说明

  • thread:要等待的线程的标识符,类型为pthread_t。
  • value_ptr:指向一个指针的指针,用于存储被等待线程的返回值。返回值通过该指针间接传递给调用者。

返回值

  • 成功返回0;失败返回错误码

函数功能

  • 当调用pthread_join时,会阻塞当前线程,直到指定的线程完成其执行,并返回其返回值。
  • 如果线程已经结束,那么pthread_join会立即返回。
  • 当一个线程终止后,它的返回值会被保留起来,并且可以由其他线程使用pthread_join进行获取

   我们先来看一下其使用,稍后会解释返回值的情况,代码如下:

void* fun(void* name)
{
    int cnt=5;
    while(true)
    {
        cout << (char*)name << ", pid: " << getpid() << endl;
        sleep(1);
        if(!--cnt)
            break;
    }
}

int main()
{
    pthread_t id;
    int n=pthread_create(&id,nullptr,fun,(void*)"new thread 1");
    (void)n;
    int* ret;
    pthread_join(id,(void**)&ret);
    cout << "main thread" << ", pid: " << getpid() <<  endl;
    return 0;
}

 直接看运行结果:   根据结果看到,时进行阻塞式等待。能不能像父进程等待子进程那样进行循环检测等待呢?答案是不能的。

  那么接下来我们再看一下其value_ptr到底是什么,和它是怎么来的。

  当我们使用pthread_create创建线程时,其返回值是void*。而pthread_join的第二个参数就是接受的线程结束的返回值。这也是其类型为void** 的原因。我们不妨通过代码来看一下。代码如下:

void* fun(void* name)
{
    int cnt=1;
    while(true)
    {
        cout << (char*)name << ", pid: " << getpid() << endl;
        sleep(1);
        if(!--cnt)
            break;
    }

    return (void*) 10;
}

int main()
{
    pthread_t id;
    int n=pthread_create(&id,nullptr,fun,(void*)"new thread 1");
    (void)n;
    int* ret=nullptr;
    pthread_join(id,(void**)&ret);
    cout << "main thread" << ", pid: " << getpid() <<  endl;
    cout<< "return value: " << (int)ret << endl;
    return 0;
}

  注意:返回值是一个以指针的形式进行返回的。我们可以对其进行强制类型转换后打印,不可以对其进行解引用。否泽就会引起段错误。具体结果如下:

2、6 线程终止 pthread_exit、pthread_cancel

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

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

  接下来我们看看其用法和细节都有哪些。

2、6、1 pthread_exit

  pthread_exit是、用于终止当前线程的执行并返回一个特定的值。它可以通过调用pthread_exit来显式地结束线程,也可以在线程函数的返回语句中隐式调用。

  下面是一个简单的示例,说明了pthread_exit的使用方法和其中的一些细节:

void* thread_function(void* arg) {
    int thread_arg = *(int*)arg;
    
    // 打印线程参数
    printf("Thread argument: %d\n", thread_arg);
    
    // 结束线程
    pthread_exit((void**)13);
}

int main() {
    pthread_t thread;
    int arg_value = 100;
    
    // 创建线程,并传递参数
    if (pthread_create(&thread, NULL, thread_function, (void*)&arg_value) != 0) {
        fprintf(stderr, "Failed to create thread.\n");
        return 1;
    }
    
    // 等待线程结束
    int* thread_result=0;
    int n=pthread_join(thread, (void**)&thread_result);
    (void)n;
      
    cout<<"thread_result : "<<(int)thread_result<<endl;
    printf("Thread finished.\n");
    
    return 0;
}

  在上述示例中,我们首先创建了一个线程(由pthread_create函数执行),并将参数arg_value传递给线程函数thread_function。在线程函数中,我们将打印出传递的参数值,然后通过调用pthread_exit函数来显式地终止线程的执行。

  参数可以是任意类型的指针(void*),用于传递线程的退出状态。通常情况下,这个参数被用来告知父线程关于子线程执行的结果或者其他相关信息。当线程调用pthread_exit时,它会将退出状态作为返回值传递给等待它的父线程。

  线程终止时,它的资源会被自动释放,包括线程栈和线程局部变量等。同时,父线程也可以通过pthread_join函数来等待子线程的退出,并获取其退出状态

  下面我们来看一下上述的运行结果,具体如下图:

  我们之前学过exit()函数用来终止当前运行的程序。但是exit()函数和 pthread_exit()函数是有所区别的。exit()函数是用来终止进程的。当我们在新线程中使用exit()函数,那么整个进程将会被终止掉

2、6、2 pthread_cancel

  pthread_cancel函数是用于取消线程的函数,它允许一个线程取消同一进程中的另一个线程的执行。pthread_cancel函数原型如下:

int pthread_cancel(pthread_t thread);

参数:

  • pthread_t thread:目标线程的标识符,即要取消的线程。

返回值:

  • 成功:返回0。
  • 失败:返回非0的错误码,表明函数调用失败的具体原因。

  下面我们来看一个实际例子,来理解一下 pthread_cancel函数 的使用。

// 目标线程函数
void* threadFunc(void* arg) {
    std::cout << "Thread has started." << std::endl;
    
    // 模拟工作
    for (int i = 0; i < 10; ++i) {
        std::cout << "Working..." << std::endl;
        sleep(1);
    }
    
    std::cout << "Thread is finished." << std::endl;
    
    // 清理工作,释放资源
    
    pthread_exit(NULL);
}

int main() {
    pthread_t tid;
    
    // 创建目标线程
    if (pthread_create(&tid, NULL, threadFunc, NULL) != 0) {
        std::cerr << "Failed to create thread." << std::endl;
        return 1;
    }
    
    // 主线程等待一段时间
    sleep(3);

    // 向目标线程发送取消请求
    if (pthread_cancel(tid) != 0) {
        std::cerr << "Failed to cancel thread." << std::endl;
        return 1;
    }
    
    // 等待目标线程结束
    if (pthread_join(tid, NULL) != 0) {
        std::cerr << "Failed to join thread." << std::endl;
        return 1;
    }
    
    std::cout << "Main thread is finished." << std::endl;
    
    return 0;
}

  上述例子,我们就是使用了pthread_cancel来终止线程。当然,线程还没有运行结束时就对其进行终止。具体运行结果如下图:

  能不能在线程的內部进行自己终止自己呢?代码如下:

pthread_cancel(pthread_self());

  这是一个取消自身线程的操作。首先,取消自身线程可能会导致未完成的工作无法正常结束,尤其是当你的线程在执行某些关键任务时。这可能导致资源泄漏或数据不一致的问题。

其次,取消自身线程可能打破了线程安全的设计原则。如果其他线程依赖于你的线程的状态或结果,那么取消自身线程可能会导致这些线程的行为出现问题。因此,一般来说,推荐使用pthread_cancel函数取消其他线程而不是自身线程。这样可以更好地控制线程的取消操作,并确保线程能够优雅地退出,以避免可能的问题。

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

2、7 线程分离 pthread_detach

  pthread_detach() 函数用于将指定线程标记为分离状态。当一个线程被标记为分离状态后,该线程的系统资源将在其退出时自动释放,无需其他线程调用 pthread_join() 来获取其返回状态

  下面是一个示例,演示了如何使用 pthread_detach() 函数将线程设置为分离状态:

void* thread_function(void* arg) {
    printf("子线程正在执行\n");
    sleep(3);
    printf("子线程执行完毕\n");
    return NULL;
}

int main() {
    pthread_t tid;
    if (pthread_create(&tid, NULL, thread_function, NULL) != 0) {
        printf("线程创建失败\n");
        return 1;
    }
    
    if (pthread_detach(tid) != 0) {
        printf("线程分离失败\n");
        return 1;
    }
    
    printf("主线程继续执行\n");
    sleep(5);
    printf("主线程执行完毕\n");

    return 0;
}

  在上述示例中,我们首先创建了一个新的线程,线程函数被设计为休眠3秒后退出。然后,我们调用 pthread_detach() 函数将线程 tid 标记为分离状态。之后,主线程继续执行并休眠5秒后退出。

  由于我们将线程 tid 分离,因此不需要调用 pthread_join() 来等待子线程结束。相反,当线程 tid 执行完毕时,系统将自动回收其资源

三、总结

  当了解完线程的控制以后,我们先大概的总结一下线程的优缺点。

线程的优点:

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

线程的缺点:

  1. 难以调试和协调:多线程程序因为涉及到共享资源的并发访问,会面临复杂的调试和协调问题。例如,线程间的竞争条件(Race Condition)会导致数据不一致或死锁等问题,这些问题难以定位和排查。

  2. 资源消耗:线程的创建和销毁需要消耗系统资源,包括内存和CPU时间片等。同时,线程之间的切换也会引入一定的开销。过多的线程数量可能会导致系统资源耗尽或降低整体性能。

  3. 容易出现同步问题:多线程程序在访问共享资源时需要进行同步操作,如加锁和解锁。而同步操作的过度使用可能导致性能下降,因为在执行同步操作期间,其他线程可能被阻塞等待资源释放。

  4. 可能引发安全问题:多线程程序中存在着线程间的竞争,如果没有正确处理竞争条件,可能会引发安全问题,如数据损坏、数据泄露等。

  5. 编程复杂性高:多线程编程相对于单线程编程来说更加复杂,需要考虑并发控制、同步机制等问题。编写高效且正确的多线程程序需要对并发编程概念和技术有深入的了解,对开发者的要求较高。

  上述的线程缺点大部分来自于代码的问题。当然,线程的大部分问题对一个优秀的的程序员来说,并不是问题。

  线程还有异常问题:单个线程如果出现除零,野指针等问题导致线程崩溃,进程也会随着崩溃。为什么呢?主要是因为线程共享进程的资源。

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

  在一个进程中,多个线程共享相同的内存空间和其他系统资源。这意味着当一个线程发生崩溃时,它可能会影响到共享的资源。例如,如果一个线程遇到除零错误导致异常终止,它可能会导致相关的共享数据被破坏或变得不可用。

  此外,操作系统为了保证进程的稳定性和安全性,在出现线程崩溃的情况下通常会终止整个进程。这是因为一个线程的崩溃可能会对其他线程产生意想不到的影响,进而导致进程无法继续正常运行。为了避免这种情况下可能出现的更严重的问题,操作系统会选择终止整个进程,以确保系统的稳定性。

  那么上述我们也讲解了线程分离。如果线程分离后,线程出现错误导致崩溃后会引起整个进程进行崩溃吗?答案是会的!因为本质上他们还是在共享一个进程的资源

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

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

相关文章

Postgresql与执行计划相关的配置项

1. ENABLE_*参数 在PostgreSQL中有一些以“ENABLE_”开头的参数&#xff0c;这些参数提供了影响查询优化器选择不同执行计划的方法。有时&#xff0c;如果优化器为特定查询选择的执行计划并不是最优的&#xff0c;可以设置这些参数强制优化器选择一个更好的执行计划来临时解决这…

java开发之个微朋友圈机器人的开发

简要描述&#xff1a; 取消点赞 请求URL&#xff1a; http://域名地址/snsCancelPraise 请求方式&#xff1a; POST 请求头Headers&#xff1a; Content-Type&#xff1a;application/jsonAuthorization&#xff1a;login接口返回 参数&#xff1a; 参数名必选类型说明…

分子对接简介-2023

分子对接简介-2023 分子对接&#xff08;Molecular Docking&#xff09;是分子模拟的重要方法之一&#xff0c;其本质是两个或多个分子之间的识别过程&#xff0c;其过程涉及分子之间的空间匹配和能量匹配。这项技术在药物研发、生物医学研究和药物设计中具有广泛的应用&#…

简明 SQL 组合查询指南:掌握 UNION 实现数据筛选

在SQL中&#xff0c;组合查询是一种将多个SELECT查询结果合并的操作&#xff0c;通常使用UNION和UNION ALL两种方式。 UNION 用于合并多个查询结果集&#xff0c;同时去除重复的行&#xff0c;即只保留一份相同的数据。UNION ALL 也用于合并多个查询结果集&#xff0c;但不去除…

MapRdeuce工作原理

hadoop - (三)通俗易懂地理解MapReduce的工作原理 - 个人文章 - SegmentFault 思否 MapReduce架构 MapReduce执行过程 Map和Reduce工作流程 (input) ->map-> ->combine-> ->reduce-> (output) Map&#xff1a; Reduce

腾讯mini项目-【指标监控服务重构】2023-07-27

今日已办 SigNoz Log Management SigNoz原生支持 OpenTelemetry 来收集日志&#xff0c;SigNoz 在收集器端进行了优化&#xff0c;为SigNoz中的日志添加了不同的功能。 OpenTelemetry 提供了各种接收器和处理器&#xff0c;用于直接通过 OpenTelemetry Collector 或通过 Flue…

Everything+cpolar内网穿透轻松实现公网远程访问本地硬盘文件

公网远程访问本地硬盘文件【内网穿透】 文章目录 公网远程访问本地硬盘文件【内网穿透】前言1. 下载cpolar和Everything软件3. 设定http服务器端口4. 进入cpolar的设置5. 生成公网连到本地内网穿透数据隧道 总结 前言 随着云概念的流行&#xff0c;不少企业采用云存储技术来保…

建筑模板的抗震性能如何评估和测试?

评估和测试建筑模板的抗震性能通常涉及以下几个方面&#xff1a; 1. 材料测试&#xff1a;首先&#xff0c;需要对建筑模板所使用的材料进行力学性能测试。这包括测量材料的抗弯强度、抗压强度、剪切强度等参数。这些测试可以通过标准化的实验方法进行&#xff0c;例如使用万能…

Gavin Wood 演讲全文:建设更具韧性以应变化的 Polkadot

我们非常激动能邀请到 Gavin Wood 博士来现场分享关于 Polkadot 的近况以及最新的进展&#xff0c;带来他对于《加密项目应该怎样应对当今世界的变化》的演讲分享。 &#x1f6a9;点击视频链接观看演讲实录&#xff1a;https://www.youtube.com/watch?vYw3mQNJ5UJQ&t1048s…

数据驱动的仿真

数据驱动的仿真 数据驱动的仿真&#xff08;1&#xff09;动态状态估算。&#xff08;2&#xff09;在线模型调整。&#xff08;3&#xff09;动态事件重构。 数据驱动的仿真 数字孪生体之所以与传统仿真不同&#xff0c;是因为它产生之初就是数据驱动的。早在1979年&#xff…

10英寸及以上占比超7成!车载大屏保持高增速,哪些供应商在领跑?

中控大屏「上车」继续保持高增速态势。 高工智能汽车研究院监测数据显示&#xff0c;今年1-7月中国市场&#xff08;不含进出口&#xff09;乘用车前装标配搭载10英寸及以上大屏&#xff08;含多联屏&#xff09;交付775.16万辆&#xff0c;同比去年同期增长38.19%。 同时&am…

生信分析Python实战练习 7 | 视频24

开源生信 Python教程 生信专用简明 Python 文字和视频教程 源码在&#xff1a;https://github.com/Tong-Chen/Bioinfo_course_python 目录 背景介绍 编程开篇为什么学习Python如何安装Python如何运行Python命令和脚本使用什么编辑器写Python脚本Python程序事例Python基本语法 数…

Docker部署Canal监听MySQL binlog

文章目录 概念简述binlogCanal MySQL配置Canal配置创建挂载目录设置权限创建MySQl的Canal账户拉取镜像运行容器简单运行配置文件复制到宿主机修改配置文件删除之前运行的canal容器正式运行Canal容器 查看运行状态排查问题 概念简述 binlog MySQL的二进制日志binlog可以说是My…

支持向量机(SVM)案例分析

支持向量机&#xff08;support vector machines, SVM&#xff09;是一种二分类模型&#xff0c;所谓二分类模型是指比如有很多特征&#xff08;自变量X&#xff09;对另外一个标签项&#xff08;因变量Y&#xff09;的分类作用关系&#xff0c;比如当前有很多特征&#xff0c;…

MySQL 解决数据重复添加

1. sql语句: insert ignore into insert ignore into 表名 (xx1,xx2,xx3) VALUES (#{xx1},#{xx2},#{xx3}) 2. 复合索引

Vue基础入门---详细简介

一&#xff0c;对Vue的概念 1.1 什么是Vue &#xff1f; 一种流行的JavaScript前端框架&#xff0c;用于构建交互式的Web应用程序。它以简洁、灵活和高效的特性而受到广泛欢迎。Vue采用了一种响应式的数据绑定机制&#xff0c;使得数据的变化能够自动更新相关的DOM元素&#x…

无涯教程-JavaScript - COMBIN函数

描述 COMBIN函数返回给定数量的项目的组合数量。使用COMBIN确定给定数量的项目的组总数。 语法 COMBIN (number, number_chosen)争论 Argument描述Required/OptionalNumberThe number of items.RequiredNumber_chosenThe number of items in each combination.Required No…

人脸识别三部曲

人脸识别三部曲 首先看目录结构图像信息采集 采集图片.py模型训练 训练模型.py人脸识别 人脸识别.py效果 首先看目录结构 引用文121本 opencv │ 采集图片.py │ 训练模型.py │ 人脸识别.py │ └───trainer │ │ trainer.yml │ └───data │ └──…

如何把VRTE的应用程序在Ubuntu上跑起来?

1、rvbuild -d project_name 20 产生一个文件夹vrte 如果此时直接将vrte文件夹打包放到Ubuntu的 /opt目录内 执行./opt/vrte/usr/bin/exmd.sh 会出现如下错误&#xff1a; 但是查看文件发现这个文件是存在的。 系统找不到文件是因为找不到对应的ld库 从VRTE里面拷贝一个库…

css动画旋转分层旋转图

1.代码 <template><view class"animations"><view class"animation animation1"><view class"animate1"></view><view class"animate2"></view><view class"animate3">&l…