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

Linux系统:线程互斥

Linux系统:线程互斥

    • 线程互斥
      • 互斥锁 mutex
      • 互斥锁原理
    • 常见的锁
      • 死锁
      • 自旋锁 spinlock
      • 其它锁


线程互斥

讲解线程互斥前,先看到一个抢票案例:

class customer
{
public:int _ticket_num = 0;pthread_t _tid;string _name;
};int g_ticket = 10000;void* buyTicket(void* args)
{customer* cust = (customer*)args;while(true){if(g_ticket > 0){usleep(1000);cout << cust->_name << " get ticket: " << g_ticket << endl;g_ticket--;cust->_ticket_num++;}else{break;}}return nullptr;
}int main()
{vector<customer> custs(5);for(int i = 0; i < 5; i++){custs[i]._name= "customer-" + to_string(i + 1);pthread_create(&custs[i]._tid, nullptr, buyTicket, &custs[i]);}for(int i = 0; i < 5; i++){pthread_join(custs[i]._tid, nullptr);}for(int i = 0; i < 5; i++){cout << custs[i]._name << " get tickets: " << custs[i]._ticket_num << endl;}return 0;
}

这个案例比较复杂,我们的目标是:设定一个全局变量g_ticket,然后派出五个线程来模拟顾客,进行抢票,每次抢票的时候g_ticket--,直到g_ticket <= 0,也就是票被抢光了,就停止抢票。

首先我封装了一个类class customer

class customer
{
public:int _ticket_num = 0;pthread_t _tid;string _name;
};

其代表一个顾客,本质来说是线程模拟的顾客,_ticket_num 表示该顾客抢到的票数,_tid表示这个线程的TID_name则为该顾客的名字。

随后让线程去执行buyTicket函数:

void* buyTicket(void* args)
{customer* cust = (customer*)args;while(true){if(g_ticket > 0){usleep(1000);cout << cust->_name << " get ticket: " << g_ticket << endl;g_ticket--;cust->_ticket_num++;}else{break;}}return nullptr;
}

一开始线程就进入while循环,只要g_ticket > 0就抢票,让g_ticket--cust->_ticket_num++;,表示总票数减少,自己的票数加一。再输出cust->_name << " get ticket: " << g_ticket,含义为:xxx 抢到了第 xxx 张票

主函数中:

int main()
{vector<customer> custs(5);for(int i = 0; i < 5; i++){custs[i]._name= "customer-" + to_string(i + 1);pthread_create(&custs[i]._tid, nullptr, buyTicket, &custs[i]);}for(int i = 0; i < 5; i++){pthread_join(custs[i]._tid, nullptr);}for(int i = 0; i < 5; i++){cout << custs[i]._name << " get tickets: " << custs[i]._ticket_num << endl;}return 0;
}

一开始用vector创建了五个customer,第一个for循环将这些customer进行初始化,给他们命名,并创建线程。此时线程就已经开始进行抢票了,随后主线程第二个for循环等待这五个线程。最后一个for循环输出每个线程抢到的票的数目。

输出结果:

在这里插入图片描述

奇怪的事情发生了,我们只有10000张票,最后却抢出了10005张票!最后几个线程,抢到了不存在的0,-1,-2,-3号的票,为什么会多出五张票?

我简化一下模型:

在这里插入图片描述

如图所示,现在有两个线程customer-1customer-2,它们共同争夺g_ticketg_ticket = 1,也就是说只有一个人可以抢到票。它们都执行左侧的代码,只要g_ticket > 0,就g_ticket--减少一个票,然后cust->_ticket_num++,表示自己的票数增加。

现在假设线程customer-1先调度:

在这里插入图片描述

该线程先判断,发现g_ticket > 0,于是进入第一个if语句,进行g_ticket--但是g_ticket--本质上是多条汇编语句,比如下面这样

MOV  eax, [0x1000]  ; 读取 g_ticket 的值
DEC  eax            ;1
MOV  [0x1000], eax  ; 将值写回 g_ticket

也许你看不懂这个指令,我简单讲解一下:第一行MOVE的作用,是把内存中g_ticket 的数据拷贝到CPU中,第二行是将g_ticket - 1,第三行是减法后的结果拷贝回内存中的g_ticket

那么假设我们现在执行到汇编的第二条指令:

在这里插入图片描述

现在突然线程customer-1的时间片结束了,要结束调度当前线程,去调度customer-2了。请问当前内存中的g_ticket被修改了吗?不还没有,这是下一条汇编的作用,于是CPU保存当前线程customer-1的上下文,切换调度customer-2

在这里插入图片描述

此时线程customer-2也通过if (g_ticket > 0)检测还有没有票,结果发现还有一张票,于是custome-2也去抢这张票,执行g_ticket--

这下出问题了,刚刚我们的customer-1已经抢了这张票,但是还没来得及把g_ticket变成0,此时customer-2又进来抢了一次票。最后就会出现一张票被两个人抢到的问题!

也就是说,为什么最开始的案例中,会出现10005张票,就是因为最后一张票,被五个线程同时抢到了!当g_ticket已经被抢走时,由于没来得及g_ticket = 0,导致后来的线程以为还有票

我先引入一部分概念,方便大家理解后续知识:

  • 临界资源:以上案例中,g_ticket是共享资源,多个线程共享。我们把这种资源称为临界资源
  • 临界区:访问临界资源的代码,叫做临界区。比如g_ticket--就是临界区代码,以为其访问了临界资源g_ticket
  • 原子性:表示一个操作对外表现只有两种状态:还没开始已经结束

我用刚才的案例帮助大家理解这个原子性的概念:我们说g_ticket--本质上会变成多条汇编语句,也就是说g_ticket--是有过程的,而不是一瞬间完成的。

这就导致在我还没有完成g_ticket--的时候,也就是在g_ticket--过程中,被其他线程打断了。导致其它线程收到错误的信息,抢到不存在的票。

如果说一个线程抢到票后,g_ticket--会立马执行完毕,下一个线程在访问这个g_ticket > 0的时候,一定是在别人已经g_ticket--完毕,而不是在g_ticket--过程中,就可以避免这个问题。这就要求访问g_ticket原子性的,也就是说在别的线程眼中,根本就不存在g_ticket--的过程,要么你没有执行g_ticket--,要么已经执行完毕。

临界区代码只要保证是原子性的,就可以避免这样线程之间错误的抢占资源相同资源的问题

那么要如何保证临界区代码是原子性的呢?此时就需要线程互斥了!

线程互斥指的是在多线程环境中,多个线程访问同一个共享资源时,只允许一个线程访问,其他线程必须等待,直到当前线程访问完成才能继续访问。

线程互斥,是通过来实现的。

锁的规则如下:

  1. 代码必须要有互斥行为:当代码进入临界区时,不允许其它线程进入临界区
  2. 如果多个线程都想执行临界区代码,并且当前临界区没有线程在执行代码,只允许一个线程进入临界区
  3. 线程不能阻止其他线程进入临界区

简单来说就是:任何时候临界区都只能有一个线程执行


互斥锁 mutex

互斥锁pthread库提供的,英文名为mutex(互斥),需要头文件<pthread.h>,先讲解互斥锁的基本创建和销毁方法。

互斥锁的类型是pthread_mutex_t,分为全局互斥锁局部互斥锁,它们的创建方式不同。

全局mutex

想要创建一个全局的互斥锁很简单,直接定义即可:

pthread_mutex_t xxx = PTHREAD_MUTEX_INITIALIZER;

这样就创建了一个名为xxx的变量,类型是pthread_mutex_t,即这个变量是一个互斥锁全局的互斥锁必须用宏PTHREAD_MUTEX_INITIALIZER进行初始化!

另外,全局的互斥锁不需要手动销毁

局部mutex

局部的互斥锁是需要通过接口来初始化与销毁的,接口如下:

pthread_mutex_init

pthread_mutex_init函数用于初始化一个互斥锁,函数原型如下:

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

参数:

  • restrict mutex:类型为pthread_mutex_t *的指针,指向一个互斥锁变量,对其初始化
  • restrict attr:用于设定该互斥锁的属性,一般不用,设为空指针即可

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


pthread_mutex_destroy

pthread_mutex_destroy函数用于销毁一个互斥锁,函数原型如下:

int pthread_mutex_destroy(pthread_mutex_t *mutex);

参数:类型为pthread_mutex_t *的指针,指向一个互斥锁变量,销毁该锁

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


创建好互斥锁后,就要使用这个锁,主要是两个操作:申请锁释放锁

三个函数的原型如下:

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:用于申请锁,如果申请失败,就阻塞等待,直到申请到锁;如果申请成功,就执行临界区代码
  • pthread_mutex_trylock:用于申请锁,如果申请失败,直接返回,而不是等待;如果申请成功,就执行临界区代码
  • pthread_mutex_unlock:用于释放锁,表明自己已经访问完毕临界区,其他线程可以来访问了

这三个函数的参数都是pthread_mutex_t *mutex,即指向互斥锁变量的指针,表示要操作哪一个互斥锁。

接下来我们修改一下最初的抢票代码,给它加锁,保证抢票g_ticket--的原子性:

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; //全局互斥锁void *buyTicket(void *args)
{customer *cust = (customer *)args;while (true){pthread_mutex_lock(&mutex); // 加锁if (g_ticket > 0){usleep(1000);cout << cust->_name << " get ticket: " << g_ticket << endl;g_ticket--;pthread_mutex_unlock(&mutex); // 解锁cust->_ticket_num++;}else{pthread_mutex_unlock(&mutex); // 解锁break;}}return nullptr;
}

我在此使用的是全局的互斥锁,第一行pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;就是定义了一个全局的互斥锁,并对其初始化。

在访问临界区前,对mutex加锁,在此我在if (g_ticket > 0)前加锁,因为不仅仅是g_ticket--是临界区,if (g_ticket > 0)也是临界区,它们都访问了临界资源g_ticket

if的第一个分支中,当g_ticket--完毕,此时当前线程就不会再访问g_ticket了,于是离开临界区,并对mutex解锁。在第二个分支else中,线程马上要break出循环了,并且退出,此时也要解锁,不然别的线程永远处于阻塞状态了。

可以想象一下,当第一个线程被调度,它要进行抢票,现在先对mutex加锁,然后再去if中访问g_ticket。假如在某个访问临界资源的过程中,CPU调度了其它线程,此时第二个线程进入。第二个线程也想访问g_ticket,于是也对mutex加锁,但是由于锁已经被第一个线程申请走了,此时第二个线程pthread_mutex_lock就会失败,然后阻塞等待

等到第一个线程再次被调度,访问完临界区后,对mutex解锁,此时锁又可以被申请了。于是线程二申请到锁,再去访问g_ticket加锁可以保证,任何时候都只有一个线程访问临界区。当第二个线程访问临界区时,一定是其他线程访问完毕了临界区,或者其它线程还没有访问临界区。这就保证了临界区的原子性,从而维护线程的安全!

输出结果:

在这里插入图片描述

最后的结果中,2158 + 2026 + 1690 + 2018 + 2108 = 10000,不多不少。


互斥锁原理

那么互斥锁是如何做到的呢?

互斥锁的汇编伪代码如下:

加锁lock

moveb $0, %al
xchgb %al, mutex
if (al寄存器的内容 > 0){return 0;
}else挂起等待;
goto lock

接下来我讲解一下这个过程:

在这里插入图片描述

如图所示,现在有两个线程thread-1thread-2,它们共同征用内存中的锁mutex。在CPU中有一个寄存器%al,用于存储和锁的值。

现在假设thread-1进行调度执行pthread_mutex_lock

首先执行指令moveb $0, %al,你可以理解为,就是把%al寄存器内部的值变成0

在这里插入图片描述

随后执行xchgb %al, mutex,该过程是让内存中的mutex%al寄存器的值进行交换:

在这里插入图片描述

此时%al寄存器的值变成1mutex的值变成0。随后执行:

if (al寄存器的内容 > 0){return 0;
}else挂起等待;

也就是说判断当前%al内部的值是0还是大于0,如果大于0那么说明争夺到了锁,此时函数pthread_mutex_lock返回0,表示加锁成功。否则执行else进行挂起等待。

这样一个线程就征用到了一把锁。

现在假设thread-1执行到第一条汇编语句后,%al的值还是0thread-2调度了:

在这里插入图片描述

现在thread-1保存自己的硬件上下文,包括%al = 0在内,随后therad-2进入:

在这里插入图片描述

现在thread-2执行了两行汇编语句,成功把内存中的mutex与自己的%al交换,申请到了锁,此时thread-1再次调度,thread-2拷贝走自己的硬件上下文:

在这里插入图片描述

恢复硬件上下文后,thread-1%al等于0,执行第二条语句后,%almutex依然是0,这表明锁已经别的线程拿走了,此时在执行if内部的内容,thread-1挂起等待。

可以看到,其实锁的本质,就是保证mutex变量中以及所有访问锁的线程的%al寄存器中,只会有一个非零值。只有拿到非零值的线程才有资格去访问临界资源。其它线程如果要再次申请锁,由于自己的%almutex都是0,就算交换后还是0,也申请不到锁。

并不是谁先调用ptherad_mutex_lock,谁就先抢到锁,而是谁先执行该函数内部的xchgb %al, mutex语句,把非零值放到自己的%al中,谁才抢到锁

再简单看看解锁:

unlock

moveb $1, mutex
唤醒等待mutex的线程;
return 0;

解锁就很简单了,moveb $1, mutex就是把自己的%al中的1还给mutex,然后唤醒所有等待该锁的线程,让它们再次争夺这把锁。最后return 0,也就是pthread_mutex_unlock函数返回0


常见的锁

Linux中不仅仅存在互斥锁这一种锁,还有非常多的锁,接下来我们看看其它的锁。

死锁

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

我简单举一个例子:

现在有两个线程thread-1thread-2,以及两把互斥锁mutex-1mutex-2

在这里插入图片描述

现在要求:一个线程想要访问临界资源,必须同时持有mutex-1mutex-2。随后therad-1去申请了mutex-1thread-2去申请了mutex-2

在这里插入图片描述

thread-1再去申请mutex-2,结果mutex-2已经被therad-2占用了,thread-1陷入阻塞:

在这里插入图片描述

thread-2再去申请mutex-1,结果mutex-1已经被therad-1占用了,thread-2陷入阻塞:

在这里插入图片描述

现在therad-1等待therad-2解锁mutex-2thread-2等待thread-1解锁mutex-1,双方互相等待。由于唤醒thread-2需要therad-1,唤醒therad-1又需要therad-2此时陷入永远的等待状态,这就是死锁

想要造成死锁,有四个必要条件:

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

以上是比较正式的说法,接下来我从线程角度简单翻译翻译:

  1. 互斥条件临界资源同时只能被一个线程访问=
  2. 请求与保持条件:请求是指:申请对方的锁,保持是指:占着自己有的锁不放
  3. 不剥夺条件:一个线程如果申请锁失败,强行抢走他人的锁
  4. 循环等待条件:以刚刚的死锁为例,therad-1thread-2,而thread-2等待thread-1,形成一个头尾相接的循环

这四个条件都是必要条件,也就是说:

解决死锁,本质就是破坏一个或多个必要条件

主要有以下方式避免死锁:

  1. 破坏互斥条件:不要用锁
  2. 破坏请求与保持条件:如果发现没有申请到锁,立刻释放自己的全部锁
  3. 破坏不剥夺条件:如果发现没有申请到锁,强行释放对方的锁,将其占为己有
  4. 破坏循环等待条件:如果申请多把锁,所有线程都必须按照相同的顺序申请(最简单的方式)

另外的,还有一些死锁的相关算法:死锁检测算法银行家算法,本博客就不做解释了。


自旋锁 spinlock

我们先前讲的锁,其机制是这样的:

在这里插入图片描述

当线程申请一个锁失败,就会阻塞等待当锁被使用完毕,唤醒所有等待该锁的线程

其实锁还有一种不用阻塞等待的策略,而是反复检测的策略,就像这样:

在这里插入图片描述

当线程没有申请到锁,一段时间后再次检测这个锁有没有被释放,一直反复申请这个锁,这个过程叫做自旋。基于这个策略来申请的锁,叫做自旋锁

Linux自带了自旋锁spinlock,类型为pthread_spinlock_t,接口如下:

创建与销毁

int pthread_spin_init(pthread_spinlock_t *lock, int pshared);
int pthread_spin_destroy(pthread_spinlock_t *lock);

加锁与解锁

int pthread_spin_lock(pthread_spinlock_t *lock);
int pthread_spin_trylock(pthread_spinlock_t *lock);
int pthread_spin_unlock(pthread_spinlock_t *lock);

你会发现,这和mutex几乎一摸一样,所以接口也就不讲解了。

不过我这里要强调一点,pthread_spin_lock并不是申请失败就返回,而是在pthread_spin_lock内部以自旋的方式申请锁,我们无需手动模拟自旋的过程


其它锁

  • 悲观锁:在每次取数据时,总是担心数据会被其他线程修改,所以会在取数据前先加锁(读锁写锁行锁等),当其他线程想要访问数据时,被阻塞挂起。
  • 乐观锁:每次取数据时候,总是乐观的认为数据不会被其他线程修改,因此不上锁。但是在更新数据前,会判断其他数据在更新前有没有对数据进行修改。主要采用两种方式:版本号机制和CAS操作。
  • CAS操作:当需要更新数据时,判断当前内存值和之前取得的值是否相等。如果相等则用新值更新。若不等则失败,失败则重试,一般是一个自旋的过程,即不断重试。
  • 公平锁非公平锁

以上出现的所有概念,本博客都不讲解。


相关文章:

Linux系统:线程互斥

Linux系统&#xff1a;线程互斥 线程互斥互斥锁 mutex互斥锁原理 常见的锁死锁自旋锁 spinlock其它锁 线程互斥 讲解线程互斥前&#xff0c;先看到一个抢票案例&#xff1a; class customer { public:int _ticket_num 0;pthread_t _tid;string _name; };int g_ticket 10000…...

【网络协议栈】TCP/IP相关知识点收集

TCP/IP知识点收集 1 TCP分段 在TCP/IP协议栈中&#xff0c;“MSS”&#xff08;Maximum Segment Size&#xff09;是一个关键参数&#xff0c;它指定了TCP协议在发送数据时可以使用的最大数据段&#xff08;segment&#xff09;的大小。这个参数是TCP连接建立时通过三次握手&…...

Java开发中的常用字段校验注解

在 Java 开发中&#xff0c;数据校验是确保应用程序的数据完整性和一致性的重要步骤。Java 提供了一系列注解来简化数据校验的过程&#xff0c;以下是一些常用的字段校验注解及其示例代码&#xff1a; NotNull NotNull 用于确保字段不为 null&#xff0c;适用于任何类型的字段…...

面试经验分享 | 24年6月某安全厂商HW面试经验

所面试的公司&#xff1a;某安全厂商 所在城市&#xff1a;安徽省 面试职位&#xff1a;蓝初 面试过程&#xff1a; 腾讯会议&#xff08;语音&#xff09; 面试过程&#xff1a;整体流程就是自我介绍加上一些问题问题balabalabala。。。由于面的是蓝队所以渗透部分不会太多…...

JSON学习

一、JSON 1.1 简介 JSON&#xff1a;JavaScript Object Notation是一种表示对象的方式 基于JavaScript语言的轻量级的数据交换格式;&#xff08;即:用来传输数据的一种格式&#xff09; 现在传输数据的方式更多是采用json的格式&#xff0c;渐渐代替了XML 1.2 JSON的数据表示 …...

LabVIEW在中国航天中的应用

​LabVIEW是一种系统设计平台及开发环境&#xff0c;由美国国家仪器公司&#xff08;NI&#xff09;开发。它在中国航天领域的应用非常广泛&#xff0c;涵盖了测试与测量、数据采集、控制系统设计等多个方面。以下是LabVIEW在中国航天中的几个主要应用实例&#xff1a; 1. 测试…...

编程思维的培养

培养编程思维是成为一名优秀程序员的重要步骤。编程思维不仅仅是写代码的能力&#xff0c;还包括解决问题的思维方式、对复杂系统的理解、代码优化的意识、团队合作的能力等。以下是一些培养编程思维的方法和建议。 1. 学习基础知识 1.1 掌握编程语言 从一门编程语言开始&am…...

Docker笔记-Debian容器内搭建ssh服务

登陆容器之后修改密码&#xff1a; passwd 密码设置完成后安装openssh-server apt-get install openssh-server 修改端口号为50022并添加配置 vim /etc/ssh/sshd_config 修改成 Port 50022 PasswordAuthentication yes PermitRootLogin yes 启动 rootlinux:~# /etc/in…...

爬虫的法律风险是什么?以及合法使用爬虫技术的建议。

爬虫的法律风险是什么&#xff1f; 网络爬虫技术&#xff0c;虽然在数据获取方面具有巨大优势&#xff0c;但其使用过程中可能引发的法律风险也不容忽视。这些风险主要包括违反数据保护法规、侵犯知识产权、构成不正当竞争等。下面将详细探讨网络爬虫的法律风险&#xff0c;并在…...

微信小程序允许相机访问相册

// 允许从相机和相册扫码 // uni.scanCode({ // success: function(res) { // console.log(条码类型&#xff1a; res.scanType); // console.log(条码内容&#xff1a; res.result); …...

AMEYA360 | 江苏润石最新发布12颗车规级模拟芯片

日前江苏润石再度新增12颗通过AEC-Q100 Grade1&#xff0c;满足MSL 1湿敏等级认证的车规级芯片。截止目前&#xff0c;润石科技总计有70颗Grade1 & MSL1的车规级芯片通过认证并进入量产。凭借卓越的产品技术指标和稳定的品质性能不仅展示了公司在车规级模拟芯片领域的技术积…...

PHP表单设计:确保必需字段完整性的最佳实践

在开发网页应用程序时&#xff0c;设计一个具有必需字段的PHP表单是至关重要的。必需字段是用户提交表单时必须填写的信息&#xff0c;它们对于确保数据完整性和准确性至关重要。本文将从多个方面讨论如何在PHP表单中设计必需字段&#xff0c;并探讨确保表单数据完整性的最佳实…...

CentOS 7 安装部署Cassandra4.1.5

一、Cassandra的介绍 Cassandra是一套开源分布式NoSQL数据库系统。它最初由Facebook开发&#xff0c;用于储存收件箱等简单格式数据&#xff0c;集GoogleBigTable的数据模型与Amazon Dynamo的完全分布式的架构于一身Facebook于2008将 Cassandra 开源&#xff0c;此后&#xff0…...

【数据结构与算法】对称矩阵,三角矩阵 详解

给出对称矩阵、三角矩阵的节省内存的存贮结构并写出相应的输入、输出算法。 对称矩阵和三角矩阵可以通过特殊的存储结构来节省内存。这种存储结构只存储矩阵的一部分元素&#xff0c;而不是全部元素。 对称矩阵&#xff1a;对于一个n阶对称矩阵&#xff0c;我们只需要存储主对…...

Apache IoTDB 走进东南大学,深入分享项目发展历程与收获

源于高校&#xff0c;回到高校&#xff0c;Apache IoTDB PMC 成员乔嘉林为同学们详细分享行业前瞻、研发历程与心得体会。 01 把领先的数据库知识带到校园 6 月 5 日&#xff0c;东南大学计算机科学与工程学院、软件学院、人工智能学院主办的“拔尖领航系列活动特别策划篇-第二…...

Stable Diffusion AI绘画助力建筑设计艺术创新——城市建筑设计大模型分享

大家好&#xff0c;我是向阳 今天我将针对建筑设计方面的AI大模型进行简单介绍&#xff0c;我们将通过富有想象力的关键词或结合Stable Diffusion 的ControlNet 给原本只有黑白线条的线稿变成彩色的效果图&#xff0c;可能你只需要短短几分钟就可以让黑白线稿变成几种甚至十几种…...

没有 ADetailer,ComfyUI 画图脸崩了怎么办?

我们都知道 SD 的 WebUI 中的面部修复神器是 ADetailer&#xff0c;不过它是 WebUI 的专属插件&#xff0c;在 ComfyUI 中是搜索不到这个插件的&#xff0c;但是并不代表 ComfyUI 就不能使用面部修复功能了&#xff0c;ComfyUI 中也是可以找到平替的。 今天我们就来讲讲在 Com…...

防爆气象仪的工作原理

TH-WFB5矿山气象传感器在矿山安全监测系统中扮演着至关重要的角色&#xff0c;它们能够及时发现异常情况&#xff0c;为矿山的安全运营提供可靠的数据支持。矿山气象传感器能够实时监测矿山环境中的风速、风向、温度、湿度和大气压力等关键气象参数。这些传感器采用先进的传感技…...

深度学习入门5——为什么神经网络可以学习?

在理解神经网络的可学习性之前&#xff0c;需要先从数学中的导数、数值微分、偏导数、梯度等概念入手&#xff0c;从而理解为什么神经网络具备学习能力。 1.数值微分的定义 先从导数出发理解什么是梯度。某一点的导数直观理解就是在该点的切线的斜率。在数学中导数表示某个瞬…...

Integer溢出问题

0. 背景 在刷 LeetCode 时&#xff0c;代码的执行结果与预期出现了偏差&#xff0c;原因是 Int 值超过了允许范围 [ − 2 31 , 2 31 − 1 ] [-2^{31},2^{31}-1 ] [−231,231−1]。工作中从来没有遇到过这种情况&#xff0c;之前的认知是如果 Int 中存储的值超过了允许范围也许…...

软件测试全面指南:提升软件质量的系统流程

一、引言 随着软件行业的飞速发展&#xff0c;确保软件质量、稳定性和用户体验已成为企业竞争的关键。本文档旨在为测试团队提供一套全面的软件测试指南&#xff0c;通过规范测试用例管理、功能测试、接口测试、性能测试及缺陷管理等流程&#xff0c;助力测试团队实现高效、系统…...

《逆贫大叔》:一部穿越时光的温情史诗

《逆贫大叔》&#xff1a;一部穿越时光的温情史诗 在历史的长河中&#xff0c;有些故事能够穿越时光的尘埃&#xff0c;直击人心。《逆贫大叔》就是这样一部作品&#xff0c;它不仅是一部电视剧&#xff0c;更是一段历史的缩影&#xff0c;一次心灵的触动。 背景设定&#xff1…...

【电机控制】FOC算法验证步骤——PWM、ADC

【电机控制】FOC算法验证步骤 文章目录 前言一、PWM——不接电机1、PWMA-H-50%2、PWMB-H-25%3、PWMC-H-0%4、PWMA-L-50%5、PWMB-L-75%6、PWMC-L-100% 二、ADC——不接电机1.电流零点稳定性、ADC读取的OFFSET2.电流钳准备3.运放电路分析1.电路OFFSET2.AOP3.采样电路的采样值范围…...

如何衡量llm 数据集的多样性

衡量大型语言模型&#xff08;LLM&#xff09;数据集的多样性是一个复杂的问题&#xff0c;因为多样性可以从多个角度来考虑。以下是一些常用的方法和指标来评估数据集的多样性&#xff1a; 词汇多样性&#xff1a; 类型-词符比&#xff08;Type-Token Ratio, TTR&#xff09;…...

编程天才是什么意思

编程天才是什么意思 编程天才&#xff0c;这个词汇似乎充满了神秘与敬畏的色彩。那么&#xff0c;它究竟意味着什么呢&#xff1f;在本文中&#xff0c;我们将从四个方面、五个方面、六个方面和七个方面深入探讨编程天才的内涵与外延&#xff0c;带您领略这一领域的独特魅力。…...

创建npm私包

参考文章&#xff1a; 使用双重身份验证访问 npm | npm 中文网 私有npm包的实例详解-js教程-PHP中文网 1.注册npm账号 npm官网&#xff1a; npm | Home 2.安装node 百度挺多的&#xff0c;安装完后&#xff0c;检查是否安装成功就行 3.写一个简单的模块 创建个文件夹&am…...

provider追加android:name的命名有哪些?

在Android中&#xff0c;为<provider>元素添加android:name属性时&#xff0c;命名应遵循Android组件的命名规范和包名的命名规范。以下是一些关于命名android:name的要点&#xff1a; 包名前缀&#xff1a;android:name属性的值通常应以包名开始&#xff0c;这是应用程序…...

长亭网络通信基础

长亭笔试之前就已经学过一遍了 这算温故而知新吧 TCP/IP 首先我在这里默写一下之前的7层和4层 应用层 应 【表示层 数据格式转换 传 【会话层 …...

hdfs源码解析之DFSClient

1、DFSClient类简介 DFSClient 是 Hadoop 分布式文件系统&#xff08;HDFS&#xff09;中的一个核心类&#xff0c;用于客户端与 HDFS 之间的交互。它提供了一组方法&#xff0c;使客户端应用程序可以方便地与 HDFS 进行通信&#xff0c;包括文件的读取、写入、创建、删除、重命…...

智能化立体仓库的种类有哪些?

在仓储运输系统中&#xff0c;自动化立体仓库可充分利用空间储存货物&#xff0c;故而也被称之为高层货架仓库。在实际应用中&#xff0c;自动化仓库系统是不需人工处理的情况下能自动存储和取出物料的系统。那么&#xff0c;智能化立体仓库的种类有哪些&#xff1f;下面就让小…...

小学生免费编程课/宝鸡seo优化

对于改扩建的公路、铁路等建设项目&#xff0c;如预测噪声贡献值时已包括了现有声源的影响&#xff0c;则以预测的噪声()作为评价量。能兴奋体温调节中枢&#xff0c;具有升高体温作用的激素是()下列不属于康复医学范围的是()。个人可以买公积金吗&#xff1f;如何买&#xff1…...

网站做端口是什么问题/关键词优化到首页怎么做到的

网站提高性能的方案有很多&#xff0c;网站架构方面考虑&#xff0c;最初的性能优化可以考虑提高单台服务器的配置。把数据库和代码分别部署在两台服务器&#xff0c;页面缓存&#xff0c;数据缓存&#xff0c;静态化&#xff0c;分布式&#xff0c;代码读写分离&#xff0c;负…...

辛集手机网站建设/扶贫832网络销售平台

ModelSerializer类使用 继承自Serializer前提&#xff1a;对应是Django模型类 功能&#xff1a;自动生成字段默认实现create和update转载于:https://www.cnblogs.com/oklizz/p/11278510.html...

京东网上商城跟京东是一家吗/站长工具查询seo

2019独角兽企业重金招聘Python工程师标准>>> Base64是网络上最常见的用于传输8Bit字节代码的编码方式之一&#xff0c;大家可以查看RFC2045&#xff5e;RFC2049&#xff0c;上面有MIME的详细规范。 Base64要求把每三个8Bit的字节转换为四个6Bit的字节&#xff08;3*…...

公司的网站怎么运营/资源优化网站排名

错误日志&#xff1a; -log-err查询日志&#xff1a; -log (可选)慢查询日志: -log-slow-queries (可选)更新日志: -log-update二进制日志&#xff1a; -log-bin在mysql的安装目录下&#xff0c;打开my.ini(win)、my.cnf(linux)在后面加上上面的参数&#xff0c;保存后重启mysq…...

加强统筹推进政府网站建设/app制作公司

分享一下我老师大神的人工智能教程&#xff01;零基础&#xff0c;通俗易懂&#xff01;http://blog.csdn.net/jiangjunshow也欢迎大家转载本篇文章。分享知识&#xff0c;造福人民&#xff0c;实现我们中华民族伟大复兴&#xff01;要想自己设计并实现一个代码自动分析、优化和…...