【Linux】Linux线程的同步与互斥

news2024/11/15 23:19:07

前言

如果你对进程/线程中通信的相关概念不太了解的话可以先看这里《进程间通信的基础概念》

Linux线程的同步与互斥

  • 一、Linux线程的互斥
    • 1、互斥的相关背景
    • 2、互斥量的接口
    • 3、互斥量实现原理探究
  • 二、可重入与线程安全
    • 1、概念
    • 2、常见的线程不安全的情况
    • 3、常见的线程安全的情况
    • 4、常见不可重入的情况
    • 5、常见可重入的情况
    • 6、可重入与线程安全联系
    • 7、可重入与线程安全区别
  • 三、死锁问题
    • 1、死锁的概念
    • 2、死锁四个必要条件
    • 3、避免死锁
  • 四、Linux线程同步
    • 1、同步引入与概念
    • 2、条件变量
    • 3、为什么pthread_cond_wait需要互斥量的理解

一、Linux线程的互斥

1、互斥的相关背景

我们先来看一段多线程抢票的代码,票数有10000张,共有4个线程

#include <iostream>
#include <cstdio>
#include <cstring>
#include <pthread.h>
#include <unistd.h>

using namespace std;

// 票数
int tickets = 10000;
void* threadRoutine(void* args)
{
    char* s = static_cast<char*>(args);
    while (true)
    {
        if (tickets > 0)
        {
            usleep(2000); //抢票花费的时间
            cout << s << " get a ticket, surplus number is :" << --tickets << endl;
        }
        else
        {
            break;
        }
    }
    cout << "The tickets are sold out" << endl;
    return s;
}

int main()
{
    pthread_t tname[4];
    int n = sizeof(tname) / sizeof(tname[0]);
    
    // 创建线程抢票
    for (int i = 0; i < n; i++)
    {
        char* str = new char[64];
        snprintf(str, sizeof(str), "线程-%d", i);
        pthread_create(tname + i, nullptr, threadRoutine, str);
        usleep(2000);
    }

    // 回收线程以及内存
    void* ret = nullptr;
    for (int i = 0; i < n; i++)
    {
        int error = pthread_join(tname[i], &ret);
        if (error == 0)
        {
            delete[] (char*)ret;
        }
        else
        {
            cerr << strerror(error) << endl;
        }
    }
    return 0;
}

运行结果:

在这里插入图片描述

我们看到抢票时把票数抢到了负数,这是为什么呢,我们一起来分析一下:

在这里插入图片描述

要解决上述抢票系统的问题,需要做到三点:

  1. 代码必须有互斥行为:当代码进入临界区执行时,不允许其他线程进入该临界区。
  2. 如果多个线程同时要求执行临界区的代码,并且此时临界区没有线程在执行,那么只能允许一个线程进入该临界区。
  3. 如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区。

要做到这三点,本质上就是需要一把锁,Linux上提供的这把锁叫互斥量。

在这里插入图片描述

2、互斥量的接口


①初始化互斥量

方法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: 初始化互斥量的属性,一般设置为NULL即可 。

返回值说明:

  • 互斥量初始化成功返回0,失败返回错误码。
  • pthread_mutex_t是一种类型,可以用来定义一把互斥锁。

  • 静态分配的的互斥锁,不需要销毁,但是必须定义在全局。

②销毁互斥量

int pthread_mutex_destroy(pthread_mutex_t *mutex);

参数说明:

  • mutex:需要销毁的互斥量的地址。

返回值说明:

  • 互斥量销毁成功返回0,失败返回错误码。

销毁互斥量需要注意:

  • 使用PTHREAD_ MUTEX_ INITIALIZER初始化的互斥量不需要销毁
  • 不要销毁一个已经加锁的互斥量
  • 已经销毁的互斥量,要确保后面不会有线程再尝试加锁

③互斥量加锁和解锁

int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);

参数说明:

  • mutex:需要加锁的互斥量的地址。

返回值说明:

  • 互斥量加锁成功返回0,失败返回错误码。

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

  1. 互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功。

  2. 发起函数调用时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量,那么pthread_mutex_lock调用会陷入阻塞(执行流被挂起),等待互斥量解锁。


有了这些知识,我们就可以解决上面的问题了,我们在上述的抢票系统中引入互斥量,每一个线程要进入临界区之前都必须先申请锁,只有申请到锁的线程才可以进入临界区对临界资源进行访问,并且当线程出临界区的时候需要释放锁,这样才能让其余要进入临界区的线程可能申请到锁。

#include <iostream>
#include <cstdio>
#include <cstring>
#include <pthread.h>
#include <unistd.h>

using namespace std;

int tickets = 10000;
// 定义一把锁 
pthread_mutex_t mutex;

void* threadRoutine(void* args)
{
    char* s = static_cast<char*>(args);
    while (true)
    {
    	// 加锁
        pthread_mutex_lock(&mutex);
        if (tickets > 0)
        {
            usleep(1000); //抢票花费的时间
            cout << s << " get a ticket, surplus number is :" << --tickets << endl;
            // 解锁
            pthread_mutex_unlock(&mutex);
        }
        else
        {
        	// 解锁
            pthread_mutex_unlock(&mutex);
            break;
        }
        // 抢票以后的后续的处理
        usleep(1000);
    }
    cout << "The tickets are sold out" << endl;
    return s;
}

int main()
{
    // 对锁进行初始化
    pthread_mutex_init(&mutex, nullptr);
    pthread_t tname[4];
    int n = sizeof(tname) / sizeof(tname[0]);
    
    
    for (int i = 0; i < n; i++)
    {
        char* str = new char[64];
        snprintf(str, 64, "线程-%d", i);
        pthread_create(tname + i, nullptr, threadRoutine, str);
        usleep(1000);
    }

    // 回收线程以及内存
    void* ret = nullptr;
    for (int i = 0; i < n; i++)
    {
        int error = pthread_join(tname[i], &ret);
        if (error == 0)
        {
            delete[] (char*)ret;
        }
        else
        {
            cerr << strerror(error) << endl;
        }
    }
    // 销毁锁
    pthread_mutex_destroy(&mutex);
    return 0;
}

运行结果正常:

在这里插入图片描述

  • 此外加锁本身都是有损于性能的事,它让多执行流由并行执行变为了串行执行,这是不可避免的。
  • 我们应该在合适的位置进行加锁和解锁,这样能尽可能减少加锁带来的性能开销成本。
  • 进行临界资源的保护,是所有执行流都应该遵守的标准,这是在编码时需要注意的。

3、互斥量实现原理探究

  • 单纯的i++或者++i都不是原子的,有可能会有数据一致性问题。
    例如:取出ticket- -部分的汇编代码
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 <ticket>

- - ,++操作并不是原子操作,而是对应三条汇编指令:

  1. load:将共享变量ticket从内存加载到寄存器中
  2. update: 更新寄存器里面的值,执行-1/+1操作
  3. store:将新值,从寄存器写回共享变量ticket的内存地址
  • 为了实现互斥锁操作,大多数体系结构都提供了swapexchange指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性,即使是多处理器平台,访问内存的总线周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。

现在我们把lockunlock的伪代码改一下。

在这里插入图片描述
我们可以认为mutex的初始值为1,al是计算机中的一个寄存器,当线程申请锁时,需要执行以下步骤:

  1. 先将al寄存器中的值清0。该动作可以被多个线程同时执行,因为每个线程都有自己的一组寄存器(用来存储线程上下文信息),执行该动作本质上是将线程自己的al寄存器清0。
  2. 然后交换al寄存器和mutex中的值。xchgb(exchange)是体系结构提供的交换指令,该指令可以完成寄存器和内存单元之间数据的交换。
  3. 最后判断al寄存器中的值是否大于0。若大于0则申请锁成功,此时就可以进入临界区访问对应的临界资源;否则申请锁失败需要被挂起等待,直到锁被释放后再次竞争申请锁。

在这里插入图片描述

当一个线程申请锁成功以后,其他线程再进行申请时,由于mutex里面是0,al里面再进行交换拿到的依然是0,继续向后执行时会被挂起。

在这里插入图片描述

当线程释放锁时,需要执行以下步骤:

  1. 将内存中的mutex置回1。使得下一个申请锁的线程在执行交换指令后能够得到1,形象地说就是“将锁放回去”。
  2. 唤醒等待mutex的线程。唤醒因为申请锁失败而被挂起的线程,让它们继续竞争申请锁。

注意点

  1. 在线程释放锁时没有将当前线程al寄存器中的值清0,这不会造成影响,因为每次线程在申请锁时都会先将自己al寄存器中的值清0,再执行交换指令。

  2. 在申请锁时本质上就是哪一个线程先执行了交换指令,那么该线程就申请锁成功,因为此时该线程的al寄存器中的值就是1了。而交换指令就只是一条汇编指令,一个线程要么执行了交换指令,要么没有执行交换指令,所以申请锁的过程是原子的。

  3. CPU内的寄存器不是被所有的线程共享的,每个线程都有自己的一组寄存器,但内存中的数据是各个线程共享的。申请锁实际就是,把内存中的mutex通过交换指令,原子性的交换到自己的al寄存器中。

问题1:临界区内的线程可能进行线程切换吗?

临界区内的线程完全有可能进行线程切换,但即便该线程被切走,其他线程也无法进入临界区进行资源访问,因为此时该线程是拿着锁被切走的,锁没有被释放也就意味着其他线程无法申请到锁,也就无法进入临界区进行资源访问了。

其他想进入该临界区进行资源访问的线程,必须等该线程执行完临界区的代码并释放锁之后,才能申请锁,申请到锁之后才能进入临界区。

问题2:锁是否需要被保护?

我们说被多个执行流共享的资源叫做临界资源,访问临界资源的代码叫做临界区。所有的线程在进入临界区之前都必须竞争式的申请锁,因此锁也是被多个执行流共享的资源,也就是说锁本身就是临界资源。

既然锁是临界资源,那么锁就必须被保护起来,但锁本身就是用来保护临界资源的,那锁又由谁来保护的呢?

锁实际上是自己保护自己的,因为申请锁的过程是原子的,那么锁就是安全的。

二、可重入与线程安全

1、概念

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

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

2、常见的线程不安全的情况

  1. 不保护共享变量的函数
  2. 函数状态随着被调用,状态发生变化的函数
  3. 返回指向静态变量指针的函数
  4. 调用线程不安全函数的函数

3、常见的线程安全的情况

  1. 每个线程对全局变量或者静态变量只有读取的权限,而没有写入的权限,一般来说这些线程是安全的
  2. 类或者接口对于线程来说都是原子操作
  3. 多个线程之间的切换不会导致该接口的执行结果存在二义性

4、常见不可重入的情况

  1. 调用了malloc/free函数,因为malloc函数是用全局链表来管理堆的
  2. 调用了标准I/O库函数,标准I/O库的很多实现都以不可重入的方式使用全局数据结构
  3. 可重入函数体内使用了静态的数据结构

5、常见可重入的情况

  1. 不使用全局变量或静态变量
  2. 不使用用malloc或者new开辟出的空间
  3. 不调用不可重入函数
  4. 不返回静态或全局数据,所有数据都有函数的调用者提供
  5. 使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据

6、可重入与线程安全联系

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

7、可重入与线程安全区别

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

三、死锁问题

1、死锁的概念


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

例如:有一份临界资源需要同时拿到A,B两把锁才能进行访问,线程1拿到了A锁,线程2拿到了B锁,然后线程1,线程2都想访问这份临界资源,于是相互申请对方的锁,但是两方都不释放锁,于是产生了僵持,这就是死锁。

单执行流可能产生死锁吗?

单执行流也有可能产生死锁,如果某一执行流连续申请了两次锁,那么此时该执行流就会被挂起。因为该执行流第一次申请锁的时候是申请成功的,但第二次申请锁时因为该锁已经被申请过了,于是申请失败导致被挂起直到该锁被释放时才会被唤醒,但是这个锁本来就在自己手上,自己现在处于被挂起的状态根本没有机会释放锁,所以该执行流将永远不会被唤醒,此时该执行流也就处于一种死锁的状态。

例如,在下面的代码中我们让主线程创建的新线程连续申请了两次锁,然后使用ps命令查看线程的状态。

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

using namespace std;

// 静态分配一把锁
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

int main()
{
    cout << "I am thread" << endl;
    // 申请锁
    pthread_mutex_lock(&mutex);
    cout << "I got a lock" << endl;
    // 再次申请锁
    pthread_mutex_lock(&mutex);
    cout << "I got a lock again" << endl;
    // 解锁
    pthread_mutex_unlock(&mutex);
    pthread_mutex_unlock(&mutex);
    return 0;
}

可以看到,线程被死锁了

在这里插入图片描述

2、死锁四个必要条件

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

注意: 这是死锁的四个必要条件,也就是说只要是死锁,就一定同时满足这四个条件。

3、避免死锁

核心是:破坏死锁的四个必要条件,必要条件被破坏,就不可能形成死锁。

  1. 不加锁,不加锁当然不会产生死锁问题,当一个方案可以加锁完成也可以不加锁完成时,优先选择不加锁就能完成的!
  2. 加锁顺序一致,例如A,B,C三把锁,必须依次获取,顺序不能乱。
  3. 避免锁未释放的场景, 锁不释放,再次申请时当然会产生死锁问题。
  4. 主动释放锁,当我们申请锁失败的时候,我们可以主动释放自己的锁,这个可以借助pthread_mutex_trylock(),与pthread_mutex_unlock()函数完成。
  5. 控制线程统一释放锁,利用一个控制线程判断如果产生了死锁问题,就将所有的锁全部释放,重新竞争。(锁的申请与释放锁可以不是同一个线程

避免死锁也有一些其他算法如:死锁检测算法,银行家算法感兴趣的可以了解一下。

四、Linux线程同步

1、同步引入与概念

有了加锁以后我们多线程访问临界资源导致数据不一致性的问题确实得到了解决,但是单纯的加锁是会存在某些问题的,如果个别线程的竞争力特别强,每次都能够申请到锁,但申请到锁之后由于条件不满足于是什么也不做,所以在我们看来这个线程就一直在申请锁和释放锁,这就可能导致其他线程长时间竞争不到锁,引起饥饿问题

为了解决饥饿问题,于是引入了线程同步。

  • 同步:在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题,叫做同步。

  • 竞态条件: 指的是两个或者以上进程或者线程并发执行时,其最终的结果依赖于进程或者线程执行的精确时序。竞态条件会产生超出预期的情况因此竞态条件是一种需要被避免的情形。

2、条件变量

条件变量是利用线程间共享的全局变量进行同步的一种机制,条件变量是用来描述某种资源是否就绪的一种数据化描述。

条件变量主要包括两个动作:

  1. 一个线程使用等待条件变量而被挂起。
  2. 另一个线程使条件成立后唤醒挂起的线程。

条件变量的使用总是和一个互斥量结合在一起。

例如一个线程访问队列时,发现队列为空,它只能等待,直到其它线程将一个节点添加到队列中这个线程才被唤醒,这种情况就需要用到条件变量

有了这个条件变量以后该线程也不必不断的申请锁,使用队列里面的数据,结果没有数据,于是释放锁的循环,同时其他线程也能够有机会拿到锁,从而避免了饥饿问题。


①初始化条件变量

动态分配

int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);

参数说明:

  1. cond:需要初始化的条件变量。
  2. attr:初始化条件变量的属性,一般设置为NULL即可。

返回值说明:

  • 条件变量初始化成功返回0,失败返回错误码。

静态分配

pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

静态分配的条件变量不用我们手动销毁。

②销毁条件变量

int pthread_cond_destroy(pthread_cond_t *cond);

参数说明:

  • cond:需要销毁的条件变量。

返回值说明:

  • 条件变量销毁成功返回0,失败返回错误码。

③等待条件变量满足

int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);

当该函数成功返回时,调用该函数的线程会被挂在等待条件变量的等待队列里面,并且该函数也会自动释放该线程持有的锁

参数说明:

  • cond:等待的条件变量。
  • mutex:当前线程所处临界区对应的互斥锁。

返回值说明:

  • 函数调用成功返回0,失败返回错误码。

④唤醒等待

int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);
  1. pthread_cond_signal函数用于唤醒等待队列中首个线程。
  2. pthread_cond_broadcast函数用于按顺序唤醒等待队列中的全部线程。

参数说明:

  • cond:唤醒在cond条件变量下等待的线程。

返回值说明:

  • 函数调用成功返回0,失败返回错误码。

下面一份代码,我们假设条件全部都是不满足的,让多个线程在等待条件变量下面挂起,等待3秒以后条件满足,主线程再让所有的线程依次唤醒,继续执行。

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

using namespace std;

// 定义锁和条件变量
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

void* threadRoutine(void* args)
{
    char* s = static_cast<char*> (args);
    while (true)
    {
        pthread_mutex_lock(&mutex);
        // 假设要访问临界资源的条件不成立
        pthread_cond_wait(&cond, &mutex);

        cout << s << "active" << endl;

        pthread_mutex_unlock(&mutex);
    }
}


int main()
{
    pthread_t tname[3];
    // 创建线程
    for (int i = 0; i < 3; i++)
    {
        char* ps  = new char[32];
        snprintf(ps, 32, "thread-%d", i);
        pthread_create(tname + i, nullptr, threadRoutine, ps);
    }

    sleep(3);
    // 3s以后唤醒等待队列里面的线程
    cout << "main thread wake up ..." << endl;

    while (true)
    {
        pthread_cond_signal(&cond);
        sleep(1);
    }
    return 0;
}

运行结果:

在这里插入图片描述

我们发现唤醒这三个线程时具有明显的顺序性,根本原因是当这若干个线程启动时默认都会在该条件变量下去等待,而我们每次都唤醒的是在当前条件变量下等待的头部线程,当该线程执行完打印操作后会继续排到等待队列的尾部进行等待,所以我们能够看到一个循环周转的现象。

3、为什么pthread_cond_wait需要互斥量的理解

  • 条件等待是线程间同步的一种手段,如果只有一个线程,条件不满足,一直等下去都不会满足,所以必须要有一个线程通过某些操作,改变共享变量,使原先不满足的条件变得满足,并且友好的通知等待在条件变量上的线程。

  • 条件不会无缘无故的突然变得满足了,必然会牵扯到共享数据的变化,所以一定要用互斥锁来保护,没有互斥锁就无法安全的获取和修改共享数据。

  • 当线程进入临界区时需要先加锁,然后判断内部资源的情况,若不满足当前线程的执行条件,则需要在该条件变量下进行等待,但此时该线程是拿着锁被挂起的,也就意味着这个锁再也不会被释放了,此时就会发生死锁问题。

  • 所以在调用pthread_cond_wait函数时,还需要将对应的互斥锁传入,此时当线程因为某些条件不满足需要在该条件变量下进行等待时,就会自动释放该互斥锁

  • 当该线程被唤醒时,该线程会接着执行临界区内的代码,此时便要求该线程必须立马获得对应的互斥锁,因此当某一个线程被唤醒时,实际会自动获得对应的互斥锁。

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

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

相关文章

【前端】 Layui点击图片实现放大、关闭效果

实现效果&#xff1a;点击图片实现放大&#xff0c;点击空白处关闭效果。下图。 实现逻辑&#xff1a;二维码是使用JQ插件生成的&#xff0c;点击二维码&#xff0c;获取图片路径&#xff0c;通过Layui的弹窗显示放大后的图片。 Html <div id"qrcode" class&quo…

【JAVA基础】数据类型,逻辑控制

❤️ Author&#xff1a; 老九 ☕️ 个人博客&#xff1a;老九的CSDN博客 &#x1f64f; 个人名言&#xff1a;不可控之事 乐观面对 &#x1f60d; 系列专栏&#xff1a; 文章目录 数据类型整型变量 int长整型变量 long单精度浮点数 float双精度浮点数 double字符类型 char字节…

Mybatis执行getById报错Parameter index out of range

博主使用的Springboot3.0&#xff0c;jdk17&#xff0c;MybatisMybatisFlex的环境 报错&#xff1a; org.mybatis.spring.MyBatisSystemException: null java.lang.RuntimeException: java.sql.SQLException: Parameter index out of range (1 > number of parameters, w…

Android DataBinding 基础入门(学习记录)

目录 一、DataBinding简介二、findViewById 和 DataBinding 原理及优缺点1. findViewById的优缺点2. DataBinding的优缺点 三、Android mvvm 之 databinding 原理1. 简介和三个主要的实体DataViewViewDataBinding 2.三个功能2.1. rebind 行为2.2 observe data 行为2.3 observe …

基于MQTT协议的物联网网关实现远程数据采集及监控

在数字化时代的浪潮中&#xff0c;工业界正面临着前所未有的变革与机遇。而在这场变革中&#xff0c;基于MQTT协议的物联网网关崭露头角&#xff0c;成为连接工业设备、实现远程数据采集与监控的利器。其中&#xff0c;HiWoo Box作为一款出色的工业边缘网关&#xff0c;引领着这…

搜索算法之内容质量评估:如何对作者和文章进行质量评价

paperClubIP属地: 江苏 编辑文章 对于搜索引擎而言&#xff0c;用户算法的核心价值是用户体验&#xff0c;包括搜索内容的相关性、内容质量及时效性等&#xff0c;其中内容质量是前置步骤&#xff0c;既可以用于优质内容源筛选&#xff0c;又可以作为搜索召回结果排序因素&am…

Leetcode.100 相同的树

给你两棵二叉树的根节点 p 和 q &#xff0c;编写一个函数来检验这两棵树是否相同。 如果两个树在结构上相同&#xff0c;并且节点具有相同的值&#xff0c;则认为它们是相同的。 力扣&#xff08;LeetCode&#xff09;官网 - 全球极客挚爱的技术成长平台 代码如下&#xff1a;…

【STM32】串口初步使用

本文只作为学习笔记&#xff0c;对串口进行一个简单的介绍&#xff0c;正确的使用方式还需要进行实际的调试 通信的类型&#xff1a; 同步 异步 单工 双工 串行 并行 STM32的串口通信&#xff1a; 配置片上外设的控制寄存器&#xff0c;通信双方进行相同的配置&#xff0c;…

【Vue】集成高德地图

Vue 集成高德地图 步骤 注册成为高德开发者 获取申请的安全密钥、申请好的Web端开发者Key 创建Vue 工程 创建地图组件 引入地图组件 高德地图开发平台地址 https://lbs.amap.com/官方示例地址 https://lbs.amap.com/demo/list/js-api-v2示例 首先创建一个vue工程 安装 npm …

jvm与锁

今天是《面霸的自我修养》的第二弹&#xff0c;内容是Java并发编程中关于Java内存模型&#xff08;Java Memory Model&#xff09;和锁的基础理论相关的问题。这两块内容的八股文倒是不多&#xff0c;但是难度较大&#xff0c;接下来我们就一起一探究竟吧。 数据来源&#xff…

CAC2.0准实时威胁检测,“无人化”防暴力破解

客户背景 上海微盟企业发展有限公司&#xff08;以下简称“微盟”&#xff09;&#xff0c;香港主板上市企业&#xff0c;成立于2013年&#xff0c;致力于为商家打造去中心化的数字化转型SaaS产品及全链路增长服务&#xff0c;助力商家经营可持续增长。 在这个快节奏的网络时…

《发电厂电气部分》进出线停送电倒闸操作理解

在《发电厂电气部分》&#xff08;部分学校也叫《供配电技术》&#xff09;中&#xff0c;停电/送电时的倒闸操作在笔者看来是比较难理解的一件事&#xff0c;即使是在bilibili上看了实际架空线路上的倒闸操作&#xff0c;还是感觉云里雾里。这里分享一下自己的理解。 这里以最…

对于前端模块化的理解与总结(很全乎)

目录 模块化的好处 模块化的commonJS导入导出 暴露(导出)模块&#xff1a;module.exports value或exports.xxx value 导入模块——使用 es6模块化 方法一逐个导出 方法二默认导出 方法三 方法四 方法五 export 和import 同时存在 多个文件导出到一个文件后在相关文件…

解决 quill Cannot import modules/imageResize. Are you sure it was registered?

这个插件是在富文本编辑器中调整图片大小的,发现拉下来的依赖会报错,于是替换了nodejs版本,没有解决,但是用同时之前拉下来的,莫名其妙正常,后来经过尝试,发现删除demo和node_modules文件夹后正常. 删除后,不报错,正常了

python遍历文件夹下的所有子文件夹,并将指定的文件复制到指定目录

python遍历文件夹下的所有子文件夹&#xff0c;并将指定的文件复制到指定目录 需求复制单个文件夹遍历所有子文件夹中的文件&#xff0c;并复制代码封装 需求 在1文件夹中有1&#xff0c;2两个文件夹 将这两个文件夹中的文件复制到 after_copy中 复制单个文件夹 # coding: ut…

电源管理(PMIC)TPS63070RNMR、TPS650942A0RSKR、LM5175RHFR器件介绍、应用及特点。

一、TPS63070RNMR&#xff0c;降压升压 开关稳压器 IC 正 可调式 2.5V 1 输出 3.6A&#xff08;开关&#xff09; 15-PowerVFQFN 1、概述 TPS63070高输入电压降压-升压转换器是一款高效的低静态电流降压-升压转换器。这些器件适用于输入电压高于或低于输出电压的应用。升压模式…

Java-day12(泛型)

泛型 解决元素存储的安全性问题 解决获取数据元素时&#xff0c;需要类型强转的问题 核心思想&#xff1a;把一个集合中的内容限制为一个特定的数据类型 静态方法中不能使用类的泛型 不能在catch中使用泛型 如果泛型类是一个接口或抽象类&#xff0c;则不可创建泛型类的对…

Docker技术--Docker镜像管理

1.Docker镜像特性 ①.镜像创建容器的特点 Docker在创建容器的时候需要指定镜像,每一个镜像都有唯一的标识:image_id,也可也使用镜像名称和版本号做唯一的标识,如果不指定版本号,那么默认使用的是最新的版本标签(laster)。 ②.镜像分层机制 Docker镜像是分层构建的,并通过…

Springboot集成Docker并将镜像推送linux服务器

案例使用springboot项目&#xff0c;在IDEA 中集成Docker生成镜像&#xff0c;并将镜像发布到linux服务器 具体步骤如下&#xff1a; 1、Centos7安装Docker 更新系统的软件包列表 sudo yum update安装Docker所需的软件包和依赖项&#xff1a; sudo yum install docker完成…

vue3中右侧26个英文字母排列,点击字母,平滑到响应内容

效果图如下&#xff1a; 右侧悬浮 <!-- 右侧悬浮组件 --><div class"right-sort"><div v-for"(item, index) in list" :key"index" class"sort-item" :class"index activeIndex ? sort-item-active : " c…