做网站的那些个人工作室/今日最新消息新闻
目录
1、线程与进程的关系
2、线程的优缺点
3、创建线程
4、查看启动的线程
5、验证线程是共享地址空间的
6、pthread_create的重要形参
6.1 线程id
6.2 线程实参
7、线程等待
8、线程退出
9、线程取消
10、线程tcb
10.1 线程栈
11、创建多线程
12、__thread
13、线程分离
结语
前言:
线程是操作系统进行调度的基本单位,他属于进程的子集。在Linux下,通过实现轻量化进程来实现线程,因此线程具有进程的相关特性,比如线程必须有自己的代码资源,有属于自己独立的数据空间,并且同一个进程下的线程所看到的地址空间是属于该进程的,因为创建线程实际上就是在该进程下创建task_struct结构体(该结构体的作用是方便操作系统对该执行流的调度),这些task_struct结构体跟进程共用空间资源,只不过线程可以在单一进程执行流的基础上实现多执行流并发式的运行代码,以至于提高cpu的效率。
1、线程与进程的关系
说到线程就离不开进程的概念,因为线程是在进程的基础上实现的,多线程是底层就是创建了多个task_struct结构体作为进程的执行分支,但是他们依然是共用进程的数据资源,线程示意图如下:
当系统里创建一个进程,则系统需要给该进程分配新的地址空间、页表、物理内存等等空间资源,所以说进程是系统分配资源的实体。但是当系统里有了新的线程则不会给线程分配新的空间资源,而是给让线程使用进程的空间资源。
线程的独立部分:
线程虽然和进程共用地址空间,但是线程也有自己独立的部分,比如:线程ID, 保存上下文的寄存器,线程栈,errno,block信号集,调度优先级。
2、线程的优缺点
线程的优点:
1、当我们需要并发执行代码时,创建一个线程的工作比创建一个进程的工作要小得多。
2、当cpu切换PCB时,切换线程的效率比切换进程的效率略高。
3、线程所占用的资源小于进程。
4、线程之间的通信代价比进程的要小。
5、提高程序的并发性。
线程的缺点:
1、若单个线程收到信号退出,则会导致整个进程都退出。
2、多线程访问共同资源时是不受保护的,会导致意料之外的错误。
3、编写多线程的难度很高。
4、若单个线程因为异常崩溃,则会导致整个进程崩溃。
3、创建线程
创建线程需要用到的接口如下:
#include <pthread.h>int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine) (void *), void *arg);
//thread是一个执行类型为pthread_t的变量的指针
//attr是一个指针,他指向的结构体包含新线程的各种属性,设为nullptr则表示采用默认属性
//start_routine是新线程要执行的函数,他接收一个void*,返回值一个void*
//arg表示新线程要执行的函数的实参
创建线程前需要先定义一个类型为pthread_t的变量作为实参传递给函数pthread_create,该变量的作用是让用户可以通过他找到对应的线程,创建线程的代码如下:
#include <iostream>
#include <pthread.h>
#include <unistd.h>using namespace std;void *threadRun(void* args)
{while(1){cout << "子线程: " << getpid() << endl;sleep(1);}return nullptr;
}int main()
{pthread_t tid;//先定义一个pthread_t类型的变量//该函数调用完成后会赋予tid新的值,表示该线程的idpthread_create(&tid, nullptr, threadRun, nullptr);while(1){cout << "主线程: " << getpid() << endl;sleep(1);}
}
测试结果:
注意:使用线程的接口时要在编译的时候要手动链接pthread库,如下图:
4、查看启动的线程
在Linux下,使用指令:ps -aL,就可以查看用户启动的线程了。如下图:
LWP表示轻量级进程的pid,即线程的pid,LWP是给系统调度线程专门设置的标识符。
5、验证线程是共享地址空间的
定义一个全局变量,若所有线程只能看到唯一一份全局变量,那么就可以证明进程下的所有线程用的是同一个地址空间,代码如下:
#include <iostream>
#include <pthread.h>
#include <unistd.h>using namespace std;int g_val = 10;
// 新线程
void *threadRoutine(void *args)
{while (true){printf("子线程 pid: %d, g_val: %d, \&g_val: 0x%p\n", getpid(), g_val, &g_val);sleep(1);}
}int main()
{pthread_t tid;pthread_create(&tid, nullptr, threadRoutine, nullptr); while (true){printf("主线程 pid: %d, g_val: %d,\&g_val: 0x%p,\n", getpid(), g_val, &g_val);sleep(1);g_val++;}return 0;
}
运行结果:
从结果可以看到,哪怕主线程对全局变量进行更改,其他线程拿到的值是更改后的值,若是父子进程关系,则另一方会发生写时拷贝,线程之间没有这么做,说明线程是共享地址空间的。
6、pthread_create的重要形参
6.1 线程id
线程有两个标识符,一个是LWP,是给系统看的,另一个是线程id,是给用户看的。线程id就是创建线程时定义的pthread_t类型的变量,该变量作为pthread_create的输出型参数,在调用完pthread_create后该变量保存的就是线程id了。
查看线程id的代码如下:
#include <iostream>
#include <pthread.h>
#include <unistd.h>using namespace std;int g_val = 10;
// 新线程
void *threadRoutine(void *args)
{
}int main()
{pthread_t tid;pthread_create(&tid, nullptr, threadRoutine, nullptr); while (true){printf("主线程 pid: %d, g_val: %d,\&g_val: 0x%p,子线程id:%p \n", getpid(), g_val, &g_val,tid);sleep(1);g_val++;}return 0;
}
运行结果:
从测试结果发现,线程id实际上就是一串地址,这个地址就是线程在地址空间内的映射,间接说明了线程的管理是在用户空间内的进行,并不是由操作系统像管理进程PCB一般在内核空间进行,具体看下文线程tcb。
6.2 线程实参
线程的任务就是执行pthread_create的函数指针,并且该函数具有一个void*的形参,那么如何使用该void*的形参呢?
测试代码如下:
#include <iostream>
#include <pthread.h>
#include <unistd.h>using namespace std;// 新线程
void *threadRoutine(void *args)
{char *name = static_cast<char *>(args);//需要强转while (true){cout << name << endl;sleep(1);}
}int main()
{pthread_t tid;pthread_create(&tid, nullptr, threadRoutine, (void *)"线程1");//需要强转while (true){}return 0;
}
运行结果:
7、线程等待
子线程退出时主线程也要对其进行等待,等待的原因和父子进程一样,防止内存泄漏和回收退出信息,若不等待线程,则线程的tast_struct会一直存在,会造成不必要的资源浪费,进行线程等待的接口介绍如下:
#include <pthread.h>int pthread_join(pthread_t thread, void **retval);
//thread表示要等待的线程id
//retval是一个二级指针,是一个输出型参数,目的是拿到线程返回的void*
线程等待的测试代码如下:
#include <iostream>
#include <pthread.h>
#include <unistd.h>using namespace std;// 新线程
void *threadRoutine(void *args)
{int count = 5;while (count){count--;sleep(1);}return (void*)1;
}int main()
{pthread_t tid;pthread_create(&tid, nullptr, threadRoutine, nullptr);void* retval;pthread_join(tid,&retval);//等待子线程cout<<"线程等待成功:"<<(long long)retval<<endl;return 0;
}
运行结果:
由于线程执行的函数虽然可以返回一个void*的变量,但是我们却没有办法接收该返回值,因为这不是一个简单的函数调用。所以只能通过调用pthread_join,然后传递一个二级指针给他,pthread_join就可以通过输出型参数把void*变量给带出来。
8、线程退出
进程退出常常用exit函数,只要一个进程调用了exit函数,则该进程就直接结束了。但是若想仅仅退出一个线程,则不能用exit,因为当一个线程用exit退出,就会把整个进程退出。线程退出有专门的退出函数,该函数介绍如下:
#include <pthread.h>void pthread_exit(void *retval);
//该函数会退出当前线程,并且返回一个void*变量
线程退出测试代码如下:
#include <iostream>
#include <pthread.h>
#include <unistd.h>using namespace std;// 新线程
void *threadRoutine(void *args)
{int count = 5;while (count){count--;sleep(1);}//return (void*)1;pthread_exit((void*)100);
}int main()
{pthread_t tid;pthread_create(&tid, nullptr, threadRoutine, nullptr);void* retval;pthread_join(tid,&retval);cout<<"线程等待成功:"<<(long long)retval<<endl;return 0;
}
运行结果:
9、线程取消
可以调用函数pthread_cancel可以在线程退出前取消该线程,该函数介绍如下:
#include <pthread.h>int pthread_cancel(pthread_t thread);
//thread表示要取消的线程
线程取消测试代码如下:
#include <iostream>
#include <pthread.h>
#include <unistd.h>using namespace std;// 新线程
void *threadRoutine(void *args)
{int count = 5;while (count){count--;sleep(1);}pthread_exit((void*)100);
}int main()
{pthread_t tid;pthread_create(&tid, nullptr, threadRoutine, nullptr);pthread_cancel(tid);void* retval;pthread_join(tid,&retval);cout<<"线程等待成功:"<<(long long)retval<<endl;return 0;
}
运行结果:
如果一个线程被取消,则该线程的退出码为-1。
10、线程tcb
上文讲到线程id是给用户看的,通过上述代码打印线程id,可以观察到线程id实际上就是一串虚拟地址,线程id的值如下:
分析id的地址,可以发现该地址数属于进程地址空间的共享区内,说明线程本身就是存储在用户空间内的,但是我们自己并没有对线程做任何管理,那么线程是如何被管理的呢?可以从函数pthread_create得知,当我们调用pthread_create后,该函数就会返回一个线程id给到我们,说明该函数内部会自己维护线程,而该函数的实现是存储在线程库(pthread.so)里的,而线程库会在程序运行起来时加载到内存并映射在共享区内,所有可以得出一个结论:线程由线程库维护,并映射在地址空间的共享区内。
具体示意图如下:
从上图可以发现,tcb就是管理线程的结构体,线程库以维护tcb从而维护线程,而tid就是线程id,他就是tcb的首地址,这也就很好的解释了为什么可以通过线程id找到对应的线程了,这个地址是线程库为用户申请,也就是用户调用函数pthread_create后所得到的地址。并且不同的进程创建的线程都会被线程库在内存中统一管理,只是这些线程会分别映射到他们的进程共享区中。
10.1 线程栈
从上图可以发现除了地址空间的栈空间外,线程tcb中也维护一个名为线程栈的空间,而线程栈是采用数组的方式模拟出来的,这些模拟栈被保存在共享区,由线程库来维护。栈与栈之间相互独立,不可直接访问,但是同一进程下的其他线程采用特别的方式也可以访问到对方的栈,因为毕竟都在同一个地址空间内。
11、创建多线程
创建多线程的思路:利用循环定义多个线程id,但是由于新的循环会覆盖旧的线程id,因此需要把每个线程id存入容器中,方便后续等待线程时能够找到他们的线程id,创建多线程代码如下:
#include <iostream>
#include <vector>
#include <unistd.h>
#include <pthread.h>using namespace std;
#define NUM 4
struct threadData//给每个线程做标记
{string threadname;
};// 所有的线程,执行的都是这个函数?
void *threadRoutine(void *args)
{threadData *td = static_cast<threadData *>(args);int i = 5;while (i){cout << "我是" << td->threadname << ", pid: " << getpid() << endl;sleep(1);i--;}delete td;return nullptr;
}void InitThreadData(threadData *td, int number)
{td->threadname = "线程-" + to_string(number);
}int main()
{// 创建多线程!vector<pthread_t> tids;for (int i = 0; i < NUM; i++){pthread_t tid;threadData *td = new threadData;InitThreadData(td, i);//标记线程pthread_create(&tid, nullptr, threadRoutine, td);tids.push_back(tid);}//线程等待 for (int i = 0; i < tids.size(); i++){pthread_join(tids[i], nullptr);}return 0;
}
运行结果:
12、__thread
__thread只能修饰内置类型,不能修饰自定义类型,他修饰的变量对于所有线程是可见的,有点类似全局变量,但是他跟全局变量的区别在于:__thread修饰的变量对于每个线程而言是独立的,换句话说,线程对该变量的修改不会影响其他线程所看到的值。
测试代码如下:
#include <iostream>
#include <pthread.h>
#include <unistd.h>using namespace std;__thread int a = 10;void *threadRun(void* args)
{while(1){cout << "子线程: " << getpid() <<" a的值:"<<a++<<endl;//线程对a进行++sleep(1);}return nullptr;
}int main()
{pthread_t tid;//先定义一个pthread_t类型的变量//该函数调用完成后会赋予tid新的值,表示该线程的idpthread_create(&tid, nullptr, threadRun, nullptr);while(1){cout << "主线程: " << getpid() <<" a的值:"<<a<< endl;//此处a的值还是10吗?sleep(1);}
}
运行结果:
从结果可以看到,主线程的a是独立于子线程的a,原因就是a作为全局变量被__thread修饰了,因此所有线程都有一份独立的a。
13、线程分离
主线程创建的子线程退出后,主线程若想拿到子进程退出的void*返回值,则主线程要对其进行join等待操作,但是若主线程不关心其返回值,则就没必要进行等待,因为等待也是一种负担。因此在这种情况下,可以让该子线程自行分离,即分离的子线程在退出后会自动释放空间资源。
分离的函数介绍如下:
#include <pthread.h>int pthread_detach(pthread_t thread);
//thread表示要分离的线程
线程分离测试代码如下:
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <cstring>using namespace std;void *threadRun(void* args)
{pthread_detach(pthread_self());//pthread_self返回该线程的idint count = 3;while(count--){cout << "子线程: " << getpid() <<endl;sleep(1);}return nullptr;
}int main()
{pthread_t tid;pthread_create(&tid, nullptr, threadRun, nullptr);sleep(1);//要保证pthread_detach在pthread_join之前触发int n = pthread_join(tid, nullptr);printf("n = %d, who = 0x%x, why: %s\n", n, tid, strerror(n));
}
测试结果:
n = 22表示等待失败了,说明这些线程已经被分离了。
结语
以上就是关于使用线程的讲解,线程是核心思想是创建多个执行流让程序实现并行运行,目的就是提高程序执行的效率,本文主要讲述如何创建线程和使用线程,包括线程的基本用法和概念,线程本身涉及的知识非常广,细节也特别多,在复杂的多线程下往往要考虑更多的东西。
最后如果本文有遗漏或者有误的地方欢迎大家在评论区补充,谢谢大家!!
相关文章:

Linux_线程的使用
目录 1、线程与进程的关系 2、线程的优缺点 3、创建线程 4、查看启动的线程 5、验证线程是共享地址空间的 6、pthread_create的重要形参 6.1 线程id 6.2 线程实参 7、线程等待 8、线程退出 9、线程取消 10、线程tcb 10.1 线程栈 11、创建多线程 12、__th…...

[word] word如何编写公式? #微信#知识分享
word如何编写公式? word如何编写公式?Word中数学公式是经常会使用到的,若是要在文档中录入一些复杂的公式,要怎么做呢?接下来小编就来给大家讲一讲具体操作,一起看过来吧! 方法一:…...

Pytest 框架快速入门
Pytest 框架常用功能介绍 一、简介 Pytest 是一个功能强大的 Python 测试框架,具有简单易用、测试用例清晰易读、支持参数化、可运行由 Nose 和 unittest 编写的测试用例、拥有丰富的第三方插件且可自定义扩展、支持重复执行失败的用例以及方便与持续集成工具集成…...

抖音视频素材去哪里找啊?视频素材网站库分享
在这个视觉盛宴的抖音平台上,高质量和有趣的视频素材常常是吸引观众的重要钥匙。如果你也正在寻找那些能让你的视频作品更加出色的资源,那么恭喜你,今天我将为你介绍10个超实用的视频素材网站,让你的抖音视频创作充满创意和效率。…...

win10 langchain-chatchat-0.3.1安装及测试
git clone https://github.com/chatchat-space/Langchain-Chatchat.git conda create -n langchain3 python3.11 conda activate langchain3 xinference安装用另一篇文章的内容处理。 pip install langchain-chatchat -U -i https://pypi.tuna.tsinghua.edu.cn/simple pip in…...

Redis 教程:从入门到入坑
目录 1. Redis 安装与启动1.1. 安装 Redis1.1.1. 在Linux上安装1.1.2. 在Windows上安装 1.2. 启动 Redis1.2.1. 在Linux上启动1.2.2. 在Windows上启动 1.3. 连接Redis1.3.1. 连接本地Redis1.3.2. 连接远程Redis1.3.2.1. 服务器开放端口1.3.2.2. 关闭防火墙1.3.2.3. 修改配置文件…...

计算机图形学入门31:动画与模拟的求解
1.前言 上一篇介绍了动画与模拟的很多方法、模拟各种运动、基本知识。定义一个物体的速度和加速度,算出物体在任何时刻的位置,但是没有介绍具体怎么实现。这篇文章就是从上一篇的概念出发,介绍怎么把一个物体或多个物体运动的位置、不同时间出…...

Jmeter-单用户单表查询千条以上数据,前端页面分页怎么做
这里写自定义目录标题 单用户单表查询千条以上数据 单用户单表查询千条以上数据 对于单用户查询千条以上数据,但是前端页面做了分页的情况下 可以直接把查询接口下的分页限制去掉,便可查询出当前页面查询条件下的全部数据 例如去掉如下内容࿱…...

夏日养猫攻略!你家猫咪缺水了吗?补水罐头秘籍大公开
炎炎夏日,高温来袭,这几天又有几只猫咪因为中暑被送到我们医院了,经过诊断,发现猫咪体温超过40C,而且严重缺水。 各位铲屎官真的得注意,酷暑炎热,给猫咪补水很重要。猫咪的汗腺数量远远不及人类…...

生成名片格式
/*** 生成名片* param array arr2 卡片素材* param array strs 素材文字 数组* param function successFn 回调函数* * */PosterCanvasCard: function(arr2, strs, successFn, errFun) {let that this;const ctx uni.createCanvasContext(myCanvas);ctx.clearRect(0, 0, 0, 0…...

Linux常用命令(简要总结)
Linux常用命令 Linux 是一个强大的操作系统,广泛应用于服务器、开发和嵌入式系统中。掌握一些常用的 Linux 命令对于高效地使用系统至关重要。以下是一些常用的 Linux 命令及其简要说明: 文件和目录操作 ls:列出目录内容 ls ls -l # …...

从挑战到实战!TDengine 新能源行业研讨会要点回顾
近年来,随着全球对可再生能源需求的不断增长,新能源行业迎来了前所未有的发展机遇。然而,伴随着行业的快速发展,海量数据的管理和高效利用成为了行业面临的重要挑战。如何通过先进的数据管理技术提升新能源系统的效率和可靠性&…...

Linux 之 设置环境变量
设置环境变量 启动帐号后自动执行的是 文件为 .bashrc,然后通过这个文件可设置自己的环境变量; 临时设置环境变量: 在终端中使用 export 命令可以临时设置环境变量,例如:export PATH$PATH:/your/custom/path这种方法设…...

postgresql删除用户
背景 **角色与用户**:在 PostgreSQL 中,用户和组的概念是通过“角色”来统一实现的。角色可以有登录权限(在这种情况下,它们通常被称为“用户”),也可以没有(在这种情况下,它们通常用…...

【java深入学习第5章】Spring Boot 统一功能的实现及处理方式
Spring Boot 统一功能处理 在开发 Web 应用程序时,为了提高代码的可维护性和可扩展性,我们通常会采用一些统一的功能处理方式。本文将介绍如何在 Spring Boot 中实现统一的数据返回格式、异常处理和功能处理,并通过一个图书管理系统的案例来…...

【常见开源库的二次开发】基于openssl的加密与解密——单向散列函数(四)
目录: 目录: 一、什么是单项散列函数? 1.1 如何验证文件是否被修改过 1.2 单项散列函数: 二、单向hash抗碰撞 2.1 弱抗碰撞(Weak Collision Resistance) 2.2 强抗碰撞(Strong Collision Resista…...

获取不重复流水号(java)
一:概述 很多业务场景都需要获取不重复的业务流水号,当微服务项目或服务多节点部署时,获取流水号场景使用分布式锁性能低下,可以基于数据库行锁实现获取不重复流水号。 二:创建流水号数据库 CREATE TABLE serial (i…...

【python虚拟环境管理】【mac m3】 使用pipx安装poetry
文章目录 一. 安装 pipx二. 安装Poetry1. 安装2. advanced 操作 官网文档:https://python-poetry.org/docs/ pipx介绍文档:https://blog.51cto.com/u_15064632/2570626 一. 安装 pipx pipx 用于全局安装 Python 命令行应用程序,同时在虚拟环…...

git使用以及理解
git练习网站 Learn Git Branching git操作大全Oh Shit, Git!?! git commit git branch name git merge bugFix 合并俩个分支 git rebase main git checkout headgit switch head 会导致HEAD分离 ,就是指head->HEAD->c1 相对引用 ------------------- …...

openlayers 3d 地图 非三维 立体地图 行政区划裁剪 地图背景
这是实践效果 如果没有任何基础 就看这个专栏:http://t.csdnimg.cn/qB4w0 这个专栏里有从最简单的地图到复杂地图的示例 最终效果: 线上示例代码: 想要做这个效果 如果你的行政区划编辑点较多 可能会有卡顿感 如果出现卡顿感需要将边界点相应…...

GEO数据挖掘从数据下载处理质控到差异分析全流程分析步骤指南
0. 综合的教学视频介绍 GEO数据库挖掘分析作图全流程每晚11点在线教学直播录屏回放视频: https://www.bilibili.com/video/BV1rm42157CT/ GEO数据从下载到各种挖掘分析全流程详解: https://www.bilibili.com/video/BV1nm42157ii/ 一篇今年近期发表的转…...

我想我大抵是疯了,我喜欢上了写单元测试
前言 大家好我是聪。相信有不少的小伙伴喜欢写代码,但是对于单元测试这些反而觉得多此一举,想着我都在接口文档测过了!还要写什么单元测试!写不了一点!! 由于本人也是一个小小程序猿🙉…...

【Visual Studio】Visual Studio使用技巧及报错解决合集
目录 目录 一.概述 二.Visual Studio报错问题及解决方法 三.Visual Studio操作过程中遇到的问题及解决方法 四.Visual Studio编译优化选项 五.Visual Studio快捷键 一.概述 持续更新Visual Studio报错及解决方法,包括Visual Studio报错问题及解决方法、Visua…...

服务器数据恢复—raid5阵列热备盘同步失败导致lun不可用的数据恢复案例
服务器存储数据恢复环境: 华为S5300存储中有一组由16块FC硬盘组建的RAID5磁盘阵列(包含一块热备盘)。 服务器存储故障: 该存储中的RAID5阵列1块硬盘由于未知原因离线,热备盘上线并开始同步数据,数据同步到…...

算法题目整合
文章目录 121. 小红的区间翻转142. 两个字符串的最小 ASCII 删除总和143. 最长同值路径139.完美数140. 可爱串141. 好二叉树 121. 小红的区间翻转 小红拿到了两个长度为 n 的数组 a 和 b,她仅可以执行一次以下翻转操作:选择a数组中的一个区间[i, j]&…...

万界星空科技AI低代码平台:重塑数字化创新边界
在这个日新月异的数字化时代,技术的飞速发展正以前所未有的力量重塑着每一个行业。企业如何在瞬息万变的市场环境中保持竞争力,实现业务的快速迭代与创新? 重塑开发模式,加速数字化转型 传统软件开发周期长、成本高、门槛高&…...

iredmail服务器安装步骤详解!如何做配置?
iredmail服务器安全性设置指南?怎么升级邮件服务器? iredmail是一个功能强大的邮件服务器解决方案,它集成了多个开源软件,使您能够快速部署和管理邮件服务。AokSend将逐步引导您完成安装过程,无需深入的编程知识即可轻…...

【vue深入学习第1章】Vue.js 中的 Ajax 处理:vue-resource 库的深度解析
在现代前端开发中,Ajax 请求是与后端进行数据交互的关键技术。Vue.js 作为一个渐进式 JavaScript 框架,提供了多种方式来处理 Ajax 请求,其中 vue-resource 是一个较为常用的库。尽管 vue-resource 在 Vue 2.x 之后不再是官方推荐的 Ajax 处理…...

JDK新特性(Lambda表达式,Stream流)
Lambda表达式: Lambda 表达式背后的思想是函数式编程(Functional Programming)思想。在传统的面向对象编程中,程序主要由对象和对象之间的交互(方法调用)构成;而在函数式编程中,重点…...

【ARM】MDK-服务器与客户端不同网段内出现卡顿问题
【更多软件使用问题请点击亿道电子官方网站】 1、 文档目标 记录不同网段之间的请求发送情况以及MDK网络版license文件内设置的影响。 2、 问题场景 客户使用很久的MDK网络版,在获取授权时都会出现4-7秒的卡顿,无法对keil进行任何操作,彻底…...