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

 

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

文章目录

一、线程概念

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.rhkb.cn/news/133032.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

AI绘画Stable Diffusion原理之扩散模型DDPM

前言 传送门&#xff1a; stable diffusion&#xff1a;Git&#xff5c;论文 stable-diffusion-webui&#xff1a;Git Google Colab Notebook部署stable-diffusion-webui&#xff1a;Git kaggle Notebook部署stable-diffusion-webui&#xff1a;Git AI绘画&#xff0c;输入一段…

SpringMVC文件的上传下载JRebel的使用

目录 前言 一、JRebel的使用 1.IDea内安装插件 2.激活 3.离线使用 使用JRebel的优势 二、文件上传与下载 1 .导入pom依赖 2.配置文件上传解析器 3.数据表 4.配置文件 5.前端jsp页面 6.controller层 7.测试结果 前言 当涉及到Web应用程序的开发时&…

Android窗口层级(Window Type)分析

前言 Android的窗口Window分为三种类型&#xff1a; 应用Window&#xff0c;比如Activity、Dialog&#xff1b;子Window&#xff0c;比如PopupWindow&#xff1b;系统Window&#xff0c;比如Toast、系统状态栏、导航栏等等。 应用Window的Z-Ordered最低&#xff0c;就是在系…

uni-app 使用uCharts-进行图表展示(折线图带单位)

前言 在uni-app经常是需要进行数据展示&#xff0c;针对这个情况也是有人开发好了第三方包&#xff0c;来兼容不同平台展示 uCharts和pc端的Echarts使用差不多&#xff0c;甚至会感觉在uni-app使用uCharts更轻便&#xff0c;更舒服 但是这个第三方包有优点就会有缺点&#xf…

医院安全不良事件报告系统源码 PHP+ vue2+element+ laravel8+ mysql5.7+ vscode开发

不良事件上报系统通过 “事前的人员知识培训管理和制度落地促进”、“事中的事件上报和跟进处理”、 以及 “事后的原因分析和工作持续优化”&#xff0c;结合预存上百套已正在使用的模板&#xff0c;帮助医院从对护理事件、药品事件、医疗器械事件、医院感染事件、输血事件、意…

数字人员工成企业得力助手,虚拟数字人为企业注入高科技基因

随着互联网和人工智能技术的快速发展&#xff0c;以“数字员工”为代表的数字生产力&#xff0c;正在出现在各行各业的业务场景中。数字人员工的出现不是替代人类&#xff0c;而是通过技术提高工作效率&#xff0c;实现更加智能化的服务体验&#xff0c;帮助企业实现大规模自动…

网上企业订货系统功能列表介绍|企业APP订单管理软件

网上企业订货系统功能列表介绍|企业APP订单管理软件 后台功能列表 &#xff08;后台支持手机版本 订货APP,管理订单的APP&#xff09; 后台登陆 输入账号密码登录企业订货管理软件系统 后台首页 显示近日,月,年订单统计&#xff0c;和收款欠款等统计。 订单模块 新建订单 …

人脸识别三部曲

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

使用 Sealos 一键部署高可用 MinIO,开启对象存储之旅

大家好&#xff01;今天这篇文章主要向大家介绍如何通过 Sealos 一键部署高可用 MinIO 集群。 MinIO 对象存储是什么&#xff1f; 对象是二进制数据&#xff0c;例如图像、音频文件、电子表格甚至二进制可执行代码。对象的大小可以从几 B 到几 TB 不等。像 MinIO 这样的对象存储…

零基础学前端(三)重点讲解 HTML

1. 该篇适用于从零基础学习前端的小白 2. 初学者不懂代码得含义也要坚持模仿逐行敲代码&#xff0c;以身体感悟带动头脑去理解新知识 3. 初学者切忌&#xff0c;不要眼花缭乱&#xff0c;不要四处找其它文档&#xff0c;要坚定一个教授者的方式&#xff0c;将其学通透&#xff…

家政服务接单小程序开发源码 家政保洁上门服务小程序源码 开源完整版

分享一个家政服务接单小程序开发源码&#xff0c;家政保洁上门服务小程序源码&#xff0c;一整套完整源码开源&#xff0c;可二开&#xff0c;含完整的前端后端和详细的安装部署教程&#xff0c;让你轻松搭建家政类的小程序。家政服务接单小程序开发源码为家政服务行业带来了诸…

shell脚本学习笔记02(小滴课堂)

可以在home目录下创建一个shell.sh文件。 按w进入命令行模式。按i进入插入模式。如果想返回命令行模式&#xff0c;按esc即可。然后可以使用x和dd进行删除内容。 在插入模式下我们点击esc键&#xff0c;再去按:键&#xff0c;我们就可以进入到底行模式了&#xff1a; 可以设…

518抽奖软件,可生成几排几列的号码座号

518抽奖软件简介 518抽奖软件&#xff0c;518我要发&#xff0c;超好用的年会抽奖软件&#xff0c;简约设计风格。 包含文字号码抽奖、照片抽奖两种模式&#xff0c;支持姓名抽奖、号码抽奖、数字抽奖、照片抽奖。(www.518cj.net) 生成号码/座号 入口&#xff1a; 主界面上点…

基于深度学习的加密恶意流量检测

加密恶意流量检测 研究目标定位数据收集数据处理基于特征分类算法的数据预处理基于源数据分类算法的数据预处理 特征提取模型选择基于数据特征的深度学习检测算法基于特征自学习的深度学习检测算法 训练和评估精确性指标实时性指标 应用检验改进 摘录自&#xff1a;Mingfang ZH…

ZABBIX 6.4官方安装文档

一、官网地址 Zabbix&#xff1a;企业级开源监控解决方案 二、下载 1.选择您Zabbix服务器的平台 2. Install and configure Zabbix for your platform a. Install Zabbix repository # rpm -Uvh https://repo.zabbix.com/zabbix/6.4/rhel/8/x86_64/zabbix-release-6.4-1.el8…

【实战】H5 页面同时适配 PC 移动端 —— 旋转横屏

文章目录 一、场景二、方案三、书单推荐01 《深入实践Kotlin元编程》02 《Spring Boot学习指南》03 《Kotlin编程实战》 一、场景 一个做数据监控的单页面&#xff0c;页面主要内容是一个整体必须是宽屏才能正常展示&#xff0c;这时就不能用传统的适配方案了&#xff0c;需要…

前端设计模式基础笔记

前端设计模式是指在前端开发中经常使用的一些解决问题的模式或思想。它们是经过实践证明的最佳实践&#xff0c;可以帮助我们更好地组织和管理我们的代码。 一、单例模式&#xff08;Singleton Pattern&#xff09; 单例模式是一种创建型模式&#xff0c;它保证一个类只有一个…

mysql远程连接失败

先上结论&#xff0c;只提出最容易忽略的地方 服务器是阿里云、腾讯云等平台&#xff0c;平台本身自带的防火墙没有开启iptables规则中禁用了3306&#xff0c;即使你根本没有启用iptables服务 第二条是最离谱的 从这里可以看到&#xff0c;我服务器并未启用 iptables 服务 但…

Java8实战-总结24

Java8实战-总结24 用流收集数据收集器简介收集器用作高级归约预定义收集器 用流收集数据 流可以用类似于数据库的操作帮助你处理集合。可以把Java 8的流看作花哨又懒惰的数据集迭代器。它们支持两种类型的操作&#xff1a;中间操作(如filter或map)和终端操作(如count、findFir…

互联网医院App开发:构建医疗服务的技术指南

互联网医院App的开发是一个复杂而具有挑战性的任务&#xff0c;但它也是一个充满潜力的领域&#xff0c;可以为患者和医疗专业人员提供更便捷的医疗服务。本文将引导您通过一些常见的技术步骤来构建一个简单的互联网医院App原型&#xff0c;以了解该过程的基本概念。 技术栈选…