多线程基本概念

news2024/12/26 23:42:05

多线程

  • 多线程基本概念
  • 线程控制
    • 创建
    • 终止
    • 等待
    • 分离
  • 线程安全
    • 基本概念
    • 实现
    • 互斥
      • 互斥锁
      • 死锁
    • 同步
  • 线程应用
    • 生产者与消费者模型
    • 线程池
    • 单例模式

多线程基本概念

线程是进程中一个执行流程,是 CPU 进行执行调度的基本单元;
进程是系统进行资源分配的基本单元。

Linux 下一个进程中是可以存在多个 pcb 的,一个 pcb 就是一个执行流程。

1、一个进程中有多个 pcb 与多个进程有多个 pcb 再多执行流程中使用有什么区别?

例如零件加工厂加工零件,若有多个零件需要加工
多进程
相当于多开几个厂子,多个厂子可以同时进行零件加工;
因此资源消耗大,但是更稳定 健壮性强

多线程
相当于在一个厂子里边多开几条生产线
资源消耗小,但是健壮性不如多进程

线程是 CPU 调用执行的基本单元,而 Linux 下 pcb 是程序运行过程的描述,因此 Linux 下线程是通过 pcb 来实现的

在这里插入图片描述

多进程:把多个任务分成多个程序,一个进程执行一个
多线程:把一个整体的程序分为几个不同模块,一个 pcb 负责调度一个模块

在这里插入图片描述

2、线程 vs 进程

进程是系统进行资源分配的基本单元(每运行一个程序,系统就要分配一次程序运行所需要的资源)

线程是 CPU 进行执行调度的基本单元,在Linux下是通过 pcb 来实现的,一个进程中可以有多个pcb,因此也被称为 轻量级进程 LWP

3、多进程 vs 多线程

多进程:程序更具健壮性、更稳定

多线程:
(1)线程间通信更加灵活(共享虚拟地址,包含进程间通信在内,全局变量、函数传参…)
(2)创建和销毁成本更低(线程之间很多资源都是共享的,创建一个线程不需要分配太多资源)
(3)同进程的线程间调度成本更低(CPU 上加载的快表信息,也表指针…都不需要替换)

对于程序的安全性要求大于性能和资源要求则使用多进程(例如 shell);其余使用多线程

4、多个线程在同一个进程中同时运行为什么不会混乱?

(1)其实每个线程调度执行的就是一个函数;
vfork - 创建子进程,父子进程公用同一个虚拟地址空间,为了避免出现栈运行混乱,因此父进程阻塞直到子进程程序替换或退出

(2)多线程关于执行出现混乱的解决方案:把所有有可能出现混乱的地方给每个线程单独存一份:

线程间独有信息:标识符、栈、上下文数据、信号屏蔽字、errno…

线程间共享信息:虚拟地址空间、文件描述符表、信号处理方式、工作路径、用户ID 、组ID…

5、多任务处理中

多任务处理中使用多执行流完成:
可以更加充分地利用计算机资源, 提升任务处理效率

在多任务处理中使用多少执行流可以由压力测试得到最合适的数量,并不是执行流越多越好,因为执行流越多,cpu 切换调度就越频繁,若执行流太多,反而会造成切换调度消耗大量资源。

6、在任务处理中,程序分两种

(1)cpu 密集型程序:

一段程序几乎都是数据的运算

(2)IO 密集型程序:

一段程序中大部分都是 IO 操作(大部分时间都在进行 IO 操作以及等待,因此对 cpu 使用率并不高)

线程控制

线程中接口都是库函数实现的--------链接库文件 -lpthread

在这里插入图片描述

#include <pthread.h> //头文件

创建

int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine) (void *), void *arg);

(1)thread:传入一个 pthread_t 类型变量的地址空间,用于接收线程 ID ---- 线程的操作句柄
(2)attr:线程属性----通常置NULL
(3)start_routine:函数指针,传入线程入口函数的地址,这个线程调度运行的就是这个函数
(4)arg:给 start_routine 线程入口函数传入的参数;
功能:创建一个线程,指定这个线程要运行的函数 start_routine ,并且给这个函数传入一个数据 arg
返回值:成功 0,失败返回非0

    1 #include<stdio.h>                                                       
    2 #include<pthread.h>
    3 #include<unistd.h>
    4 
    5 void* thread_entry(void* arg)
    6 {
    7   //线程入口函数
    8   while(1){
    9     printf("i am nomal thread:%s\n",arg);
   10     sleep(1);
   11   }
   12   return NULL;
   13 }
   14 
   15 
   16 int main()
   17 {
   18   //线程的创建
   19    //int pthread_create(pthread_t *thread, const pthread_attr_t *attr,vo      id *(*start_routine) (void *), void *arg);
   20 
   21   pthread_t tid;     //用来保存线程 ID
W> 22   char* arg="leihoua!!";     //给线程入口函数传入的参数
   23 
   24   int ret=pthread_create(&tid,NULL,thread_entry,(void*)arg);
   25   if(ret!=0){
   26     printf("thread create error!\n");
   27     return -1;
   28   }
   29    
   30   //普通线程一旦创建成功,创建出来的这个线程调度的是传入的线程入口函数,      因此能够走下来的只有主线程
   31   //线程中不存在父子线程
   32   while(1){
   33     printf("i am main thread!!\n");
   34     sleep(1);
   35   }
   36   return 0;
   37 }                         

在这里插入图片描述
在这里插入图片描述

线程被创建出来之后,谁先运行不一定,看操作系统的调度

(1)创建一个线程其实就是让操作系统给我们提供一个执行流,至于这个线程做什么,取决于线程的入口函数(由程序员自己决定)

线程是一个执行流,调度一段函数运行

(进程:fork() 创建一个子进程之后,父子进程公用一段代码)

查看线程信息 : ps -L 选项
查看的其实是轻量级进程信息

(2)一个程序运行起来,默认会创建一个线程(pcb),这个线程有自己的 pid
如果下边通过 pthread_create 创建了一个线程(pcb),这个线程也有自己的pid
真正使用 ps 查看进程信息的时候查看的是主线程pcb对应的信息

在这里插入图片描述
在这里插入图片描述

(3)pthread_create 接口创建第一个参数获取到的 tid 并不是轻量级进程的 pid(lwp)

在这里插入图片描述
tid 实际保存的是一个地址,这个地址指向了线程相对独立的空间

终止

线程终止:退出一个线程的运行

线程其实调用运行的是创建时所传入的入口函数,因此线程的入口函数运行完了,线程就退出了

1、在线程入口函数中 return

注意:main 函数中 return 退出的不仅仅是主线程,还有整个程序

在这里插入图片描述
在这里插入图片描述

2、在任意位置调用接口实现线程的退出
void pthread_exit(void *retval);
retval :用于设置线程的退出返回值
谁调用谁退出

在这里插入图片描述
在这里插入图片描述

3、在任意位置调用接口用于取消指定线程的运行
int pthread_cancel(pthread_t thread);
一个线程若是被取消的,则返回值就不是一个正经的返回值

线程正常退出是有返回值设置的,但若是 pthread_cancel 取消线程的,则没有设置返回值。

在这里插入图片描述
在这里插入图片描述

等待

1、主线程退出,其实并不会影响其他线程的运行(不多见);
所有线程退出了,则进程退出释放所有资源;
若进程要退出,则会先退出所有线程。
2、一个线程退出了,资源也并没有完全被释放(要保存返回值)

线程等待

等待指定的线程退出,获取退出线程的返回值,回收退出线程的所有资源

int pthread_join(pthread_t thread, void **retval);
(1)thread:要等待退出的指定线程 tid
(2)retval:用于获取线程的退出返回值;因为线程退出返回值为 void
,因此传入指针变量的地址,将变量的地址放在指针变量空间
返回值:成功返回 0,失败返回错误编号

线程等待:
在这里插入图片描述

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

void* thread_entry(void* arg)
{
  //线程入口函数
  int count=0;
  while(1){
    printf("i am nomal thread:%s\n",arg);
    count++;
    
    //pthread_exit(NULL);    //任意位置调用可以退出线程
    
    if(count==3)return "nihao ";         //返回值为字符串 类型
    
    sleep(1);
  }
  return NULL;
}

int main()
{
  //线程的创建
   //int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine) (void *), void *arg);

  pthread_t tid;     //用来保存线程 ID
  char* arg="leihoua!!";     //给线程入口函数传入的参数

  int ret=pthread_create(&tid,NULL,thread_entry,(void*)arg);
  if(ret!=0){
    printf("thread create error!\n");
    return -1;
  }

  //线程等待
  void* retval;   //用于接收退出返回值
  ret=pthread_join(tid,&retval);     //等待指定的线程 tid 退出,并获取退出返回值 retval  
  if(ret!=0){
    printf("pthread_join failed\n");
    return -1;
  }
  printf("exit----%s\n",retval);  //打印退出返回信息


  //pthread_cancel(tid);         //任意位置调用退出线程

   
  //普通线程一旦创建成功,创建出来的这个线程调度的是传入的线程入口函数,因此能够走下来的只有主线程
  //线程中不存在父子线程
  while(1){                      //打印线程ID
    printf("i am main thread!!----%d\n",tid);
    sleep(1);
  }
  return 0;
}


运行结果:
在这里插入图片描述

线程之间传递数据一定要注意数据的生命周期
(可以是 malloc 动态申请的空间 \ static j静态数据 \ 字面常量 )

若一个线程是被取消的,则获取的返回值是一个宏:PTHREAD_CANCELED ==== 》 (void*)(-1)

在这里插入图片描述

分离

在线程属性中,有一个属性叫做分离属性,默认值是 joinable 状态,表示线程退出后不会自动释放资源, 需要被其他线程等待。

但有时候我们并不关心一个线程的返回值,也不想等待它退出,则这个时候将这个分离属性设置为 detach 状态,表示线程退出后自动释放所有资源,不需要被释放(资源是自动释放的,因此也不能被等待)

int pthread_detach(pthread_t thread);
设置指定线程的分离属性为 detach
返回值:成功返回0 ,失败返回错误编号

在这里插入图片描述

=============================================================================================================================

在这里插入图片描述

线程安全

基本概念

不安全:在多线程程序中,若涉及到了对共享资源的操作,则有可能导致数据的二义性。

线程安全:对共享资源的操作不会导致数据二义性

实现

实现:如何实现多线程对共享资源的操作不会出问题?
同步:通过条件控制,让多执行对资源的获取更加合理
互斥:通过同一时间执行流对资源访问的唯一性,保证访问安全

互斥的实现:互斥锁(读写锁、自旋锁…)

同步的实现:条件变量、信号量

互斥锁实现互斥----实现对共享资源的唯一访问

本质也就是一个 0/1计数器,通过 0/1 标记资源的访问状态(0-不可访问,1-可访问)
在访问资源之前进行加锁操作(通过状态判断是否可以访问,不可访问则阻塞);
在访问资源之后进行解锁操作(将资源状态置为可访问状态,唤醒其他阻塞的进程);

多个线程想要实现互斥,就必须访问同一个锁-----------意味着锁是一个共享资源

互斥

互斥锁

互斥锁如何实现自身安全?----> 一步置换

互斥锁本身的计数器操作是原子操作

对变量内容的修改过程----------------将变量从内存中加载到(CPU)寄存器中,在寄存器中修改变量值,再将修改之后的值返回到内存中。

因此对于互斥锁来说,若某一个线程将互斥锁的数据(1-可访问)加载到cpu上进行处理,要修改成 0-不可访问,但是还没有将处理之后的数据返回内存之前,线程进行了切换,第二个进程也要进行加锁操作,要将互斥锁的值(1)加载到 cpu ,此时会形成矛盾。

因此 指令 exchange :交换 cpu 指定寄存器与内存中的数据
互斥锁操作:
1)先将指定寄存器中值修改为 0
2)将寄存器与内存中数据互换
3)判断是否符号获取锁的条件/判断能否可以加锁

置换操作是一条指令完成的,不可被打断
置换之前,把寄存器的值置为 0,因此置换之后内存中互斥锁的值为 0,这样就保证,不管当前线程是否可以加锁,至少在之后的线程都不能进行加锁。

接口

pthread_mutex_t :互斥锁变量类型

初始化互斥锁

int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
(1)mutex:互斥锁变量地址
(2)attr:互斥锁变量属性-----NULL

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
这种初始化,不需要使用 destroy 进行释放

访问资源前进行加锁

int pthread_mutex_lock(pthread_mutex_t *mutex);
阻塞加锁; 加不上锁则一直等待

int pthread_mutex_trylock(pthread_mutex_t *mutex);
非阻塞加锁;加不上锁则立即报错返回

int pthread_mutex_unlock(pthread_mutex_t *mutex);
访问资源完毕之后进行解锁

释放销毁互斥锁

int pthread_mutex_destroy(pthread_mutex_t *mutex);

练习

1、多线程中共享资源访问若不加锁可能会出什么问题?

2、如何通过使用互斥锁来保护临界区(共享资源的访问过程)

练习代码:

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

int ticket=100;

void* Scalper(void* arg)
{
  //入口函数
  while(1){
    if(ticket>0){
      printf("%p 抢到了 %d 号票\n",pthread_self(),ticket);
      ticket--;
    }else{
      printf("票没了~~!\n");
      break;
    }
  }
  return NULL;
}

int main()
{
  pthread_t tid[4];   //创建四个线程

  int i=0;
  for(i=0;i<4;++i){
    int ret=pthread_create(&tid[i],NULL,Scalper,NULL);
    //四个线程调用入口函数的顺序是不一定的
    if(ret!=0){
      printf("create pthread error~!\n");
      return -1;
    }
  }

  for(i=0;i<4;++i){
    pthread_join(tid[i],NULL);      //等待指定的线程退出,退出返回值 NULL
  }

  return 0;
}

在这里插入图片描述
由于判断有没有票与抢票过程不是原子性的不是一次性完成的,中间有可能被打断,因此存在其他线程也可以抢-----------------------定义互斥锁

在定义线程之前定义锁, 并对其进行初始化操作。

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

int ticket=100;

void* Scalper(void* arg)
{
  pthread_mutex_t *mutex=(pthread_mutex_t*)arg;            //定义互斥锁
  //入口函数
  while(1){
    pthread_mutex_lock(mutex);   //进行加锁操作
    usleep(10);
    
    if(ticket>0){
      printf("%p 抢到了 %d 号票\n",pthread_self(),ticket);
      ticket--;
      //抢票结束进行解锁
      pthread_mutex_unlock(mutex);

    }else{
      printf("票没了~~!\n");
      pthread_mutex_unlock(mutex);   
  //加锁后在任意有可能退出线程的地方都需要解锁
      pthread_exit(NULL);    //线程退出
    }
  }
  usleep(1);
  return NULL;
}

int main()
{
  pthread_t tid[4];   //创建四个线程
  
  pthread_mutex_t mutex;   //定义互斥锁
  pthread_mutex_init(&mutex,NULL);   //互斥锁的初始化操作

  int i=0;
  for(i=0;i<4;++i){
    int ret=pthread_create(&tid[i],NULL,Scalper,&mutex);  //将锁传给入口函数参数
    if(ret!=0){
      printf("create pthread error~!\n");
      return -1;
    }
  }

  for(i=0;i<4;++i){
    pthread_join(tid[i],NULL);      //等待指定的线程退出,退出返回值 NULL
  }

  //线程运行结束释放锁
  pthread_mutex_destroy(&mutex);
  return 0;
}

死锁

多个线程对锁资源的争抢使用不当导致程序流程卡死,无法继续向下推进的状态

(1)加锁之后没有释放就退出,导致其它线程获取不到锁资源卡死;
(2)多锁使用,加锁顺序不当,线程 1 加锁顺序为 AB,线程 2 加锁顺序为 BA

死锁产生的必要条件

(1)互斥条件
同一时间一个锁只能被一个线程获取,多个线程无法同时加同一把锁;
(2)不可剥夺条件
一个线程加的锁,只有自己可以释放,其他线程不能释放
(3)请求与保持条件
线程加 A 锁后请求 B 锁,若请求不到 B 锁也不释放 A 锁
(4)环路等待条件
线程 1 加锁 A 请求 B 锁,线程 2 加锁 B 请求 A 锁

预防死锁产生-------破坏死锁产生的必要条件

1、2 是互斥锁的要义所在,是无法进程破坏的;因此在写代码时应格外注意:
(1)多个线程间加锁顺序要保持一致:预防环路条件产生
(2)采用非阻塞加锁,若加不上锁,则把已经加锁成功的释放----破环请求与保护条件

避免死锁

(1)银行家算法--------风险控制

将系统运行状态分为:安全状态 、不安全状态
建立三张表:
资源剩余表:有没有这个资源可以分配

资源分配表:一旦请求的资源被分配了,是否会导致系统进入非安全状态(不能分配)

资源请求表:谁请求什么资源

(2)死锁检测算法

同步

通过条件控制让多线程对资源的获取更加合理

互斥只能保证安全,不一定能保证合理
同步能保持合理,不一定能保持安全

资源获取的合理:
有资源才能处理,没资源则阻塞—等有资源在唤醒处理

条件变量:
提供一个 pcb 等待队列以及阻塞和唤醒线程的接口
思想:若一个线程不满足获取资源的条件则通过阻塞接口阻塞线程
一个线程促使资源获取条件满足了,则通过唤醒接口唤醒线程
注意:条件变量本身并不知道什么时候该阻塞,什么时候唤醒,仅仅是提供了阻塞和唤醒的接口-------------具体由程序员来控制

线程应用

生产者与消费者模型

线程池

单例模式

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

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

相关文章

SpringBoot很熟?那手撕一下自定义启动器吧

一. 前言 哈喽&#xff0c;大家好&#xff0c;不知道你有没有想辉哥呢&#xff1f;我可是很想你们哟&#xff01;最近金九银十&#xff0c;又有不少小伙伴私信辉哥&#xff0c;说自己在面试时被问到SpringBoot如何自定义启动器&#xff0c;结果自己不知道该怎么回答。那么今天…

maltose-BSA 麦芽糖-牛血清白蛋白 BSA-PEG-maltose,牛血清白蛋白-PEG-麦芽糖

maltose-BSA 麦芽糖-牛血清白蛋白 BSA-PEG-maltose,牛血清白蛋白-PEG-麦芽糖 中文名称&#xff1a;麦芽糖-牛血清白蛋白 英文名称&#xff1a;maltose-BSA 纯度&#xff1a;95% 别称&#xff1a;牛血清白蛋白修饰麦芽糖&#xff0c;BSA-麦芽糖 麦芽糖-聚乙二醇-牛血清白…

设计模式日常学习(七)

6.5 状态模式 6.5.1 概述 【例】通过按钮来控制一个电梯的状态&#xff0c;一个电梯有开门状态&#xff0c;关门状态&#xff0c;停止状态&#xff0c;运行状态。每一种状态改变&#xff0c;都有可能要根据其他状态来更新处理。例如&#xff0c;如果电梯门现在处于运行时状态…

什么是副业思维,副业应该怎么做,用创业思维分析副业的可行性

副业其实也算是创业的一种&#xff0c;他考量的不仅仅是自身的知识储备&#xff0c;还有你对市场的看法&#xff0c;再加上一定的做副业的技巧&#xff0c;下面分享七个做好副业的技巧​。 1.循序渐进投入 不要大量投资。首先&#xff0c;使用相对较轻的方法来验证创业理念是否…

教程六 在Go中使用Energy创建跨平台GUI - 应用下载事件

教程-示例-文档 介绍 Energy应用下载文件时触发的下载事件和使用 我们在页面上下载文件时&#xff0c;可以对文件下载时的处理&#xff0c;例如&#xff1a;保存路径&#xff0c;下载取消&#xff0c;开始、暂停。 下面将用代码和注释&#xff0c;和简要的说明来演示 Go代码…

GUI编程--PyQt5--布局管理

文章目录布局管理布局步骤QHBoxLayout & QVBoxLayoutQFormLayoutQGridLayout布局管理 布局&#xff0c;按照一定规则&#xff0c;将子控件放入父控件 手动布局&#xff1b;绝对布局move & resize & resizeEvent布局管理器&#xff0c;实现快速布局&#xff0c;是…

08 SQL优化

上一篇文章记录了索引的创建、使用、设计&#xff0c;除了索引方面还需要注意平日对于SQL的使用&#xff0c;对SQL进行优化&#xff1b;SQL的优化是建立在索引使用的基础上 这篇笔记将从以下7个方面对SQL进行优化。 1. 插入数据 使用批量插入,避免循环单条插入 注意批量插入不…

贤鱼的刷题日常(数据结构栈学习)--P1175 表达式的转换--题目详解

&#x1f3c6;今日学习目标&#xff1a; &#x1f340;例题讲解P1175 表达式的转换 ✅创作者&#xff1a;贤鱼 ⏰预计时间&#xff1a;25分钟 &#x1f389;个人主页&#xff1a;贤鱼的个人主页 &#x1f525;专栏系列&#xff1a;c &#x1f341;贤鱼的个人社区&#xff0c;欢…

ServletConfig 和 ServletContext

1 ServletConfig 1.1 ServletConfig 介绍 ServletConfig 是 Servlet 的配置参数对象&#xff0c;在 Servlet 的规范中&#xff0c;允许为每一个 Servlet 都提供一些初始化的配置。所以&#xff0c;每个 Servlet 都有一个自己的 ServletConfig。作用&#xff1a;在 Servlet 的…

静息态fMRI中的非线性功能网络连接

在这项工作中&#xff0c;我们关注功能网络中的显式非线性关系。我们介绍了一种使用归一化互信息(NMI)计算不同大脑区域之间非线性关系的技术。我们使用模拟数据演示了我们提出的方法&#xff0c;然后将其应用到Damaraju等人先前研究过的数据集。静息状态fMRI数据包括151名精神…

玩转高并发,17年开发经验架构师,历时三年编写Java高并发三部曲

前言 5G&#xff0c;IO&#xff0c;多屏合一&#xff0c;方物互联时代来了&#xff01;太分n式、高并发、微服务架构己经成为Java后端应用的主流架构。但是对Java高并发&#xff0c;springcloudRPC底层原理、Nginx底层原理等核心知识&#xff0c;广大的Java开发同学们相对欠缺…

【踩坑汇总】CLion开启QT编程

一下全部内容全都是大佬lht的经验&#xff0c;我只是记录一下给大家。 问题&#xff1a;Qt5Config.cmake找不到 解决办法&#xff1a; set(CMAKE_PREFIX_PATH "E:/Qt/Qt5.12.11/5.12.11/mingw73_64/lib/cmake/Qt5") 找到Qt5Config.cmake路径&#xff0c;添加上面这…

东南亚LazadaShopee文具类目好做吗?一文带你了解各国热销及需求品类

在东南亚&#xff0c;消费者刚刚经历完双11独有的“速度与激情”——11月11日00&#xff1a;11&#xff0c;开售11分钟&#xff0c;Lazada平台的销售额相比日销暴涨124倍&#xff1b;早上8&#xff1a;17&#xff0c;第一单跨越重洋的中国跨境商品就已成功送达签收。 东南亚&a…

Listen,Attend,and Spell(LAS)——李宏毅人类语言处理学习笔记3

Listen Encoder目标&#xff1a; 去掉noises&#xff0c;提取出相关信息 encoder有很多做法&#xff1a; CNN见文章&#xff1a;CNN-卷积神经网络 self-attention见文章self-attention Pyramid RNN将两个结合&#xff0c;然后送到下一层。Pooling over time则是两个中取一…

代谢组学文献分享:地中海饮食、血浆代谢组和心血管疾病风险

​全球三分之一的死亡由心血管疾病造成&#xff0c;2015-2020年美国膳食指南建议&#xff0c;地中海饮食是预防心血管疾病的一项重要且具有成本效益的战略措施。代谢组学文献分享&#xff0c;发表在期刊European Heart Journ-al&#xff08;IF 22.637&#xff09;上题目为“Th…

网络协议

网络通信协议&#xff1a;计算机网络中实现通信必须有一些约定&#xff0c;即通信协议&#xff0c;对速率、传输代码、代 码结构、传输控制步骤、出错控制等制定标准。 问题&#xff1a;网络协议太复杂&#xff1a;计算机网络通信涉及内容很多&#xff0c;比如指定源地址和目标…

【多标签, 极限的多标签算法】评价指标梳理

具体研究多标签和极限多标签 (XML) 的时候, 合理使用评价指标是关键. 最近在研究极限多标签算法的时候发现了它和传统多标签算法的评价指标是有异的, 而且我曾经积累的传统多标签评价指标也没有一个系统的体系 (很混乱). 于是写下本文用于自我总结. 查询目录<想看什么直接通…

语音识别翻译怎么做?这些方法值得收藏

随着网络的不断发展&#xff0c;我们可以通过网络与世界各地的网友进行聊天。小伙伴们平时会和外国人交流吗&#xff1f;如果是文字聊天&#xff0c;我们看不懂的时候&#xff0c;还可以直接复制文字进行翻译。那如果外国网友发了段语音&#xff0c;结果我们大部分内容听不懂的…

电力行业人员定位管理解决方案之智能巡检

智能巡检引入大数据分析理念&#xff0c;通过数字化技术实现对生产现场各关键要素的全面感知和实时互联&#xff0c;形成项目现场“数据一个库、监管一张网、管理一条线“。 在信息技术高速发展的今天&#xff0c;传统人工巡视、手工纸介质记录的工作方式已经无法满足电力设备巡…

第7章 博客文章的前端渲染显示

说实话本人通过Vue页面实现前端对后端数据的渲染显示也是初学咋练&#xff0c;但后端实现本人却是老鸟&#xff0c;对于后端开发者来说如果&#xff0c;渲染显示的软件是浏览器&#xff0c;除非团队中有Vue方面的大拿&#xff0c;不管是PC浏览器还是移动PC浏览器&#xff0c;Ra…