Linux系统编程5(线程概念详解)

线程同进程一样都是OS中非常重要的部分,线程的应用场景非常的广泛,试想我们使用的视频软件,在网络不是很好的情况下,通常会采取下载的方式,现在你很想立即观看,又想下载,于是你点击了下载并且在线观看。学过进程的你会不会想,视频软件运行后在OS内形成一个进程,有一个执行流,但下载和在线观看是两件事情,这两件事情是如何同时进行的呢?你可能会想到CPU的时间片轮转,不过曾经提到过的时间片轮转是针对进程间的切换的,下载和在线观看这两件事本身处于同一个进程内完成,你可能还会想到在这个进程内创建一个子进程,主进程负责播放,子进程负责下载,这确实是一个解决问题的方法,但是创建一个进程所带来的开销是不小的。本篇文章将会介绍另一种更加轻便的解决方案——线程,同时我们需要重新理解CPU时间片轮转的调度单位

目录

什么是线程

深入理解页表 

理解进程和线程 

实践线程操作 

线程终止

线程等待 

分离线程 

线程取消 

TCB

线程的优缺点 

优点

缺点 

C++提供的线程库 


什么是线程

按照课本上的定义,线程就是进程内部的执行流,有多个执行流就意味着一个进程可以同时进行多个操作,比如视频软件,同时具备播放视频和下载视频的功能,如果只有一个执行流,那么在播放视频时就不能同时下载视频,因为播放视频和下载视频的代码是不同的

以前我们一直认为进程是CPU的调度单位,现在我们要改变这个看法,被CPU调度意味着被CPU执行,也就是一个执行流,一个进程里可以有多个线程,线程才是CPU的调度单位。所谓的调度单位就是CPU时间片轮转时的切换单位,以前我们解释CPU时间片轮转时说的是每个进程都被分配一定的CPU执行时间,到达时间,CPU会强制切换到下一个进程,以保证每个进程都能够被执行

此时,通过线程的概念能得知,CPU时间片轮转切换的并不是进程,而是线程。但上面的话并没有说错,一是创建一个进程时,默认只有一个执行流,也就是只有一个线程,时间片轮转时可以认为是切换进程。二是在后面我们将学习到Linux其实并没有线程的概念,所谓的线程在Linux中是轻量级进程

有些懵没有关系,后面会一一解释原因

现在线程的概念先放到一边,我们接下来再次回顾曾经学习过的进程地址空间

深入理解页表 

这是笔者曾经多次提到过的进程地址空间映射图,并且说过虚拟地址空间和物理内存之间一一映射,那么大家有没有思考过这么一个问题,假设虚拟地址空间有4G大小,物理内存也是4G大,而页表是虚拟地址空间和物理地址空间的一一映射,这意味着页表自身得有8G大小的空间才能够满足虚拟地址空间和物理内存之间一一映射,要知道,页表可也得加载到内存中才能让CPU执行,照这样的映射法,物理内存连一个页表都存不了,更何况4G物理内存空间还得留1G给OS呢

可想而知,页表的映射不会像哈希表那样一一对应,要明白页表的真实构造,我们就得从物理内存的划分开始

 ​​​

实际上物理内存是按4kb为单位进行划分的,每个大小单位被称为页框,大家知道磁盘往内存中加载数据时就是以4kb大小为单位,正好能够加载到物理内存的页框中,这看似巧妙的背后是前人无数日夜的精心设计

但是这好像并没有说明页表的真实构造,别急,接着往下看

真实的页表并不是只有一张,页表里存储的也不是虚拟地址和物理地址的一一对应,页表里正真存储的是物理内存中每个页框的起始地址,一张页表里只存储指定数量的页框,整个物理内存的页框被多张页表存储着

这多张页表被页目录记录着,通过页目录可以找到每一张页表,到这里,页表的整体结构就出来了,可见,当初我们刚了解页表时,进行了很大程度的简化。但是这就结束了吗?笔者只是把页表真实的结构给描绘出来,但是并没有解释现在的页表是如何进行映射的 

 

上图是虚拟内存中的一个虚拟地址,接下来我们刨析这个虚拟地址如何通过页表最终映射到物理内存 

虚拟地址映射到物理内存的方法就在地址本身上,通过虚拟地址的前10位可以到页目录中找到该地址对应在哪个页表,找到具体的页表之后,虚拟地址的中间10位标识着该地址在物理内存的哪个页框里,找到具体的页框之后,那么最后12位想必大家已经猜出来了

最后12位正是页框内的偏移地址,因为一个页框大小就是4kb,要想在某个页框内准确定位,就要知道该页框的起始地址以及在该页框内的偏移地址。至于对不对,咱们验证一下

一个地址的大小是4字节,2的12次方是4096,4096 * 4字节 = 4kb,所以验证正确

如上,真实的页表映射结构就展现在我们眼前,笔者这里并不是心血来潮讲一下页表,通过上述的过程大家能感受到地址空间是进程接触并使用资源的窗口,页表则决定了,进程拥有哪些资源,只有页表映射到的物理内存,进程才能够访问,那么通过地址空间+页表映射进行资源划分,就可以对一个进程所用的资源进行分类

理解进程和线程 

现在回到对线程的讲解上,前面说到过线程是进程内部的执行流,一个进程可以拥有多个线程,如下图,这些线程通过使用共同的地址空间和页表从而共享进程的资源,这意味着一个进程里的多个线程共享该进程的资源

前面还提到过,CPU的基本调度单位是线程,被CPU调度执行,那就得有上下文信息,那么线程就要保存好自己的上下文信息,当被CPU切换执行时,可以将上下文信息重新载入到CPU的寄存器中。线程在共享进程资源的同时也会产生自己的执行数据,也是需要保存起来的。线程是CPU调度的基本单位,这就意味着系统中会存在大量的线程等待被CPU调用,根据以往的经验,存在大量的线程时,OS要有序将其管理起来,就得给线程设计一种数据类型,设计方法还是多次提到过的先描述,再组织

给线程设计数据类型就要考虑线程的id号在系统中唯一,同时要能存储上下文信息,线程在被CPU调度时,要有自己的状态信息,同时在执行过程中要有自己的栈结构, 并且线程共享进程资源,那么文件描述符表什么的也要有,越往下举例,就能明显感受到这不就是当初学习进程时,进程的结构里所包含的内容吗?

可以发现进程结构和线程结构大量的内容都是重叠的,如果进程和线程两种结构同时存在系统中,就会造成大量的冗余,而Linux是一个非常注重效率的OS,于是聪明的Linux设计者决定不为线程设计一个独立的结构,而是采用了轻量级进程结构,也就是说在Linux系统中,进程和线程实际上使用的是同一种结构

这一点与windows有很大的不同,windows就为线程设计了一个独立的结构,这也体现了两种OS各自的设计哲学

如何理解线程就是轻量级进程呢?如何理解现在的进程概念呢?

曾经我们认为一个task_struct就是一个进程,一个task_struct有一个执行流,并且记录着该执行信息的执行状态。现在学了线程,知道进程和线程共同使用task_struct结构,对于每个进程或线程,内核都会为其分配一个唯一的task_struct结构,现在的task_struct是一种轻量级进程,也就是说一个进程里可能含有多个task_struct,不能再将一个task_struct理解成一个进程。但这并不是说曾经学的就是错误的,曾经创建一个进程,默认有一个执行流,也就是有一个主线程,该主线程是创建进程本身的执行流,task_struct就是这个主线程的结构,故而也可以将task_struct理解成进程本身,但是多线程后,有多个task_struct,再按照以前的方法理解进程就显得不严谨了

假设现在一个进程创建了三个线程,那么会有几个task_struct呢?

如果一个进程创建了三个线程,那么通常会有四个task_struct结构,在Linux中,每个进程都有一个主线程,也就是创建该进程的线程,主线程有一个对应的task_struct结构,对于每个创建的线程,也会有一个对应的task_struct结构。 故而,对于一个进程而言,如果额外创建了三个线程,那么会有一个主线程的task_struct结构,以及三个子线程的task_struct结构,共计四个task_struct结构,这四个task_struct结构共同构成了该进程的线程组成部分

站在CPU的角度上,曾经时间片轮转时切换task_struct就是切换一个进程,现在CPU时间片轮转切换一个task_struct是切换进程的一个分支,如果这个进程只有一个主线程,那就是切换进程本身

总而言之,现在一个进程有多个执行流,进程的概念不能局限于曾经只有一个执行流的task_struct,而是一个拥有多个task_struct的承担分配系统资源的基本实体

实践线程操作 

说了这么多,咱们连线程长什么样子都不知道,接下来咱们通过实践来感受线程的魅力

不过在动手敲代码之前,需要明确一些事情,因为用轻量级进程来表示线程是Linux系统独特的线程处理方式。虽然这能很大提高效率,但是也带来了不通用的麻烦,很多OS,包括OS的理论基础上都是有线程这个概念的,因此并不通用Linux的轻量级进程,大家都在使用线程接口,而你Linux搞特殊提供轻量级进程接口,大家是不认的,为了解决这个问题,Linux工程师就将轻量级进程接口进行封装,适配成大家都通用的线程接口

这意味着,我们在使用Linux线程接口时,要在编译时带上线程动态库即选项 -l pthread

创建一个线程是通过接口

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

头文件:pthread.h   
参数
thread:返回线程ID (输出型参数)
attr:设置线程的属性,attr为NULL表示使用默认属性
start_routine:是个函数地址,线程启动后要执行的函数
arg:传给线程启动函数的参数
返回值:成功返回0;失败返回错误码

第一个参数是一个输出型参数,我们在主函数里创建一个pthread_t类型的变量,将其的地址传过去,创建线程后,会把该线程的id写入到这个pthread_t类型变量里

我们目前不需要关心 att r这个参数,可以看到第三个参数是一个函数指针,其所指向的函数就是创建一个线程后,该线程去执行的任务

第四个参数是对第三个参数的补充,在我们编写线程要执行的函数时,有时是需要外部给这个函数传参的,那个这个函数就会默认有一个void* 类型的参数,这个参数就是通过pthread_create的第四个参数传递过去的

下面看一个线程代码示例

#include<iostream>
#include<unistd.h>
#include<pthread.h>
#include<cstring>
#include<cstdlib>
#include<cstdio>using namespace std;void* start_routine(void * arg)
{while(true){printf("%s\n", (char*)arg);sleep(1);}
}int main()
{pthread_t thread_id;char buff[64];snprintf(buff, sizeof(buff), "我是新创建的线程,我正在运行");pthread_create(&thread_id, nullptr, start_routine, (void*)buff);int counter = 10;while(counter--){printf("我是主线程,运行倒计时:%d\n", counter);sleep(1);}return 0;
}

这个示例可以看到,真的有两个执行流同时在跑

通过命令ps -aL可以查看所有进程内的线程,接下来我们让两个线程不间断运行,然后查看这两个线程的相关信息

可以发现,当test程序跑起来后,出现了两个test线程,这两个线程的PID是相同的,说明这两个线程来自同一个进程,不过两个线程的LWP不同,LWP(light weight process,即轻量级进程)LWP就是所谓的线程ID了,并且第一个线程的PID和LWP相同,这说明该线程是主线程,CPU在调度时,是以LWP为标识,表示一个特定的执行流

上面只是创建单个线程,那么如何同时创建多个线程呢?

看下面的demo,我们一次创建10个线程,并且不停打印他们的序号

#include<iostream>
#include<unistd.h>
#include<pthread.h>
#include<cstring>
#include<cstdlib>using namespace std;#define MAX 10void* _start_test(void* arg){while(true){sleep(1);cout << (char*)arg << endl;}return nullptr;
}int main(){for (int i = 0; i<MAX; i++){pthread_t tid;char buff[64];snprintf(buff, sizeof(buff), "this is %d thread", i);pthread_create(&tid, nullptr, _start_test, buff);}while(true){sleep(1);cout << "我是主线程"<<endl;}return 0;
}

 

当执行结果出来后,完全超出了我们的预期,我们本想这10个线程,各自打印各自的序号,可是结果每个线程都打印序号9

出现这种情况的原因是线程被创建后的执行顺序是不确定的,当第一个被创建的进程还没来得及执行它的start_routine函数时,主线程就已经把所有的线程都创建完毕了,buff是在循环里被被创建的,出了循环后就被销毁,然后再次创建,因为都是在同一个栈里,所以每次buff的地址都不变,且buff的值不断被覆写,直到最后一个线程创建完毕,buff的值被覆写为序号9

此时循环退出,buff也被销毁了,但是由于main函数这个栈还在,也没有开其他的栈,因此原先buff指向的空间并没有被清理,导致所有的线程都打印最后一次覆写buff的内容,通过这个demo,可得知线程除了独自的PCB,独自的上下文结构,独自的栈结构,其他几乎所有内容都是共享的

每一个线程都有自己独立的栈,这是因为一个线程在执行时,可能会调用各种函数,因此需要一个独立的栈,这个栈里的内容不与其他线程共享

线程终止

会创建线程之后,自然而然的会想到,线程如何终止,导致线程终止的原因有很多

1.执行完start_routine()后,线程会自动return结束

2.使用pthread_exit()来终止当前线程,但是要注意,不要习惯性的使用exit()来终止线程,exit()是用来终止进程的,进程终止,该进程内所有的线程都会终止

3.某个线程执行过程中,出现错误,触发OS检查,会给当前线程的进程发送信号,进程收到信号会终止,该进程内其他所有线程都会终止

4. 一个线程可以调用pthread_ cancel()终止同一进程中的另一个线程

线程等待 

同进程一样,线程结束后其所申请的各种资源都是需要被回收的,不然会产生类似僵尸进程一样的问题,线程等待使用函数pthread_join()

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

第一个参数就是被等待的线程id,第二个参数是获取该线程的返回值的,还记得start_routine有一个void* 返回值吗?这个返回值就是通过pthread_join获取的

注意:OS维护的是轻量级进程PCB,因为Linux特殊的线程方案,可以说没有线程概念。程序员日常使用习惯了线程接口,因此Linux提供了线程库,线程库负责线程接口与轻量级进程接口之间的转换,以及维护用户通过接口创建好的线程

分离线程 

默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄漏
如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源
使用接口:int pthread_detach(pthread_t thread);
可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离
pthread_detach(pthread_self());   pthread_self()获取自己的线程id

 需要注意的是,一旦一个线程已经处于分离状态,那么该线程就不能被等待

线程取消 

线程取消也就是当线程跑起来后,我们通过主线程或者其他线程可以取消这个线程继续运行

也可以自己取消自己

int pthread_cancel(pthread_t thread);

返回值:成功返回0;失败返回错误码

注意:只有当该进程运行起来,有自己的线程ID时才可以被取消

TCB

PCB是Linux内核用来管理轻量级进程的内核,因为Linux没有线程的概念,程序员要使用线程的接口,因此要通过线程库进行转接,那么程序员每申请一个线程,线程库就得维护好这个线程和轻量级进程进行转换,那么TCB就是线程库维护线程的结构

  

由图中可以得知,我们接收的所谓的线程id值其实就是库中维护的该线程TCB的起始地址 

线程的优缺点 

优点

线程的使用能非常大程度上发挥多核CPU的实力,并且创建多个线程比创建多个进程的开销要小的多,为什么呢?

如果CPU执行时,要切换一个进程,那么要切换的内容至少包含页表,虚拟地址空间,PCB,上下文数据

而切换一个线程,那么只需要切换PCB,上下文数据等主要内容

CPU在执行一个进程时,会在寄存器中缓存该进程的很多热点数据,例如虚拟地址空间,页表等,一旦切换进程,这些热点数据要全部重新加载,而切换线程,这些数据不需要动

缺点 

在运行计算密集型程序时,线程需要不停的计算,持续占有CPU,切换到其它线程的时间就会延长,导致效率低下

使用多线程编程会有互斥和同步等问题,程序的编写和维护成本很高

C++提供的线程库 

虽说Linux提供了线程库,但是Linux的线程接口和Windows下的线程接口很多都是不同的,这就导致程序的可移植性很低, C++11之后,在语言层面上对Linux和windows平台下的线程接口再进行一次封装。如此以来,用C++线程库编写的多线程程序可以同时在这两个OS平台下执行,代码的可移植性大大提高

下面的demo简单演示了如何使用C++提供的线程库,这部分属于C++的知识了,笔者将在C++专栏中介绍其详细使用方法

#include<thread>
#include<iostream>
#include<unistd.h>using namespace std;void* start_routine()
{int counter = 10;while(counter--){sleep(1);cout << "我是新创建的线程,运行倒计时:" << counter <<endl;}return nullptr;
}int main()
{//创建一个线程,并把执行函数传递过去thread t1(start_routine);cout << "我是主线程" <<endl;//主线程阻塞等待回收子线程t1.join();cout << "线程回收完毕,准备退出"<<endl;return 0;
}

文章的最后,大家可以尝试自己模仿C++的线程库,对Linux的线程库再进行一次封装 

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

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

相关文章

【多线程】线程间通信及状态

文章目录 1. 线程间的通信1.1 wait和notify1.2 notify随机唤醒1.3 notifyAll()1.4 join() 2. 线程间的状态3. 验证线程的状态3.1 验证NEW、RUNNABLE、TERMINATED3.2 验证WAITING3.3 验证TIMED-WAITING3.4 验证BLOCKED 4. 面试题&#xff1a;wait和sleep对比 1. 线程间的通信 1…

Linux系统下的zabbix监控平台(单机安装服务)

目录 一、zabbix的基本概述 二、zabbix构成 1.server 2.web页面 3.数据库 4.proxy 5.Agent 三、监控对象 四、zabbix的日常术语 1.主机(host) 2.主机组(host group) 3.监控项(item) 4.触发器(trigger) 5.事件&#xff08;event&#xff09; 6.动作&#xff08;a…

JUC并发编程--------CAS、Atomic原子操作

什么是原子操作&#xff1f;如何实现原子操作&#xff1f; 什么是原子性&#xff1f; 事务的一大特性就是原子性&#xff08;事务具有ACID四大特性&#xff09;&#xff0c;一个事务包含多个操作&#xff0c;这些操作要么全部执行&#xff0c;要么全都不执行 并发里的原子性…

ESP32之LEDC(PWM信号的输出)

一、PWM信号简介 PWM&#xff1a;脉冲宽度调制&#xff0c;简称脉宽调制频率(f)&#xff1a;一秒钟PWM有多少个周期(单位Hz)周期(T)&#xff1a;一个周期的时间占空比(duty)&#xff1a;在一个脉冲周期内&#xff0c;高电平的时间与整个周期时间的比例脉宽时间&#xff1a;一个…

Java学习笔记之----I/O(输入/输出)一

在变量、数组和对象中存储的数据是暂时存在的&#xff0c;程序结束后它们就会丢失。想要永久地存储程序创建的数据&#xff0c;就需要将其保存在磁盘文件中(就是保存在电脑的C盘或D盘中&#xff09;&#xff0c;而只有数据存储起来才可以在其他程序中使用它们。Java的I/O技术可…

机器学习:可解释学习

文章目录 可解释学习为什么需要可解释机器学习可解释还是强模型可解释学习的目标可解释机器学习Local ExplanationGlobal Explanation 可解释学习 神马汉斯&#xff0c;只有在有人看的时候能够答对。 为什么需要可解释机器学习 贷款&#xff0c;医疗需要给出理由&#xff0c;让…

MongoDB 会丢数据吗? 在次补刀MongoDB 双机热备

开头还是介绍一下群&#xff0c;如果感兴趣PolarDB ,MongoDB ,MySQL ,PostgreSQL ,Redis &#xff0c;Oracle ,Oceanbase 等有问题&#xff0c;有需求都可以加群群内有各大数据库行业大咖&#xff0c;CTO&#xff0c;可以解决你的问题。加群请加微信号 liuaustin3 &#xff08;…

makefile开发应用程序的一个通用模板

下面是一个通用的 Makefile 模板&#xff0c;用于开发 C 语言应用程序&#xff1a; # 编译器设置 CC gcc CFLAGS -Wall -Wextra -stdc99# 可执行文件名 TARGET your_program# 源文件和对象文件 SRCS main.c file1.c file2.c OBJS $(SRCS:.c.o)# 默认目标 all: $(TARGET)#…

【C++】异常处理详解

本篇文章重点将会对C中的异常的相关处理操作进行详解。希望本篇文章的内容会对你有所帮助。 目录 一、C语言的异常处理 二、C异常 2、1 异常概念 2、2 异常的使用 2、3 异常类 2、4 异常的重新抛出 三、异常的安全与规范 3、1 异常的安全 3、2 异常的规范 四、异常的优缺点 &am…

CVE-2023-25157:GeoServer OGC Filter SQL注入漏洞复现

CVE-2023-25157&#xff1a;GeoServer OGC Filter SQL注入漏洞复现 前言 本次测试仅供学习使用&#xff0c;如若非法他用&#xff0c;与本文作者无关&#xff0c;需自行负责&#xff01;&#xff01;&#xff01; 一.GeoServer简介 GeoServer 是用 Java 编写的开源软件服务…

界面控件Telerik UI for WPF——Windows 11主题精简模式提升应用体验

Telerik UI for WPF拥有超过100个控件来创建美观、高性能的桌面应用程序&#xff0c;同时还能快速构建企业级办公WPF应用程序。Telerik UI for WPF支持MVVM、触摸等&#xff0c;创建的应用程序可靠且结构良好&#xff0c;非常容易维护&#xff0c;其直观的API将无缝地集成Visua…

【力扣】416. 分割等和子集 <动态规划、回溯>

【力扣】416. 分割等和子集 给你一个 只包含正整数的非空数组 nums 。请你判断是否可以将这个数组分割成两个子集&#xff0c;使得两个子集的元素和相等。 示例 1&#xff1a; 输入&#xff1a;nums [1,5,11,5] 输出&#xff1a;true 解释&#xff1a;数组可以分割成 [1, 5,…

供热管网安全运行监测,提升供热管网安全性能

城市管网是城市的“生命线”之一&#xff0c;是城市赖以生存和发展的基础&#xff0c;在城市基础设施高质量发展中发挥着重要作用。供热管网作为城市生命线中连接供热管线与热用户的桥梁&#xff0c;担负着向企业和居民用户直接供热的重要职责。随着城市热力需求的急剧增加&…

组相联cache如何快速实现cache line eviction并使用PMU events验证

如何快速实现cache line eviction 一&#xff0c;什么是cache hit、miss、linefill、evict &#xff1f;1.1 如果要程序员分别制造出cache hit、miss、linefill、evict这四种场景&#xff0c;该怎么做&#xff1f; 二&#xff0c;实现cache line eviction的方法1.1 直接填充法3…

Android——基本控件(下)(二十)

1. 树型组件&#xff1a;ExpandableListView 1.1 知识点 &#xff08;1&#xff09;掌握树型组件的定义&#xff1b; &#xff08;2&#xff09;可以使用事件对树操作进行监听。 2. 具体内容 既然这个组件可以完成列表的功能&#xff0c;肯定就需要一个可以操作的数据&…

vue从零开始学习

npm install慢解决方法:删掉nodel_modules。 5.0.3:表示安装指定的5.0.3版本 ~5.0.3:表示安装5.0X中最新的版本 ^5.0.3: 表示安装5.x.x中最新的版本。 yarn的优点: 1.速度快,可以并行安装 2.安装版本统一 项目搭建: 安装nodejs查看node版本:node -v安装vue clie : np…

swaggo的一点小理解

如有错误&#xff0c;希望指出&#xff0c;谢谢&#xff01; 很低级的概念不清&#xff0c;大佬嘴下留情。 1.关于swag的注释 我的理解是这些注释是专门提供给Swagger UI界面测试使用的&#xff0c;根据注释内容告诉swag文档这个函数应该有哪些参数&#xff0c;从什么路由走&…

HarmonyOS/OpenHarmony(Stage模型)应用开发单一手势(二)

三、拖动手势&#xff08;PanGesture&#xff09; .PanGestureOptions(value?:{ fingers?:number; direction?:PanDirection; distance?:number}) 拖动手势用于触发拖动手势事件&#xff0c;滑动达到最小滑动距离&#xff08;默认值为5vp&#xff09;时拖动手势识别成功&am…

关于已经安装了TorchCRF,导入时却提示“ModuleNotFoundError: No module named ‘torchcrf‘”的解决办法

应用python时&#xff0c;想导入torchcrf库 from torchcrf import CRF 但系统提示&#xff1a;ModuleNotFoundError: No module named torchcrf 在命令提示符里输入“pip list”检查已安装库&#xff0c;发现torchcrf已经安装 搞了半天&#xff0c;发现是大小写的问题&#x…

Locked勒索病毒:最新变种.locked袭击了您的计算机?

导言&#xff1a; 在今天的数字时代&#xff0c;勒索病毒已经不再是仅仅让数据变得不可访问的小威胁。 .locked 勒索病毒&#xff0c;作为其中的一种&#xff0c;以其高度复杂的加密算法和迅速变化的攻击手法而备受恶意分子喜爱。本文91数据恢复将带您深入了解 .locked 勒索病毒…