Linux -- 线程互斥

news2025/1/14 18:04:48

一 线程互斥的概念

  大部分情况,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量归属单个线程,其他线程无法获得这种变量。但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完成线程之间的交互。多个线程并发的操作共享变量,会带来一些问题。

  例如下面我们模拟一个多线程抢票的程序。使用一个全局变量 ticket 表示票的数量,创建多个线程进行抢票,代码如下:

#include<iostream>
#include<unistd.h>
#include<pthread.h>
#include<vector>

using namespace std;

#define NUM 5
int ticket = 100;


class threadData
{
public:
	threadData(int number){
		threadname = "thread-" + to_string(number);
	}
				
public:
	string threadname;
};

void* getTicket(void* args)
{
	threadData *td = static_cast<threadData*>(args);		    
	while(1)
	{
		if(ticket > 0)
		{
			usleep(1000);     // 模拟抢票的时间
			 printf("%s is running, get a ticket: %d\n", td->threadname.c_str(), ticket);
			ticket--;
			}
		else{
         break;
        }
	}
	return nullptr;
}


int main(){
	 vector<pthread_t> tids;
	 vector<threadData*> thread_datas;

	 for(int i = 1; i <= NUM; i++){
	    threadData* td = new threadData(i);
	    pthread_t tid;
		pthread_create(&tid, nullptr, getTicket, td);
		tids.push_back(tid);
		thread_datas.push_back(td);
	}
				    
	for(auto e : tids){
      pthread_join(e, nullptr);
    }		

	for(auto td : thread_datas){
	  delete td;
    }

	return 0;
}

我们运行起来之后,会看到线程抢到了负数的票! 

  为什么会出现这种情况呢?这种情况我们称为共享数据在无保护的情况下,被多线程并发访问,造成了数据不一致问题!所以对于一个全局变量进行多线程并发减减或者加加,不是安全的!下面我们来分析一下。

  首先需要对 ticket- -,先要将 ticket 读入到 CPU 的寄存器中,然后在 CPU 中要进行计算操作,最后再将 ticket 数据写回内存中。至此就完成了一次 ticket- -,所以上面三个步骤,都会对应每一个汇编语句。

  那么假设我们现在有两个线程,分别为线程1和线程2,在线程执行的代码间隙中,线程是随时有可能会被切换的!而线程在执行的时候,将共享数据加载到 CPU 寄存器的本质就是把数据的内容变成了自己上下文的内容!也就是以拷贝的方式给自己单独拿了一份!

  那么如果线程1刚好读取到内存中的数据,假设此时数据还是100,此时它要被切换了,那么它就要把自己上下文数据保存起来,而保存上下文的本质就是以拷贝的方式,给自己单独拿了一份!那么此时线程1就把100保存到自己的上下文中了。

   

  接下来线程2就开始抢票了,此时线程2在它的时间片内已经抢了99张票了!此时内存中只剩下一张票!

  那么当线程2切换后,线程1继续拿着它的上下文数据放回CPU中计算,注意,此时线程1中的 ticket 还是100,那么计算完后为99,再将 99 写回内存中!此时就导致了 ticket 的数据不一致问题!所以 ticket- - 操作是不安全的!也就是它不具备原子性!

  

  另外,我们不仅仅在对 ticket- -,这种叫做数值计算,而且还在对 ticket 做判断是否大于0,这个过程也是在对 ticket 计算,这种叫做逻辑运算!所以,假设当前 ticket 为1了,在判断期间,可能会有多个线程在进行判断!因为一个线程在判断的期间有可能会被切走!此时它们每一个线程的上下文中都认为 ticket 是1,所以会将 ticket 减到负数!而且判断完毕之后,ticket 就不会被用了,在计算 ticket- - 的时候要重新到内存中读取数据!

  那么这个问题要怎么解决呢?对于共享数据的访问,需要保证任何时候只有一个执行流访问,这就是互斥!所以我们需要通过互斥的方式来解决,也就是互斥锁!接下来我们就开始学习互斥锁。

二 互斥锁 

   在 Linux 中,pthread 库给我们提供了一种互斥锁解决上面多线程访问共享数据不一致的问题。

互斥锁,顾名思义,我们用一个锁将多个线程访问的代码锁住,使其只有一个线程有钥匙,也就是同时只让一个线程进行访问,进而实现了互斥。

接下来我i们认识一下互斥锁的相关接口:

2.1 锁的初始化

  pthread_mutex_init()

#include <pthread.h>


int pthread_mutex_init(pthread_mutex_t *restrict mutex,
const pthread_mutexattr_t *restrict attr);

  该接口是对一把锁初始化。我们可以看到第一个参数的类型是 pthread_mutex_t,这是库给我们提供的一种数据类型,也可以理解为锁的类型!

  第一个参数是输入型参数,我们定义一个 pthread_mutex_t 类型的锁传入它的地址即可

  第二个参数代表这把锁的属性,我们也不管,设置为 nullptr 即可。

  其实,初始化一把锁有两种方式,以上是一种方式,下面还有一种方式是定义一把全局的锁,如果我们使用下面的方法定义了一把锁,就不需要使用上面的方式了;而且也不用释放这把锁了,但是释放也没有问题。其中定义全局锁是固定的,如下:

				pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

2.2 锁的释放

pthread_mutex_destroy()

 #include <pthread.h>

 int pthread_mutex_destroy(pthread_mutex_t *mutex);

  该接口是释放一把锁。第一个参数和初始化时的第一个参数一样。注意如果我们使用 pthread_mutex_init() 的方式初始化一把锁,必须要使用 pthread_mutex_destroy() 进行释放;但是使用全局锁就可以不用释放。

2.3 加锁

  pthread_mutex_lock()

 #include <pthread.h>

 int pthread_mutex_lock(pthread_mutex_t *mutex);

该接口就是对一把锁进行加锁(也就是对该锁进行使用,上锁)

2.4 解锁

 #include <pthread.h>

int pthread_mutex_unlock(pthread_mutex_t *mutex);

该接口就是对一把锁进行解锁,也就是解除对资源的锁定。

2.5 使用示例与说明

  下面我们就使用上面抢票的代码进行加锁,对 ticket 加锁。根据我们以前的知识,这个 ticket 就变成了临界资源,而临界资源并不是全部代码都在访问,而是只有一小部分在访问,我们就把这一小部分的代码的区域称为临界区。

  其实加锁的本质是用时间来换取安全,加锁的表现就是线程对于临界区代码需要串行执行,也就是类似于排队,所以加锁的原则就是尽量要保证临界区代码越少越好!所以上述的代码中,临界区应该是对 ticket 进行访问的区域!如下代码:

首先我们在类中定义一把锁,方便每个线程都有自己的锁:

class threadLock
{//我们将锁的id和锁姓名封装成一个类
public:
    std::string _name; // 给程序员识别的
    pthread_mutex_t* _lock; //让不同的线程使用的锁
public:
    threadLock(int num,pthread_mutex_t* Lock)
    :_lock(Lock)
    {
        _name="Thread"+std::to_string(num);
    }
};

  接下来我们在主函数中定义一把锁,注意,这里定义的锁,是在 main() 函数的栈帧中的,也就是主线程中的,由于我们抢票的程序也在主函数中,所以这样定义不会有问题;最后在主函数返回前释放锁,代码如下:

 

int main(){

    pthread_mutex_t lock; //定义一把锁
    pthread_mutex_init(&lock,nullptr); //进行锁的初始化

	std::vector<pthread_t> tids; //线程编号数组 这两组是为了方便释放
	std::vector<threadLock*> thread_locks; //锁编号数组
    
    for(int i=0;i<=NUM;i++){ //循环创建线程,进行抢票
        
        threadLock* td=new threadLock(i,&lock); //创建一个新锁
        pthread_t tid; 
        pthread_create(&tid, nullptr, getTicket, td);//创建线程 同时抢票
        //让线程和锁进入不同的数组
        tids.push_back(tid);
		thread_locks.push_back(td);
    }
     
     //循环释放
     for(auto e : tids){
      pthread_join(e, nullptr);
     }
				
				
	for(auto td : thread_locks){
        delete td;
     }
				       
    //删除锁
    pthread_mutex_destroy(&lock);
	return 0;
}

接下来是抢票的程序:

void* getTicket(void* args)
{
    //获取锁名和id
	threadLock *td = static_cast<threadLock*>(args);		    
	while(1){ //循环抢票
        //上锁 每一次我们将要访问下面这段代码时,除了当前线程,禁止其它线程访问
        pthread_mutex_lock(td->_lock);
		if(ticket > 0){//有票 
			usleep(1000);     // 模拟抢票的时间
			 printf("%s is running, get a ticket: %d\n", td->_name.c_str(), ticket);
			ticket--;
            pthread_mutex_unlock(td->_lock); //关锁
            usleep(10);
		}
		else{ //无票 关锁 退出
           pthread_mutex_unlock(td->_lock);
           break;
        }

        //pthread_mutex_unlock(td->_lock); //在这里不如上面分两端写,两端写锁占用的资源更少
	}
    std::cout << td->_name.c_str() << "` quit!" << std::endl;
	return nullptr;
}

头文件及杂项:

#include<iostream>
#include<pthread.h>
#include<string>
#include<vector>
#include<time.h>
#include<unistd.h>


const int NUM=5; //创建的线程数量
int ticket=10000; //票数

 

  如上代码,在加锁和解锁锁的区域中,就称为临界区。其中,执行流在申请锁的时候,如果申请锁成功,才能往后执行后面的代码,如果不成功,就会阻塞等待!

  执行的结果如下:

我们可以看到,ticket 不会出现 0 和负数的情况了,也就是说,临界资源被并发访问导致数据不一致问题已经解决了!

但是代码中有些细节我们还需要讲解一下。

  1. 在抢票的程序中,我们可以看到,在一个线程抢完票后,解锁后,我们在其后面加了一句 usleep(10);,这是什么意思呢?很简单,当一个线程加锁后,其它线程就被阻塞等待挂起了,那么当该线程解锁时,其它线程还没来得及从阻塞状态转为运行状态,该线程又去申请锁了,也就是说,唤醒线程的成本更大;而且,我们抢完票后还有后续的代码需要执行,比如处理票的后续动作,这里我们就没有实现。也就是说,所以我们在一个线程解锁后,加上短暂的休眠时间,一是为了有时间唤醒其它线程,二是为了模拟抢票后的后续动作。
  2. 如果我们没有加上 usleep(10); 这句代码,那么该线程就会一直占用这把锁,所以导致票就被它抢完了。那么也就是说,这种纯互斥环境,如果锁分配不够合理,容易导致其它线程一直没有等待到资源,进程一直等待,一直在消耗,直到饿死,这就是饥饿问题。但是不是说只要有互斥,必有饥饿,而是适合纯互斥的场景,就用互斥!
  3. 新来的线程,必须要从等待队列的最后开始排队;解锁的线程,不能马上重新申请锁,必须也要从等待队列的最后开始排队。这就可以让所有的线程获取钥匙,按照一定的顺序,这种按照一定顺序性获取资源的称为同步,这个我们后面详谈。
  4. 每一个线程进入临界区访问临界资源的时候,首先需要申请加锁,所以锁本身就是共享资源,也就是临界资源!所以申请加锁和解锁本身就被设计为原子性的操作了!如何做到的呢?我们后面讲原理再谈。
  5. 那么在临界区中,线程可以被切换吗?可以切换!因为在线程被切出去的时候,是持有锁被切走的,所以在该线程被切换的时候,其他线程也不能进临界区访问临界资源,因为锁只有一把!所以对于其它线程来说,一个线程要么没有锁,要么释放锁,当前线程访问临界区的过程,对于其它线程是原子的!

三 锁的原理

  我们已经知道,ticket>0 不是原子的,因为这个操作会被分为三个汇编语句,那么什么是原子的呢?在计算机底层,我们认为,一条汇编语句就是原子的!

  为了实现互斥锁操作,大多数体系结构都提供了 swapexchange 指令,该指令的作用是把寄存器和内存单元的数据交换,由于只有一条汇编指令,保证了原子性。现在我们把 lockunlock 的伪代码演示一下:

  首先 movb 就是把 0 写入 al 寄存器,可以理解为 eax 寄存器:

  接下来 xchgb 就是将 al 寄存器中的内容和内存中定义的一个变量 mutex 进行交换,其中 mutex 的初始值为1:

接下来对 al 寄存器中的值进行判断,如果大于 0,说明申请加锁成功,否则申请加锁失败,挂起等待。

上面我们演示的都是一个线程来申请加锁,如果有两个线程来申请加锁呢?例如,线程1和线程2来申请加锁,而加锁的语句是一句,但是它被分为上面多个汇编语句,所以当一个线程执行到某一个汇编语句的时候,随时都有可能被切换!

假设线程1申请加锁的过程中,刚刚执行完第一步,即将 0 写入了 al 寄存器中,实际上是写入线程的硬件的上下文中。此时线程2来了,线程1要被切走,所以线程1将 al 寄存器中的内容保存起来,即将 0 保存起来,当切换回来的时候执行 xchgb 语句。  

线程2来的时候,再次将0写入 al 寄存器中,然后执行xchgb语句,将 al 寄存器中的内容和内存的内容交换,交换完成后,al 寄存器中的内容变成1,线程2中的上下文内容也变成1,正常来说线程2此时做判断,此时al寄存器的值大于0,所以可以直接返回。

但是如果在线程2做判断的时候,线程2需要被切走,线程1切回来,首先先要将上下文恢复回来,此时将 al 寄存器中的内容恢复成为0,然后和内存中的值交换,交换完后发现 al 寄存器中的值为 0,此时线程1就被挂起等待了。 

线程1被挂起等待后不会被调度,所以此时线程2被切回来,恢复上下文,把1放回al寄存器中,然后做判断,大于0,申请加锁成功,返回0。

所以从上面的过程我们可以看出,其实 xchgb 的语句最重要。交换的本质就是把内存中的数据,交换到CPU的寄存器中,也就是将数据交换到线程的上下文中!而线程上下文是线程私有的!另外,内存中的数据是被所有线程共享的,而锁只有一把,所以申请加锁的本质就是把一把共享的锁,让一个线程以一条汇编的方式,交换到自己的上下文中,就代表当前线程持有锁了!

那么解锁的汇编语句如下:

 

  其实就是将内存中的数据重置为1即可。并没有将上一次线程申请加锁的 1 交换回内存中,因为并不需要,因为每一个线程在申请加锁的时候首先需要将 0 写入 al 寄存器中,也就是写入自己的硬件上下文中,此时就相当于将原来申请过加锁的 1 覆盖掉。 

四、可重入和线程安全

4.1 概念

  • 线程安全:多个线程并发同一段代码时,不会出现不同的结果。常见对全局变量或者静态变量进行操作,并且没有锁保护的情况下,会出现该问题;
  • 重入:同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则,是不可重入函数。

也就是说,如果一个函数不可重入,那么在多线程执行时,可能会出现线程安全问题。如果一个函数可被重入的,那么就一定不会出现线程安全问题。

4.2 可重入与线程安全联系

  • 函数是可重入的,那就是线程安全的;
  • 函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题,如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的。

4.3 可重入与线程安全区别

  • 可重入函数是线程安全函数的一种;
  • 线程安全不一定是可重入的,而可重入函数则一定是线程安全的;
  • 如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数若锁还未释放则会产生死锁,因此是不可重入的。

最后总结就四个结论:

  • 线程安全描述的是并发的问题
  • 可重入描述的是函数特点的问题
  • 不可重入函数在多线程访问时,可能会出现线程安全问题
  • 可重入函数在多线程访问时,不会有线程安全问题

五、死锁

5.1.死锁概念

死锁是指在一组执行流中的一个线程持有一把锁,另一个线程持有另一把锁,但因互相申请对方的锁,并不释放自己的锁而处于的一种永久等待状态。

5.2 死锁的必要条件

首先我们了解一下什么叫做死锁的必要条件,也就是只要产生了死锁,必定所有的条件都要满足。也就是以下四个条件都要满足:

  • 互斥条件:一个资源每次只能被一个执行流使用
  • 请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放
  • 不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺
  • 循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系

5. 3. 避免死锁

  • 破坏死锁的四个必要条件之一
  • 加锁顺序一致
  • 避免锁未释放的场景
  • 资源一次性分配

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

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

相关文章

【网络原理】TCP三次握手四次挥手

文章目录 &#x1f332;三次握手四次挥手总览&#x1f340;三次握手&#xff08;建立连接&#xff09;&#x1f338;为什么需要第三次通信 &#xff1f; &#x1f384;四次挥手&#xff08;断开连接&#xff09;&#x1f338;四次挥手中涉及到的两个重要的状态 ⭕总结 &#x1…

nginx学习记录-nginx初步配置

1. 虚拟机安装系统并配置网络 系统网上找个能用的镜像就行&#xff0c;我用的是阿里的镜像&#xff0c;地址是centos安装包下载_开源镜像站-阿里云 (aliyun.com) 以下是我本地的虚拟机配置 配置过程中按照提示操作系统即可。 安装好系统后&#xff0c;配置centos的ip&#x…

C++进阶之路---我们在何种情况下使用set和map

顾得泉&#xff1a;个人主页 个人专栏&#xff1a;《Linux操作系统》 《C从入门到精通》 《LeedCode刷题》 键盘敲烂&#xff0c;年薪百万&#xff01; 前言 在之前的学习中&#xff0c;我们已经接触过STL中的部分容器&#xff0c;比如&#xff1a;vector、list、deque、for…

【多智能体强化学习02---训练范式+独立学习+多智能体策略梯度算法】

文章目录 多智能体强化学习训练和执行范式CTCEDTDECTDE MARL符号表示分布式学习&#xff08;Independent Learning)基于值函数的分布式学习&#xff08;Independent value-based learning)基于策略梯度的分布式学习&#xff08;Independent policy gradient learning) 多智能体…

力扣:数组篇

1、数组理论基础 数组是存放在连续内存空间上的相同类型数据的集合。 需要两点注意的是 数组下标都是从0开始的。数组内存空间的地址是连续的 因为数组的在内存空间的地址是连续的&#xff0c;所以我们在删除或者增添元素的时候&#xff0c;就难免要移动其他元素的地址。 …

LLM之RAG实战(二十九)| 探索RAG PDF解析

对于RAG来说&#xff0c;从文档中提取信息是一种不可避免的场景&#xff0c;确保从源文件中提取出有效的内容对于提高最终输出的质量至关重要。 文件解析过程在RAG中的位置如图1所示&#xff1a; 在实际工作中&#xff0c;非结构化数据比结构化数据丰富得多。如果这些海量数据无…

免费视频背景素材下载

找免费视频素材、背景就上这6个网站&#xff0c;高质量&#xff0c;无版权可商用。 1、菜鸟图库 https://www.sucai999.com/video.html?vNTYwNDUx 菜鸟图库虽然是个设计素材网站&#xff0c;但除了设计类素材之外还有很多视频、音频、办公类等素材&#xff0c;视频素材就有上…

课堂行为动作识别数据集

一共8884张图片 xml .txt格式都有 Yolo可直接训练 已跑通 动作类别一共8类。 全部为教室监控真实照片&#xff0c;没有网络爬虫滥竽充数的图片&#xff0c;可直接用来训练。以上图片均一一手工标注&#xff0c;标签格式为VOC格式。适用于YOLO算法、SSD算法等各种目标检测算法…

自主通用多物理场仿真PaaS平台伏图(Simdroid)及伏图电子散热模块上架华为云商店

随着云计算、大数据等前沿技术的蓬勃发展&#xff0c;国内制造业正面临智能制造转型升级的机遇与挑战。工业软件是制造业研发创新不可或缺的核心工具&#xff0c;《“十四五”智能制造发展规划》中明确了工业软件对于智能制造的核心支撑作用&#xff0c;着重提出加强关键核心技…

DevOps方案中10款最佳开源监控工具

DevOps方案中10款最佳开源监控工具 2024年,监视对现代DevOps团队的工作至关重要。DevOps团队需要可靠且灵活的工具,以有效监视和管理复杂系统,这些系统可以提供有关系统性能、可用性和安全性的实时见解。 开源监控工具由于其成本效益、灵活性和社区支持而日益受到青睐。 …

MySQL教程-SQL

SQL(Structured Query Language)结构化查询语言&#xff0c;操作关系型数据库的编程语言&#xff0c;定义了一套操作关系型数据库统一标准。 语法 SQL语句可以单行或多行书写&#xff0c;以;为结束标记SQL可以使用空格或缩进来增强语句的可读性SQL分单行注释(-- 注释内容 或 …

景略JL5104-N048C 可P2P代替RTL8304MB

话不多说&#xff0c;直接上参数&#xff0c;可免费申请样片上机测试。 JL5104是一个带有RMIl接口的4端口快速以太网交换机。 它支持四级优先级队列的QoS功能&#xff0c;保证了一些实时网络应用的性能。 JL5104集成了一个2k入口地址查找表和一个4路关联散列算法&#xff0c;该…

ios xcode 15 PrivacyInfo.xcprivacy 隐私清单 查询应用使用的隐私api

1.需要升级mac os系统到13 兼容 xcode 15.1 2.升级mac os系统到14 兼容 xcode 15.3 3.选择 New File 4.直接搜索 privacy 能看到有个App Privacy 5.右击Add Row 7.直接选 Label Types 8.选中继续添加就能添加你的隐私清单了 苹果官网文档

稀碎从零算法笔记Day15-LeetCode:判断子序列

跑样例的时候LC炸了&#xff0c;以为今天回断更 题型&#xff1a;字符串、双指针 链接&#xff1a;392. 判断子序列 - 力扣&#xff08;LeetCode&#xff09; 来源&#xff1a;LeetCode 题目描述&#xff08;此题建议结合样例理解&#xff09; 给定字符串 s 和 t &#xf…

鸿蒙Socket通信示例(TCP通信)

前言 DevEco Studio版本&#xff1a;4.0.0.600 参考链接&#xff1a;OpenHarmony Socket 效果 TCPSocket 1、bind绑定本地IP地址 private bindTcpSocket() {let localAddress resolveIP(wifi.getIpInfo().ipAddress)console.info("111111111 localAddress: " …

性能测试总结 —— 测试流程篇!

本文主要介绍下性能测试的基本流程&#xff0c;性能测试从实际执行层面来看&#xff0c;测试的过程一般分为这么几个阶段&#xff0c;如下图&#xff1a;       下面分别介绍下每个阶段具体需要做什么&#xff1a; 一、性能需求分析&#xff1a; 性能需求分析是整个性能…

【PLC】现场总线和工业以太网汇总

1、 现场总线 1.1 什么是现场总线 1&#xff09;非专业描述&#xff1a; 如下图&#xff1a;“人机界面”一般通过以太网连接“控制器(PLC)”&#xff0c;“控制器(PLC)”通过 “现场总线”和现场设备连接。 2&#xff09;专业描述&#xff08;维基百科&#xff09; 现场总线…

MTK安卓开发板_联发科开发板评估套件_安卓主板硬件开发

在介绍开发板之前&#xff0c;让我们先来区分一下核心板和开发板的区别。核心板是一种集成度高、功能完整的计算模块&#xff0c;搭载系统&#xff0c;简化了外围接口&#xff0c;体积尺寸相对较小&#xff0c;主要适用于嵌入式系统。而开发板由核心板底板组成&#xff0c;提供…

探索直播美颜SDK的未来发展方向:虚拟现实、增强现实与混合

如今&#xff0c;随着虚拟现实&#xff08;VR&#xff09;、增强现实&#xff08;AR&#xff09;和混合现实&#xff08;MR&#xff09;等技术的不断发展&#xff0c;直播美颜SDK的未来发展方向也将面临着更多的可能性和挑战。今天我将与大家共同探讨直播美颜SDK在虚拟现实、增…

国科大网络行为学导论代码作业--更新中

一、Xray安装 参考自&#xff1a;Xray的安装与使用&#xff08;超详细&#xff09;_xray使用教程-CSDN博客 下载网址&#xff1a;Releases chaitin/xray GitHub 解压 双击安装 生成证书 cd到xray目录&#xff0c;生成证书 复制链接 然后cd到xray目录 .\xray_windows_amd6…