当前位置: 首页 > news >正文

Linux 线程互斥

前言

对于初学线程的伙伴来讲,多线程并发访问导致的数据不一致问题,总是让人陷入怀疑,很多人只是给你说加锁!但没有人告诉你为什么?本篇博客将详解!

目录

前言

一、线程互斥

• 为什么票会出现负数的情况?

• 互斥锁 mutex

互斥量的定义

互斥量的销毁

互斥量的加锁和解锁

初识RAII风格

总结

 • 从原理的角度理解锁

• 从实现的角度理解锁

二、可重入VS线程安全

三、常见锁的概念

• 死锁问题

• 死锁 产生的四个必要条件

• 避免死锁

• 避免死锁的算法


一、线程互斥

OK,我们先来看一个多线程并发访问的例子(模拟抢票):

class customer
{
public:customer(const std::string &name): _name(name){}std::string getName(){return _name;}private:std::string _name;      // 线程的名字
};

这里先来实现一个顾客的类,其中_name是线程的名字

int tickets = 10000;// 1W张票void* route(void* args)
{customer* cum = static_cast<customer*>(args);while (true){if(tickets > 0){usleep(1000);// 模拟抢票std::cout << cum->getName() << ", 抢到了第" << tickets << "票" << std::endl;tickets--;}else{break;}}return nullptr;
}

在实现一个模拟抢票的函数,其中这个函数当每个线程后面进入后判断当前票数大于0,说明有票,就抢票,然后打印出当前抢到的票的编号,然后让票的总数--,否则就结束掉

int main()
{std::vector<customer*> cusm; // 管理customer对象std::vector<pthread_t> tids; // 管理线程for (int i = 0; i < num; i++){std::string name = "thread_" + std::to_string(i + 1);pthread_t tid;customer *cum = new customer(name);pthread_create(&tid, nullptr, route, (void *)cum);// 创建线程cusm.emplace_back(cum);tids.emplace_back(tid);}for (auto &tid : tids){pthread_join(tid, nullptr);// 等待线程结束}return 0;
}

然后我们在这里创建num = 5个线程并启动,让你他们都执行抢票,其中创建两个数组来分被管理对象和tid,最后等待他们结束!OK,先来看结果:

这里在出现了负数票的编号?不应该是到判断是0的时候就不在抢了吗?

上面的全局变量tickets是所有的线程共享的,像这种资源叫做共享资源!上面出现的负数票编号的情况就是典型的多线程并发访问同一个共享资源导致的数据不一致问题!下面我们不急着解决,我们先来探索一下,为什么会出现这种情况!

在正式的介绍前,我们先来铺垫一些前置的知识

1、计算机中的运算不仅仅值得是算数运算,还有逻辑运算!

上面的if判断票数是否大于0就是一次逻辑运算,tickets--就是算数运算

2、CPU中的寄存器只有一套但是里面的数据可以有多套!

这里主要说的就是我们进程切换那里说的上下文数据,其实不止进程,线程也是有上下文的!

3、语言上的算数/逻辑运算底层不是一条语句!

这句话的意思就是上述的运算虽然在语言上看起来是一条语句,但是在汇编层面上可能就是很多条!上述的tickets,他是存放在内存的(直接透过虚拟地址空间),而所有的运算是CPU运算的,所以要被运算前,先得要加载到CPU的寄存器,然后在运算,完了将结果写回内存

其中执行tickets--这里有一个大坑,即他分为步执行

1、重新从内存读取数据

2、--数据

3、写回内存!

我们可以看看他的汇编

// 取出 tickets--部分的汇编代码
objdump -d a.out > test.objdump
152 40064b: 8b 05 e3 04 20 00 mov 0x2004e3(%rip),%eax
# 600b34 <ticket>
153 400651: 83 e8 01 sub $0x1,%eax
154 400654: 89 05 da 04 20 00 mov %eax,0x2004da(%rip)
# 600b34 <tickets>

这里有的朋友可能对汇编不咋熟悉,我来解释一下: 

OK,有了这些认识我们就可以介绍为什么会出现负数了!

• 为什么票会出现负数的情况?

OK,我们先把上面的模型简化一下,多线程简化为两线程,这里多线程访问共享资源的地方共有两个:

我们直接来分析一个极端的情况,当只有最后一张票时,假设此时 thread_1 先被 CPU 调度,此时 thread_1先判断,发现票数是1,然后进入抢的时候,时间片到了,此时要从 CPU 上切换下来,并将 CPU 寄存器中的数值保存到当前线程的上下文中!然后,thread_2被调度,一看票数是1大于0, 就进入了,他把票买了消息输出了,刚要做tickets--的时候,时间片到了,他也将 CPU中的数据保存到上下文中被切换走了!然后再切换回 thread_1继续执行,这次 thread_1继续执行到tickets-- 的操作,然后将此时重读内存数据1,加载到寄存器1,然后运算完后写入结果寄存器0,最后写道内存0!调度结束,thread_2又开始调度,他开始往后执行tickets--的操作,即重新读取内存的数据0,在做--操作,将结果-1放到结果寄存器,最后写入内存-1,此时不就是出现了负数了嘛!

当这里的线程不是两个而是好多个,这不就是出现了更多的负数了吗!OK,现在分析清楚了问题,那如何解决呢?答案是:加锁

• 互斥锁 mutex

Linux中的互斥锁,又称互斥量!我们先来见一见他的接口!

互斥量的定义

定义互斥量/锁的方式有两种,分别是静态和动态:

1、静态定义

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

2、动态定义

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

参数:

mutex:要初始化的互斥锁

attr:内部属性直接不关心,设为nullptr即可

返回值:

成功:返回0

失败:返回一个非0 的错误码

注意:互斥锁接口的返回值都是一样的,后续不再介绍!

互斥量的销毁

int pthread_mutex_destroy(pthread_mutex_t *mutex);

销毁互斥量需要注意:

1、使用全局/静态定义的互斥量mutex,不需要销毁!

2、不要销毁一个已经加锁的互斥量

3、已经销毁的互斥量,确保后期不会再有线程使用

互斥量的加锁和解锁

int pthread_mutex_lock(pthread_mutex_t *mutex);/*没抢到锁,阻塞时的等待*/
int pthread_mutex_trylock(pthread_mutex_t *mutex);/*没抢到锁,不发生阻塞时的等待*/
int pthread_mutex_unlock(pthread_mutex_t *mutex);/*解锁*/

调用pthread_mutex_lock时,可能会遇到以下的情况:

1、互斥量处于解锁状态,该函数会将该互斥量锁定,同时成功返回,执行后续代码

2、当他调用时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但当前线程没有抢到,此时pthread_mutex_lock会导致调用该函数的线程陷入阻塞(当前执行流被挂起到相关的阻塞队列),等待互斥量解锁,再去竞争!

3、上面的phread_mutex_lockphread_mutex_trylock的区别就是,前者当发现互斥量已经被其他的执行流给锁定时,会陷入阻塞等待,而后者不会阻塞等待,会直接返回一个错误码EBUSY,然后去执行其他的内容!

OK,介绍完互斥锁,按我们对上述的抢票改一下:

上面的代码是由于,多个执行流并发访问共享资源导致的数据不一致,为了保障数据安全,我们让他们在访问个共享资源时,串行的访问,即一个访问完了另一个在访问,所以,我们可以把锁加在访问全局的ticktes的前后

我们先来使用以下全局的锁:

pthread_mutex_t g_mutex = PTHREAD_MUTEX_INITIALIZER;// 定义一把全局的互斥锁void *route(void *args)
{customer *cum = static_cast<customer *>(args);while (true){// 对访问共享资源的前后加锁pthread_mutex_lock(&g_mutex);// 临界区if (tickets > 0){usleep(1000); // 模拟抢票std::cout << cum->getName() << ", 抢到了第" << tickets << "票" << std::endl;tickets--;// 临界区pthread_mutex_unlock(&g_mutex);// 访问结束了解锁}else{pthread_mutex_unlock(&g_mutex);// 条件不满足解锁break;}}return nullptr;
}

再来看看运行结果:

OK,这就没有问题了!介绍到这里我们在引入一批概念:

1、临界资源:多个执行流共享的资源就叫做临界资源

2、临界区:访问临界资源的代码就叫做临界区

3、互斥:任何时刻,只允许一个执行流进入临界区访问临界资源(可以保护临界资源)

4、原子性:不会被任何调度机制打断操作,该操作只有两态:要么完成、要么未完成

5、对所有临界资源的访问,本质都是通过代码访问;所以对临界资源的保护本质上是对临界区代码的保护!

其中,上述代码中tickets就是被多执行流关系的资源,即临界资源!进行对tickets进行访问的代码就叫做临界区互斥就是加锁之后只有一个执行流进入临界区访问临界资源!原子性表现在,当一个执行流加锁访问临界资源执行tickets--时要么做完,要么一次性执行完他的那三步,要么不执行!

OK,下面我们再来使用以下,局部定义的互斥锁

在这里直接将上述的互斥锁,放到线程的属性中:

const int num = 5;   // 创建5个线程
int tickets = 10000; // 1W张票class customer
{
public:customer(const std::string &name, pthread_mutex_t& mutex): _name(name), _mutex(mutex){}std::string getName(){return _name;}pthread_mutex_t &getMutex(){return _mutex;}private:std::string _name;      // 线程的名字pthread_mutex_t& _mutex; // 互斥锁
};

注意:这里的锁属性必须得是引用,也就是那把局部所的别名,这样做的目的是让不同的线程看到同一把锁!当然你这里使用指针也是可以的!

void *route(void *args)
{customer *cum = static_cast<customer *>(args);while (true){// 对访问共享资源的前后加锁pthread_mutex_lock(&cum->getMutex());// 临界区if (tickets > 0){usleep(1000); // 模拟抢票std::cout << cum->getName() << ", 抢到了第" << tickets << "票" << std::endl;tickets--;// 临界区pthread_mutex_unlock(&cum->getMutex()); // 当访问结束了就解锁}else{pthread_mutex_unlock(&cum->getMutex()); // 当访不符合条件就解锁break;}}return nullptr;
}

然后买票这里就可以直接使用了!

int main()
{pthread_mutex_t mutex; // 定义一把局部的互斥锁pthread_mutex_init(&mutex, nullptr);//初始化std::vector<customer *> cusm; // 管理线程std::vector<pthread_t> tids; // 管理线程for (int i = 0; i < num; i++){std::string name = "thread_" + std::to_string(i + 1);pthread_t tid;customer *cum = new customer(name, mutex);pthread_create(&tid, nullptr, route, (void *)cum);// 创建线程cusm.emplace_back(cum);tids.emplace_back(tid);}for (auto &tid : tids){pthread_join(tid, nullptr);// 等待线程结束}for(auto& e : cusm){delete e;// 释放资源}pthread_mutex_destroy(&mutex);// 释放锁return 0;
}

然后使用完之后,记得将锁释放!OK,看一下效果:

初识RAII风格

OK,互斥锁的相关接口比较简单,代码也比较好写!但是让人麻烦的一点是:每次都得手动的加锁和解锁,如果忘记解锁,会导致其他线程一直阻塞!为了解决这个问题,我们实现一个小的组件

这里也就是实现一个专门管理锁的类(LockGuard类):

#ifndef __LOCKGUARD
#define __LOCKGUARD#include <pthread.h>class LockGuard
{
public:LockGuard(pthread_mutex_t &mutex):_mutex(mutex){pthread_mutex_lock(&_mutex);}~LockGuard(){pthread_mutex_unlock(&_mutex);}private:pthread_mutex_t &_mutex; // 注意这里得是引用!
};#endif

然后有了这个类之后,我们就不再担心忘记解锁了:

这里就完美的结合了类和对象的特性!构造时加锁,用完之后,不管你是时间片完了结束还是没有满足条件的结束,此时出了作用域对象就会调用析构函数,自动解锁了~!很优雅!!

像这种 获取资源即初始化 的风格称为 RAII 风格,由 C++ 之父 本贾尼·斯特劳斯特卢普 提出,非常巧妙的运用了 类和对象 的特性,实现半自动化操作!

这里我们是第一次见RAII,后面我们在介绍C++11智能指针的时候我们再来谈~!

总结

1、加锁是让临界资源保证"安全"的,在加锁时当尽量使得加锁的粒度小(加锁的代码行数少)

2、线程申请锁成功了,在执行临界区期间,可以被切换走吗?

答案是肯定的,如果当前线程持有锁被切走了,其他线程依然在阻塞,因为被切走的线程没有释放锁!这样其实也保证了,我在访问临界区时对于其他线程是原子的~!

 • 从原理的角度理解锁

原理角度其实上面我们都介绍过了,本质就是让多执行流在执行临界区的代码访问临界资源时,一个一个的来,即串行的访问,当一个线程持有锁时,对于其他线程是原子的!OK,这都没有问题,我在的问题是:

1、如何理解申请锁成功,允许你进入临界区?

加锁本质上是去调用,pthread_mutex_lock函数,当他的内部判断当前线程是持有锁资源时成功返回!当pthread_mutex_lock函数返回后,不就是可以继续执行后面的代码了吗!后面不就是临界区嘛!

2、如何理解申请锁失败,允许你进入临界区?

pthread_mutex_lock函数内部判断,当前线程不具有锁时,直接阻塞住!其实内部可能也就是一个判断,将当前的线程挂接到特定的阻塞队列!等到上一个线程把锁释放了再去把他们唤醒,开始竞争锁资源~!其实,这个和我们C语言的scanf一样,当CPU执行到它时,会检测键盘是否输入了数据,没有输入不就是一直的卡着嘛!

• 从实现的角度理解锁

所有线程在竞争锁的前提是得让他们都能够看到同一把锁,即锁本身也是共享资源,那加锁的过程也必须也得是原子的!

现在的问题是如何保证加锁的过程也是原子的呢?

在如今,大多数 CPU 的体系结构(比如 ARMX86AMD 等)都提供了 swap 或者 exchange 指令,这种指令可以把 寄存器 和 内存单元 的数据 直接交换,由于这种指令只有一条语句,可以保证指令执行时的 原子性

即便是在多处理器环境下(总线只有一套),访问内存的周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期,即 swap 和 exchange 指令在多处理器环境下也是原子的

我们来看看一段pthread_mutex_lock的汇编伪代码:

lock:movb $0, %alxchgb %al, mutexif(al寄存器里的内容 > 0){return 0;} else挂起等待;goto lock;

这里的意思是先将0放到al寄存器,然后将内存中的值和al寄存器中的值交换(使用的时swap/exchange指令,原子的)再下来就是判断al中的值是否大于0,如果是返回!否则挂起等待!等到有锁资源了再去继续执行上述的操作,继续判断!

OK,我们现在回头看前面的加锁过程,假设有两个线程加锁,需要申请锁资源:

同理多线程也是一样的!OK,再来看看,解锁的汇编伪代码:

unlock:movb $1, mutex唤醒等待 [锁资源] 的线程;return

相比较加锁,解锁简单的多!这里只是一个lock中的值进行swap/exchange并没有新增值,多个线程都在竞争这一个值~!这就是加锁和解锁的实现原理!至于各种被线程执行某条汇编指令时被切出的情况,都可以不会影响整体 加锁 情况~!

总结

  • 加锁是一个让不让你通过的策略
  • 交换指令 swap exchange 是原子的,确保 锁 这个临界资源不会出现问题
  • 未获取到 [锁资源] 的线程会被阻塞至 pthread_mutex_lock 
  • 数据在内存中是被多执行流共享的,但是加载到CPU后就将共享变成了私有!

二、可重入VS线程安全

概念

1、线程安全:多个线程并发访问同一段代码时,不会出现不同的结果。常见对全局变量或者静态变量进行操作,并且没有锁保护的情况下,会出现该问题!

2、重入:同一个函数被不同的执行流调用,当前一个执行流还没有执行完,就有其他的执行流再次进入,这被称为重入!一个函数在重入情况下,运行结果不会出现任何不同或者任何问题,则称该函数为可重入函数,否则,是不可重入函数;

常见的线程不安全的情况

• 不保护共享变量的函数

• 函数状态随着被调用,状态发生变化的函数、

• 返回指向静态变量指针的函数

• 调用线程不安全函数 的函数

常见线程安全的情况

• 每个线程对全局变量或静态变量只有读取权限,而没有写入权限,一般来说都是线程安全的

• 类或者接口对于线程来说都是原子操作

• 多个线程之间的切换不会导致该接口的执行结果存在二义性

常见可重入的情况

• 调用了 malloc/free 函数,因为 malloc 函数使用全局链表来管理堆的

• 调用标准I/O库函数,标准I/O库函数的很多实现都以不可重入的方式,使用全局数据结构

• 可重入函数内使用了静态的数据结构

常见的可重入的情况

• 不适用全局或者静态的变量

• 不适用malloc 或者 new 开辟出的内存空间

• 不调用不可重入函数

• 不返回静态或者全局的数据,所有数据都由函数的调用者提供

• 使用本地数据,或者通过制作全局数据的本地拷贝来保护全局的数据

可重入与线程安全的联系

• 函数是可重入的,那就是线程安全的

• 函数是不可重入的,那就不能由多个线程使用,否则有可能引起线程安全的问题

• 如果一个函数中有全局变量,那么这个函数既不是线程安全,也不是可重入的

可重入与线程安全的区别

• 可重入函数是线程安全函数的一种

• 线程安全不一定是可重入的,二可重入函数一定是线程安全的

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

三、常见锁的概念

• 死锁问题

死锁指在一组进程中的各个线程均占有不会释放的资源,但因相互申请被其他线程所占用不会释放的资源处于一种永久等待状态

概念比较绕,简单举个例子

两个小朋各持 五毛钱 去商店买东西,俩人同时看中了一包 辣条,但这包 辣条 售价 一块钱,两个小朋友都想买了自己吃,但彼此的钱都不够,双方互不谦让,此时局面就会僵持不下

两个小朋友:两个不同的线程
辣条:临界资源
售价:访问临界资源需要的锁资源数量,这里需要两把锁
两个小朋友各自手里的钱:一把锁资源
僵持不下的场面:形成死锁,导致程序无法继续运行

所以死锁就是 多个线程都因锁资源的等待而被同时挂起,导致程序陷入 死循环

只有一把锁会造成死锁吗?
答案是 会的,如果线程 thread_A 申请锁资源,访问完临界资源后没有释放,会导致 线程 thread_B无法申请到锁资源,同时线程 thread_A 自己也申请不到锁资源了,不就是 死锁 吗!

• 死锁 产生的四个必要条件

1、互斥:一个资源每次只能被一个执行流使用

2、请求与保持:一个执行流因请求资源而阻塞时,对已获得的资源保持不释放

3、环路等待:若干执行流之间形成一种首尾相接的循环等待资源关系

4、不剥夺条件:不能强行剥夺其他线程的资源

注意:只有四个条件都满足了,才会引发 死锁 问题!!!

避免死锁

1、破坏死锁的四个必要条件

2、加锁的顺序一致

3、避免锁未释放的场景

4、资源一次性分配

避免死锁的算法

1、死锁检测算法(了解)

2、银行家算法(了解)


OK,好兄弟本期分享就到这里,我是cp我们下期再见~!

相关文章:

Linux 线程互斥

前言 对于初学线程的伙伴来讲&#xff0c;多线程并发访问导致的数据不一致问题&#xff0c;总是让人陷入怀疑&#xff0c;很多人只是给你说加锁&#xff01;但没有人告诉你为什么&#xff1f;本篇博客将详解&#xff01; 目录 前言 一、线程互斥 • 为什么票会出现负数的情…...

【Redis 源码】6AOF持久化

1 AOF功能说明 aof.c 文件是 Redis 中负责 AOF&#xff08;Append-Only File&#xff09;持久化的核心文件。AOF 持久化通过记录服务器接收到的每个写命令来实现数据的持久化。这样&#xff0c;在 Redis 重启时&#xff0c;可以通过重放这些命令来恢复数据。 2 AOF相关配置 a…...

6.MySQL基本查询

目录 表的增删查改Insert&#xff08;插入&#xff09;插入替换插入替换2 Retrieve&#xff08;查找&#xff09;SELECT 列全列查找指定列查询查询字段为表达式为查询结果指定别名结果去重 WHERE 条件order by子句筛选分页结果 Update&#xff08;更新&#xff09;delete&#…...

Linux字符设备驱动开发

Linux 字符设备驱动开发是内核模块开发中的一个重要部分&#xff0c;主要用于处理字节流数据设备&#xff08;如串口、键盘、鼠标等&#xff09;。字符设备驱动的核心任务是定义如何与用户空间程序交互&#xff0c;通常通过一组文件操作函数进行。这些函数会映射到 open、read、…...

HTML5+JavaScript绘制闪烁的网格错觉

HTML5JavaScript绘制闪烁的网格错觉 闪烁的网格错觉&#xff08;scintillating grid illusion&#xff09;是一种视觉错觉&#xff0c;通过简单的黑白方格网格和少量的精心设计&#xff0c;能够使人眼前出现动态变化的效果。 闪烁的栅格错觉&#xff0c;是一种经典的视觉错觉…...

每日OJ题_牛客_拼三角_枚举/DFS_C++_Java

目录 牛客_拼三角_枚举/DFS 题目解析 C代码1 C代码2 Java代码 牛客_拼三角_枚举/DFS 拼三角_枚举/DFS 题目解析 简单枚举&#xff0c;不过有很多种枚举方法&#xff0c;这里直接用简单粗暴的枚举方式。 C代码1 #include <iostream> #include <algorithm> …...

[uni-app]小兔鲜-01项目起步

项目介绍 效果演示 技术架构 创建项目 HBuilderX创建 下载HBuilderX编辑器 HBuilderX/创建项目: 选择模板/选择Vue版本/创建 安装插件: 工具/插件安装/uni-app(Vue3)编译器 vue代码不能直接运行在小程序环境, 编译插件帮助我们进行代码转换 绑定微信开发者工具: 指定微信开…...

安全的价值:构建现代企业的基础

物理安全对于组织来说并不是事后才考虑的问题&#xff1a;它是关键的基础设施。零售商、医疗保健提供商、市政当局、学校和所有其他类型的组织都依赖安全系统来保障其人员和场所的安全。 随着安全技术能力的不断发展&#xff0c;许多组织正在以更广泛的视角看待他们的投资&am…...

门面(外观)模式

简介 门面模式&#xff08;Facade Pattern&#xff09;又叫作外观模式&#xff0c;提供了一个统一的接口&#xff0c;用来访问子系统中的一群接口。其主要特征是定义了一个高层接口&#xff0c;让子系统更容易使用&#xff0c;属于结构型设计模式。 通用模板 创建子系统角色类…...

kotlin flow 使用

1 创建flow 方式1 通过携程扩展函数FlowKt中的flow扩展函数可以直接构建flow&#xff0c;只需要传递FlowCollector收集器实现类就可以了 private fun create1(){val intFlow createFlow()println("创建int flow: $intFlow")runBlocking {println("开始收集&…...

vue3 实现文本内容超过N行折叠并显示“...展开”组件

1. 实现效果 组件内文字样式取决与外侧定义 组件大小发生变化时,文本仍可以省略到指定行数 文本不超过时, 无展开,收起按钮 传入文本发生改变后, 组件展示新的文本 2. 代码 文件名TextEllipsis.vue <template><div ref"compRef" class"wq-text-ellip…...

根据源码解析Vue2中对于对象的变化侦测

UI render(state) VUE的特点是数据驱动视图&#xff0c;在这里可以把数据理解为状态&#xff0c;而视图就是用户可以看到的页面&#xff0c;页面是动态变化的&#xff0c;而他的变化或是用户操作引起&#xff0c;或是后端数据变化引起&#xff0c;这些都可以说是数据的状态变…...

爬虫技术深潜:探究 JsonPath 与 XPath 的语法海洋与实战岛屿

Python爬虫中JSON与XML字符串的XPath和JsonPath过滤语法区别对比 在信息爆炸的互联网时代&#xff0c;数据抓取成为了获取宝贵信息的关键技能。对于技术爱好者&#xff0c;特别是Python程序员来说&#xff0c;熟练掌握JSON和XML数据解析方法至关重要。本文旨在深入探讨这两种格…...

纠删码参数自适应匹配问题ECP-AMP实验方案(一)

摘要 关键词&#xff1a;动态参数&#xff1b;多属性决策&#xff1b;critic权重法&#xff1b;DBSCA聚类分析 引言 云服务存储系统是一种基于互联网的数据存储服务&#xff0c;它可以为用户提供大规模、低成本、高可靠的数据存储空间。云服务存储系统的核心技术之一是数据容…...

五、人物持有武器攻击

一、手部添加预制体&#xff08;武器&#xff09; 1、骨骼&#xff08;手&#xff09; 由于人物模型有骨骼和动画&#xff0c;在添加预制体后&#xff0c;会抓握武器 建一个预制体在手部位置 二、添加武器拖尾 下载拖尾特效 赋值特效中的代码&#xff0c;直接使用 清空里面…...

mysql索引 -- 全文索引介绍(如何创建,使用),explain关键字

目录 全文索引 引入 介绍 创建 使用 表数据 简单搜索 explain关键字 使用全文索引 mysql索引结构详细介绍 -- mysql索引 -- 索引的硬件理解(磁盘,磁盘与系统),软件理解(mysql,与系统io,buffer pool),索引结构介绍和理解(page内部,page之间,为什么是b树)-CSDN博客 全文…...

Wayfair封号的常见原因及解决方案解析

近期关于Wayfair账号封禁的问题引发了广泛讨论。许多用户报告称&#xff0c;他们的Wayfair账户被突然封禁&#xff0c;这一现象不仅影响了用户的购物体验&#xff0c;也对Wayfair的品牌形象造成了一定的冲击。本文将深入探讨Wayfair封号的原因&#xff0c;并提出相应的解决方案…...

计算机视觉方面的一些模块

# __all__ 是一个可选的列表&#xff0c;定义在模块级别。当使用 from ... import * 语句时&#xff0c;如果模块中定义了 # __all__&#xff0c;则只有 __all__ 列表中的名称会被导入。这是模块作者控制哪些公开API被导入的一种方式。 # 使用 * 导入的行为 # 如果模块中有 __a…...

进阶美颜功能技术开发方案:探索视频美颜SDK

视频美颜SDK&#xff08;SoftwareDevelopmentKit&#xff09;作为提升视频质量的重要工具&#xff0c;越来越多地被开发者关注与应用。接下俩&#xff0c;笔者将深入探讨进阶美颜功能的技术开发方案&#xff0c;助力开发者更好地利用视频美颜SDK。 一、视频美颜SDK的核心功能 …...

【重学 MySQL】三十八、group by的使用

【重学 MySQL】三十八、group by的使用 基本语法示例示例 1: 计算每个部门的员工数示例 2: 计算每个部门的平均工资示例 3: 结合 WHERE 子句 WITH ROLLUP基本用法示例注意事项 注意事项 GROUP BY 是 SQL 中一个非常重要的子句&#xff0c;它通常与聚合函数&#xff08;如 COUNT…...

前端倒计时误差!

提示:记录工作中遇到的需求及解决办法 文章目录 前言一、误差从何而来?二、五大解决方案1. 动态校准法(基础版)2. Web Worker 计时3. 服务器时间同步4. Performance API 高精度计时5. 页面可见性API优化三、生产环境最佳实践四、终极解决方案架构前言 前几天听说公司某个项…...

LeetCode - 394. 字符串解码

题目 394. 字符串解码 - 力扣&#xff08;LeetCode&#xff09; 思路 使用两个栈&#xff1a;一个存储重复次数&#xff0c;一个存储字符串 遍历输入字符串&#xff1a; 数字处理&#xff1a;遇到数字时&#xff0c;累积计算重复次数左括号处理&#xff1a;保存当前状态&a…...

【快手拥抱开源】通过快手团队开源的 KwaiCoder-AutoThink-preview 解锁大语言模型的潜力

引言&#xff1a; 在人工智能快速发展的浪潮中&#xff0c;快手Kwaipilot团队推出的 KwaiCoder-AutoThink-preview 具有里程碑意义——这是首个公开的AutoThink大语言模型&#xff08;LLM&#xff09;。该模型代表着该领域的重大突破&#xff0c;通过独特方式融合思考与非思考…...

生成 Git SSH 证书

&#x1f511; 1. ​​生成 SSH 密钥对​​ 在终端&#xff08;Windows 使用 Git Bash&#xff0c;Mac/Linux 使用 Terminal&#xff09;执行命令&#xff1a; ssh-keygen -t rsa -b 4096 -C "your_emailexample.com" ​​参数说明​​&#xff1a; -t rsa&#x…...

select、poll、epoll 与 Reactor 模式

在高并发网络编程领域&#xff0c;高效处理大量连接和 I/O 事件是系统性能的关键。select、poll、epoll 作为 I/O 多路复用技术的代表&#xff0c;以及基于它们实现的 Reactor 模式&#xff0c;为开发者提供了强大的工具。本文将深入探讨这些技术的底层原理、优缺点。​ 一、I…...

如何理解 IP 数据报中的 TTL?

目录 前言理解 前言 面试灵魂一问&#xff1a;说说对 IP 数据报中 TTL 的理解&#xff1f;我们都知道&#xff0c;IP 数据报由首部和数据两部分组成&#xff0c;首部又分为两部分&#xff1a;固定部分和可变部分&#xff0c;共占 20 字节&#xff0c;而即将讨论的 TTL 就位于首…...

鸿蒙DevEco Studio HarmonyOS 5跑酷小游戏实现指南

1. 项目概述 本跑酷小游戏基于鸿蒙HarmonyOS 5开发&#xff0c;使用DevEco Studio作为开发工具&#xff0c;采用Java语言实现&#xff0c;包含角色控制、障碍物生成和分数计算系统。 2. 项目结构 /src/main/java/com/example/runner/├── MainAbilitySlice.java // 主界…...

GitHub 趋势日报 (2025年06月06日)

&#x1f4ca; 由 TrendForge 系统生成 | &#x1f310; https://trendforge.devlive.org/ &#x1f310; 本日报中的项目描述已自动翻译为中文 &#x1f4c8; 今日获星趋势图 今日获星趋势图 590 cognee 551 onlook 399 project-based-learning 348 build-your-own-x 320 ne…...

MySQL 8.0 事务全面讲解

以下是一个结合两次回答的 MySQL 8.0 事务全面讲解&#xff0c;涵盖了事务的核心概念、操作示例、失败回滚、隔离级别、事务性 DDL 和 XA 事务等内容&#xff0c;并修正了查看隔离级别的命令。 MySQL 8.0 事务全面讲解 一、事务的核心概念&#xff08;ACID&#xff09; 事务是…...

MinIO Docker 部署:仅开放一个端口

MinIO Docker 部署:仅开放一个端口 在实际的服务器部署中,出于安全和管理的考虑,我们可能只能开放一个端口。MinIO 是一个高性能的对象存储服务,支持 Docker 部署,但默认情况下它需要两个端口:一个是 API 端口(用于存储和访问数据),另一个是控制台端口(用于管理界面…...