Linux系统编程——线程控制

目录

一,关于线程控制

二,线程创建

2.1 pthread_create函数

2.2 ps命令查看线程信息

三,线程等待

3.1 pthread_join函数

 3.2 创建多个线程

3.3 pthread_join第二个参数 

四,线程终止

4.1 关于线程终止

4.2 pthread_exit线程退出

4.3 pthread_cancel线程取消

五,线程分离

七,线程ID和进程地址空间布局

7.1 线程ID与LWP

7.2 地址空间共享区中的线程栈

7.3 代码验证几个问题

八,pthread_create第四个参数传类的对象


一,关于线程控制

  1. 在线程概念章节,我们已经说过:Linux内核中没有明确的线程的概念,只有轻量级进程的概念,所以Linux没有给我们提供线程的系统调用,只有轻量级进程的系统调用
  2. 但又因为没有系统接口,用户用起来就比较犯难,所以他也必须给我们提供一套完整的线程接口。于是Linux设计者在用户层实现了一套线程方案,以动静态库的方式提供给用户进行使用 --> pthread线程库(也叫POSIX线程库或者原生线程库)
  3. 原生指的是大部分的Linux系统默认内置该线程库,使用这些库函数需要包含头文件"#include<pthread.h>",同时在编译链接时需要加上"-lpthread"选项
  4. 并且由于原生线程库是第三方库,不属于系统接口,那么pthread函数出错时不会设置errno全局变量,而是直接用返回值来告诉用户执行结果

 下面是常规情况下使用原生线程库的makefile内容:

mythread:mythread.ccg++ -o $@ $^ -std=c++11 -lpthread
.PHONY:clean
clean:rm -f mythread

二,线程创建

2.1 pthread_create函数

pthread_create就是的创建线程的函数了,在man手册中函数声明如下:

 解释下四个参数:

  1. 第一个参数是输出型参数,负责返回成功创建的新线程的id,pthread_t也是一个unsigned long int
  2. attr表示设置创建线程的属性,我们目前只要传入nullptr即可,表示不添加其它属性
  3. 第三个参数很明显是一个函数指针,该指针指向一个参数为void*,返回值为void*的函数
  4. 第四个参数arg表示要传入第三个函数指针参数的值,这个arg不仅仅可以传内置类型参数,也可以传C++类的对象,后面有代码

下面是用pthread_create函数创建线程,然后打印pid的代码:

#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;void show(const string &name)
{cout << name << ", pid: " << getpid() << "\n" << endl; // 在一个进程里面获取pid
}void *threadRun(void *args)
{const string name = (char *)args;while (true){show(name);sleep(1);}
}int main()
{pthread_t tid;char name[64];snprintf(name, sizeof name, "new thread");              // 以特定格式化,把内容搞到字符串里去,然后创建进程时把该字符串传过去pthread_create(&tid, nullptr, threadRun, (void *)name); // 新线程就跳转过去执行这个函数,主线程就继续向下运行while (true){cout << "I am main thread" << ", pid: " << getpid() << endl;sleep(1);}
}

 打印结果如下:

 

可以发现,两个执行流打印的进程pid一样,可以证明确实创建了线程 

2.2 ps命令查看线程信息

先让上面的进程挂着,可以另起一个窗口,先查看进程的

ps -ajx | head -1 && ps ajx | grep mythread

发现只有一条,正常,因为我们创建的是线程不是进程,要查看线程信息需要使用ps -aL 

ps -aL | head -1 && ps -aL | grep mythread

 其中PID就是进程的PID,LWP(Light Weight Process)就是线程的ID了,其中最小的就是主线程,主线程的LWP值和PID一样,LWP也存在task_struct里。

所以CPU调度时本质是看的LWP,由于我们前面说的是看的PID,那其实是一个进程只有一个线程的这种特殊情况,所以在CPU看来,LWP和PID其实是一样滴

三,线程等待

3.1 pthread_join函数

线程和进程一样,线程也是需要进行等待的,如果主线程不等待新线程,就会引起类似僵尸进程得问题,导致内存泄漏。线程等待得函数为pthread_join,下面是函数声明

 参数解释:

  1. 第一个参数thread表示要等待得线程得ID
  2. 第二个参数retval表示线程退出时得退出码信息

 3.2 创建多个线程

和进程一样,我么也可以创建多个线程并进行等待,如下代码:

#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <string>
#include <cstdio>
using namespace std;void show(const string &name)
{cout << name << ", pid: " << getpid() << endl; // 在一个进程里面获取pid
}void *threadRun(void *args)
{const string name = (char *)args;for (int i = 0; i < 5; i++){show(name);sleep(1);}return nullptr;
}int main()
{pthread_t tid[5]; // 创建5个线程,这个pthread_t类型也是一个unsigned long intchar name[64];for (int i = 1; i <= 5; i++){snprintf(name, sizeof name, "%s-%d", "thread", i);         // 以特定格式化,把内容搞到字符串里去,然后创建进程时把该字符串传过去pthread_create(tid + i, nullptr, threadRun, (void *)name); // 新线程就跳转过去执行这个函数,主线程就继续向下运行}for (int i = 1; i <= 5; i++){int a = pthread_join(tid[i], nullptr);if (a == 0){cout << "thread-" << i << " quit" << endl;}else{cout << "thread-" << i << " quit error: " << a << endl;}}
}

上面代码的逻辑大致是:一次性创建5个线程,每个线程各自打印部分内容,主线程等待,5秒过后,5个线程全部退出,和进程等待一样 

3.3 pthread_join第二个参数 

前面提到过:①原生线程库函数是通过返回值来告诉用户退出结果的    ②pthread_join的第二个参数可以获取线程退出码

但其实,第二个参数说明白点其实是获取线程退出信息,“获取退出码”其实是代表退出信号恰好是数字,所以我们可以用第二个参数获取其它信息,如下代码:

#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;
// 1,线程谁先运行与调度器有关
// 2,线程一旦异常, 就可能导致整个进程崩溃:所以,所有线程共用一个寄存器的标记位
// 3,线程的输入值和返回值问题
// 4,线程异常退出的理解void *threadRoutine(void *args)
{int *data = new int[10];for (int i = 0; i < 10; i++){cout << "新线程:" << (char *)args << " running " << i << endl;sleep(1);data[i] = i;// int a = 100;// a /= 0;// 线程出现除0异常,进程也随之终止}// exit(10); exit是进程退出,所以不要在线程里轻易调用exit//  return (void *)10; // 这个返回值一般返回给主线程,返回给pthread_join的第二个参数,直接保存在ret里return (void *)data; // data保存i的值,返回给主线程
}int main()
{pthread_t tid;pthread_create(&tid, nullptr, threadRoutine, (void *)"thread");int *ret = nullptr;               // 这里的ret属于指针变量。可以存值pthread_join(tid, (void **)&ret); // 默认是阻塞等待新线程退出,不关心线程异常退出,所以线程的健壮性会差一些// cout << "main thread wait done, main quit, " << (long long)ret << endl;    //64位下指针是8字节,指针是四字节,所以用long longfor (int i = 0; i < 10; i++) // 打印线程返回给主线程的执行结果{cout << ret[i] << " ";}cout << endl;return 0;
}

上面代码大致逻辑是: 创建一个线程,然后在线程里new一个数组,然后循环打印,并且在每次循环就往数组里写一个数组,最后线程执行完后,把数组的指针返回给主线的pthread_join的第二个参数,然后打印这个数组,结果如下:

所以我们可以感觉到这些传来传去的值,其实自由度很高,而这都是void*这个特殊类型的优势所在,后面会专门讲下void*相比其它类型指针的优势

四,线程终止

4.1 关于线程终止

  1. 线程执行进程代码的一部分,换到代码上就是执行一个函数,而一个函数常见的退出方式有三种:return,exit,信号退出。
  2. return是线程的正常退出,对其它线程没有影响,但是如果是exit,那么就是进程退出,会把所有的线程全部给退出掉,而且发生异常例如除0错误,是信号退出,信号退出也是进程退出
  3. 所以主线的pthread_join等待线程函数不关心线程的异常退出信息,因为线程如果出异常,那么全没了,再等待也没有意义了;而且主线程return时,其它线程也会退出,因为主线程一旦退出,其它线程赖以生存的地址空间资源也就全释放了,所以也会退出
  4. 所以要想线程退出并且不影响其它线程,有三种方法:return,pthread_exit和pthread_cancel,就是我们接下来要讲的

4.2 pthread_exit线程退出

使用起来很简单,功能就是终止当前,但是由于线程结时无法返回退出信息给它的调用者(因为是干掉自身,所以返回值无意义),所以pthread_exit和return返回的指针必须是全局的或者是malloc从堆上申请的,不能返回线程的独立栈上的数据,因为线程退出也会销毁栈

如下代码:

#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;void *threadRun(void *args)
{cout << "I am new thread, my quit number is: 666" << endl;pthread_exit((void *)666);
}int main()
{pthread_t tid;void *ret = nullptr;pthread_create(&tid, nullptr, threadRun, nullptr);pthread_join(tid, &ret);cout << (long long)ret << endl;
}

代码的逻辑很简单,线程函数直接调用pthread_exit退出,然后将666作为返回值返回,然后主线程的pthread_join捕捉到,就拿到了线程的返回值“666”

4.3 pthread_cancel线程取消

上面的pthread_exit是线程自己退出自己,其实使用起来比较别扭,而且容易出错,所以我们可以用pthread_cancel线程取消函数,这个函数可以指定线程ID取消,相比上面线程退出,线程取消灵活性更高,我们可以直接在主线程像类似“远程操控”一样控制线程退出

如下代码:

#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;void *threadRoutine(void *args)
{int i = 0;// int *data = new int[10];cout << "新线程:" << (char *)args << " running " << endl;while (true){sleep(1);}
}int main()
{pthread_t tid;pthread_create(&tid, nullptr, threadRoutine, (void *)"thread 1");for (int i = 5; i > 0; i--){cout << "线程取消倒计时: " << i << endl;sleep(1);}pthread_cancel(tid); // 直接取消线程int *ret = nullptr;pthread_join(tid, (void **)&ret); // 线程被取消,join的时候,退出码是-1 --> #define PTHREAD_CANCELED ((void *)-1)cout << "新线程退出,退出码为:  " << (long long)ret << endl;
}

五,线程分离

  1. 其实线程分离的概念和进程分离的概念是高度重合的,都是“如果不关心线程退出情况”,就不再需要pthread_join等待了,因为在不关心线程退出的情况下,join反而是一种负担,会降低整体效率,毕竟是阻塞式等待的
  2. 分离线程后,该线程依旧要使用进程的资源,崩溃时也要退出,只是该线程不再需要join等待了,线程分离后也不会造成“僵尸线程”,系统会自动回收该线程的PAB资源
  3. 分离和join是冲突的,只能存在一个

 

 下面是线程分离的样例代码:

#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;void *threadRoutine(void *args)
{pthread_detach(pthread_self()); // pthread_self()返回自己线程的idcout << "新线程:" << (char *)args << " running " << endl;sleep(3);cout << "新线程退出" << endl;pthread_exit((void *)666);
}int main()
{pthread_t tid;pthread_create(&tid, nullptr, threadRoutine, (void *)"thread 1");sleep(5);cout << "主线程退出" << endl;
}

七,线程ID和进程地址空间布局

7.1 线程ID与LWP

先阐明一个事实:线程ID和LWP没关系

下面是创建一个线程,然后获取线程ID,再以16进制打印出来,如下代码:

#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;std::string toHex(pthread_t tid) // 把tid转16进制
{char hex[64];snprintf(hex, sizeof(hex), "%p", tid);return hex;
}void *threadRoutine(void *args)
{while (true){cout << "thread id: " << toHex(pthread_self()) << endl;sleep(1);}
}int main()
{pthread_t tid;pthread_create(&tid, nullptr, threadRoutine, (void *)"thread 1");pthread_join(tid, nullptr);return 0;
}

 

  • pthread_create函数会产生一个线程ID,存放在第一个参数指向的空间中
  • 这个ID与LWP是两回事,LWP属于进程调度的范畴,因为线程是操作系统调度的最基本单位,所以需要一个数值来唯一表示,类似线程PID
  • pthread_create函数的第一个参数获取的数值与pthread_self()函数返回的数值是一样的

7.2 地址空间共享区中的线程栈

谈论这个要结合上面提到的线程ID

  1. 因为我们用的不是Linux自带的创建线程的接口,我们用的是pthread库中的接口,所以线程的概念是库给我们维护的,而且线程库不用维护线程的执行流,我们用的原生线程库,也是要加载到内存里的,加载到地址空间里
  2. 库的加载,默认是动态链接的,先把磁盘上的pthread库加载到内存里,然后通过页表映射到共享区,当我想调用库,就直接从代码区跳转到共享区,(如果想访问系统接口,就直接跳转到内核区然后通过内核级页表找到内核代码)
  3. 由于OS没有具体的关于线程的概念,所以关于线程的各种属性要由库来管理,而要管理,就要“先描述,再组织”,所以库就在共享区中为每一个线程都创建了对应的用户层线程的数据集合,这个集合里就包含了线程栈,而由于地址空间是线性的,所以为了让每个线程都能快速找到自己的属性集合,就把每个描述线程的结构体的的起始地址充当线程id了
  4. 主线程就用的是内核级栈结构,新线程用的就是共享区内部提供的私有栈结构
  5. 所以,本质上我们说的进程ID,其实就是一个虚拟地址,每一个进程的虚拟地址都是不同的,因此可以用它来区分每一个线程

问题:如何保证栈区是每一个线程独占的呢?
解答:用户要线程,但是OS只提供轻量级进程,所以在中间加了个线程库作为中间软件层,一样的,线程也要被管理起来,所以这个管理OS承担一部分,库承担一部分,OS承担的主要是对轻量级进程的调度和它的内核数据结构的管理,库主要是要为用户提供线程相关的属性字段。(线程也需要又自己私有的一部分属性,而这部分属性有可能无法和进程一样完整地表示线程,所以这部分工作就由库来完成。)

7.3 代码验证几个问题

通过下面的代码可以验证两个线程的周边结论:

①验证每个线程都有独立的栈空间

②主线程能访问每一个线程栈里的数据

#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <vector>
using namespace std;#define NUM 3
int *p = NULL;struct threadData
{string threadname;
};string toHex(pthread_t tid)
{char buffer[128];snprintf(buffer, sizeof(buffer), "0x%x", tid);return buffer;
}void InitThreadData(threadData *td, int number)
{td->threadname = "thread-" + to_string(number);
}void *threadRoutine(void *args)
{int test_i = 0;threadData *td = static_cast<threadData *>(args);if (td->threadname == "thread-2")p = &test_i; // 验证主线程能访问其它线程的栈数据int i = 0;while (i < 10){cout << "pid: " << getpid() << ", tid: " << toHex(pthread_self()) << ", threadname: " << td->threadname<< ", test_t: " << test_i << ", &test_i: " << &test_i << endl;sleep(1);i++;test_i++;}delete td;return nullptr;
}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);}sleep(1);cout << "main thread get a thread local value, val: " << *p << ", &p: " << p << endl;for (int i = 0; i < tids.size(); i++){pthread_join(tids[i], nullptr);}return 0;
}

 打印结果乱序很正常,比较线程本来就是并发执行的,从上面的信息我们看到下面几点:

  1. 每个线程打印的pid是一样的,每个线程的tid也就是地址不同,表示每个线程都有独立的栈结构
  2. 主线程定义全局指针p,p能访问2号线程的test_i值,说明主线程能访问每个线程的栈数据
  3. 每个线程都对test_i++,打印的值一样,再次证明线程有独立的栈结构
  4. 在线程当中,线程有独立的栈结构,但是没有私有的栈结构,其它线程仍然能访问,但是以后编写代码时,非常不建议这样搞

八,pthread_create第四个参数传类的对象

pthread_create第四个参数不仅可以传整数或字符串,也可以传类的对象,这就是void*的好处,如下代码:

#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;class Request
{
public:Request(int start, int end, const string &threadname): _start(start), _end(end), _threadname(threadname){}public:int _start;int _end;string _threadname;
};class Response
{
public:Response(int result, int exitcode): _result(result), _exitcode(exitcode){}public:int _result;int _exitcode;
};void *sumCount(void *args)
{Request *rq = static_cast<Request *>(args); // 类似于Request *eq = (Request*)argsint sum = 0;for (int i = rq->_start; i <= rq->_end; i++) // 求start到end的数的和{cout << rq->_threadname << " is runing... " << i << endl;sum += i;usleep(60000);}Response *rsq = new Response(sum, 0);delete rq;return rsq;
}int main()
{pthread_t tid;Request *rq = new Request(1, 100, "thread 1:");pthread_create(&tid, nullptr, sumCount, rq); // 这个参数不仅仅只能传整数或字符串,我还可以传类的对象void *ret;pthread_join(tid, &ret);Response *rsp = static_cast<Response *>(ret);cout << "rsp->result: " << rsp->_result << endl;delete rsp;return 0;
}


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

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

相关文章

提示词工程课程,白嫖A100活动课程

扫下面二维码即可参加 免费使用A100&#xff0c;学习大模型相关知识&#xff01; 前置知识&#xff1a; 内容来源&#xff1a;Docs 模型设置 在使用提示词的时候&#xff0c;您会通过 API 或者网页版与大语言模型进行交互&#xff0c;将这些参数、设置调整到最佳程度会提高使…

14-47 剑和诗人21 - 2024年如何打造AI创业公司

​​​​​ 2024 年&#xff0c;随着人工智能继续快速发展并融入几乎所有行业&#xff0c;创建一家人工智能初创公司将带来巨大的机遇。然而&#xff0c;在吸引资金、招聘人才、开发专有技术以及将产品推向市场方面&#xff0c;人工智能初创公司也面临着相当大的挑战。 让我来…

UML中用例和用例图的概念

用例 用例模型的基本组成部分有用例、参与者&#xff08;或角色&#xff09;和系统。用例用于描述系统的功能&#xff0c;也就是从用户的角度来说&#xff0c;系统具体应包含哪些功能&#xff0c;从而帮助分析人员理解系统的行为。它是对系统功能的宏观的、整体的描述。一个完…

idea中的块映射中的子元素无效

在yml文件中&#xff0c;出现块映射中的子元素无效&#xff0c;如图&#xff1a; 在YAML文件中&#xff0c;通常需要在键和值之间添加空格&#xff0c;以确保文件的可读性和正确解析。一些YAML解析器可能要求在冒号后面必须有空格才能正确解析文件。如果不加空格&#xff0c;解…

变长输入神经网络设计

我对使用 PyTorch 可以轻松构建动态神经网络的想法很感兴趣&#xff0c;因此我决定尝试一下。 我脑海中的应用程序具有可变数量的相同类型的输入。对于可变数量的输入&#xff0c;已经使用了循环或递归神经网络。但是&#xff0c;这些结构在给定行的输入之间施加了一些顺序或层…

Hugging Face使用笔记

1. HuggingFace简介 Hugging Face Hub和 Github 类似&#xff0c;都是Hub(社区)。Hugging Face可以说的上是机器学习界的Github。Hugging Face为用户提供了以下主要功能&#xff1a; 模型仓库&#xff08;Model Repository&#xff09;&#xff1a;Git仓库可以让你管理代码版…

kei5l中不能跳转到函数定义的原因和个人遇到的问题

快捷键 CTRLK或F12&#xff0c;在选择要查看的函数定义时按下可以查看到&#xff08;文件没问题的情况下&#xff09; 出现不能查看的原因 1&#xff0c;没有设置生成文件信息&#xff08;第一次打开工程常遇到问题&#xff09; 2, 定义函数的代码没有加入工程 解决方式如下…

南大通用数据库-Gbase-8a-学习-44-DDLEVENT恢复

目录 一、环境信息 二、前景提要 1、情况描述 2、3号节点gc_recover日志截图 3、3号节点express日志截图 4、ddlevent截图 5、报错赋权语句分别在1节点和4节点执行 6、gcadmin 三、解决方法 1、描述 2、清理系统user表DDLEVENT 3、拷贝系统user表数据 &#xff08;…

3.js - 灯光与阴影 - 聚光灯

// ts-nocheckimport * as THREE from three // 导入轨道控制器 import { OrbitControls } from three/examples/jsm/controls/OrbitControls // 导入hdr加载器 import { RGBELoader } from three/examples/jsm/loaders/RGBELoader.js // 导入lil.gui import { GUI } from thre…

数据库之索引(三)

目录 一、简述索引实现的原理 二、简述数据库索引的重构过程 三、为什么MySQL的索引使用B树 四、简述联合索引的存储结构及其有效方式 五、MySQL的Hash索引和B树索引有何区别 一、简述索引实现的原理 在MySQL中&#xff0c;索引是在存储引擎层实现的&#xff0c;不同存储引…

Echarts:渲染成Canvas还是SVG,该如何抉择?

ECharts 从初始一直使用 Canvas 绘制图表。而 ECharts v4.0 发布了 SVG 渲染器&#xff0c;从而提供了一种新的选择。在初始化图表实例时&#xff0c;只需设置 renderer 参数 为 canvas 或 svg 即可指定渲染器&#xff0c;比较方便。贝格前端工场带领大家看下如何选择。 一、C…

如何使用HTML和JavaScript读取文件夹中的所有图片并显示RGB范围

如何使用HTML和JavaScript读取文件夹中的所有图片并显示RGB范围 在这篇博客中&#xff0c;我将介绍如何使用HTML和JavaScript读取文件夹中的所有图片&#xff0c;并显示这些图片以及它们的RGB范围。这个项目使用现代浏览器提供的<input type"file" webkitdirecto…

苹果电脑为什么要清理软件?

你有没有想过&#xff0c;你的苹果电脑也许是一个秘密的收藏家&#xff1f;没错&#xff0c;你的Mac可能在悄悄收集那些你曾经用过的每一个字节&#xff0c;从那次偶然下载的应用到你已经忘记了的各种文件。久而久之&#xff0c;这些“收藏品”就会堆积成山&#xff0c;让你的苹…

智能电表在碳中和实现过程中发挥什么作用

智能电表在碳中和实现过程中发挥着至关重要的作用&#xff0c;这些作用主要体现在提高碳排放计量的精准度、推动绿色能源转型、促进电力领域低碳发展等方面&#xff1b;随着技术的不断发展和应用的不断深入相信智能电表将在碳中和实现过程中发挥更加重要的作用。以下是对智能电…

开始尝试从0写一个项目--前端(二)

修改请求路径的位置 将后续以及之前的所有请求全都放在同一个文件夹里面 定义axios全局拦截器 为了后端每次请求都需要向后端传递jwt令牌检验 ps&#xff1a;愁死了&#xff0c;翻阅各种资料&#xff0c;可算是搞定了&#xff0c;哭死~~ src\utils\request.js import axio…

【最新鸿蒙应用开发】——Router页面路由

Router路由 页面路由指的是在应用程序中实现不同页面之间的跳转&#xff0c;以及数据传递。通过 Router 模块就可以实现这个功能. 1. 创建页面 之前是创建的文件&#xff0c;使用路由的时候需要创建页面&#xff0c;步骤略有不同 方法 1&#xff1a;直接右键新建Page&#xf…

Python28-10 LightGBM对乳腺癌数据集进行分类

LightGBM&#xff08;Light Gradient Boosting Machine&#xff09;是一个梯度提升框架&#xff0c;由微软开发。它用于机器学习中的分类、回归和排序等任务&#xff0c;特别适合处理大规模数据和高维特征。LightGBM的核心是梯度提升决策树&#xff08;GBDT&#xff09;算法&am…

虚拟现实3d场景漫游体验实现了“所见即所得”

如今&#xff0c;从实体店铺到工厂企业&#xff0c;再到政府单位&#xff0c;各行各业都已纷纷加入VR数字化升级的行列&#xff0c;相比传统的2D商品展示&#xff0c;三维交互展示成为商企客户交流的主流方式。产品展示、服务介绍、考察洽谈等都可以通过在3D虚拟场景网站中真实…

7月学术会议:7月可投的EI国际会议

随着科技的迅猛发展&#xff0c;学术交流与研讨成为了推动科研进步的重要途径。进入7月&#xff0c;众多高质量的EI国际会议纷纷拉开帷幕&#xff0c;为全球的科研工作者提供了一个展示研究成果、交流学术思想的平台。以下&#xff0c;我们将详细介绍一些在7月可投的EI国际会议…

Chromium编译指南2024 Linux篇-安装官方工具depot_tools(二)

1.引言 在上一节中&#xff0c;我们已经完成了 Git 的安装&#xff0c;并了解了其在 Chromium 编译过程中的重要性。接下来&#xff0c;我们将继续进行环境的配置&#xff0c;首先是安装和配置 Chromium 编译所需的重要工具——depot_tools。 depot_tools 是一组用于获取、管…