【操作系统笔记九】并发安全问题

news2024/11/25 4:50:52

用户态抢占和内核态抢占

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

内核中可以执行以下几种程序:

  • ① 当前运行的进程陷阱程序(系统调用)故障程序(page fault) ,进程运行在内核态的时候,其实就是在执行进程在用户态触发的异常对应的异常处理程序
  • ② 中断处理程序
  • ③ 内核线程

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

用户态线程抢占的调度时机

检查当前线程是否需要被抢占的时机点(检查点):

  • 时钟中断发生,在时钟中断处理程序中判断进程实际运行的时间大于规定运行的最长时间且运行队列有优先级更高的任务

当前线程被抢占的时机点(抢占点):

  • 中断处理程序回到用户态之前

在这里插入图片描述

内核抢占的调度时机

检查当前线程是否需要被抢占的时机点(检查点):

  • 时钟中断发生,在时钟中断处理程序中判断进程实际运行的时间大于规定运行的最长时间且运行队列有优先级更高的任务

  • 线程被唤醒的时候,唤醒的任务虚拟运行时间更小(优先级高)比如,磁盘 I/O 中断唤醒正在等待数据的线程。比如,fork/ clone 创建的新进程/线程,被唤醒时。

当前线程被抢占的时机点(抢占点):

  • ① 从中断处理程序回到内核态之前
  • ② 开启抢占(preemt_enable)的时候
  • 条件:标记了 tlf_need_sched 且抢占计数器等于 0(开启抢占)

疑问点:时钟中断发生时,当前任务被抢占的条件到底是什么呢?

其实这个是要区分任务类型的,如果当前运行的任务是普通任务的话,那么就需要根据任务运行时间和它的 vruntime 来决定要不要抢占当前进程,如下代码逻辑:

在这里插入图片描述

如果当前运行的任务是实时任务的话,那么就是根据 RR 调度算法来决定要不要抢占当前任务,如下图:
在这里插入图片描述
在这里插入图片描述

根据使用场景来决定要不要配置内核抢占:

  • PREEMPT_NONE:不支持抢占,吞吐量优先,后台计算场景(处理数据)
  • PREEMPT_VOLUNTARY: 内核中放置了一些抢占点,桌面应用
  • PREEMPT:支持内核抢占,低延迟的桌面应用、嵌入式

支持内核抢占的内核,称为抢占式内核,不支持内核抢占的内核,称为非抢占式内核。

数据并发访问安全问题

内核中:每个内核程序都可以访问所有的物理内存

  • ① 两个中断处理程序同时访问一个全局数据

  • ② 中断处理程序和内核态中运行的线程同时访问一个全局数据

  • ③ 两个运行在内核态的线程同时访问一个全局数据

用户态:

  • ① 运行在同一个进程中的多个线程,共享全局内存

  • ② 进程与进程之间也可以共享内存,进程间通信

在这里插入图片描述

  • 内核可配置为抢占和非抢占,在抢占式内核中,多个线程同时访问共享内存时,会发生并发安全问题。

两个线程访问一个全局变量

一个进程中的所有线程共享这个进程的虚拟地址空间,多个线程可以同时访问一个全局变量。

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

  • 并发问题的根本原因是操作临界区的共享全局变量的操作不是原子操作,在高级开发语言中的一行代码可能对应多条底层汇编指令代码,如cnt++,在底层可能对应三条汇编指令,分别是从内存读取cnt保存到寄存器、将寄存器中的cnt+1,将寄存器中的cnt写回到内存,而因为抢占,我们无法预测线程在哪一条指令上被抢占,线程可能在任意指令上被抢占后切换到另一个线程去执行。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

  • 使用对应底层是原子操作的指令代码,如atomic一类的api,使用不能被打断的原子指令,确保指令执行时不会被中断或抢占,两次读写访问的是同一个内存单元

  • 使用lock加锁技术锁定内存总线,在指令执行结束之前,其他CPU不能访问这个内存单元

  • 原子操作需要硬件级别保证,高级语言中都有对应的原子操作类

CAS 的 ABA 问题

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

说白了,就是某个变量中间被别人改过了(A->B),但是又改回去了(B->A),但是我们不知道它中间是否被人改过,此时必须通过增加版本号,也就是添加额外标志信息,来比较是否曾经被改过,每改一次版本号就加 1,这样比较时,除了变量值一样,版本号也必须一致,才认为是没有被修改过的。否则,就是被动过的。

CAS 自旋锁

在这里插入图片描述
在这里插入图片描述

普通的自旋锁存在并发问题。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

typedef struct _lock_t {
    // 正如同餐厅叫号,turn表示当前的号码,ticket表示手中的号码
    atomic_int ticket;
    atomic_int turn; 
} lock_t;

void init(lock_t *mutex) { 
    mutex->ticket = 0; 
    mutex->turn = 0; 
}

void lock(lock_t *mutex) {
    int my_turn = atomic_fetch_add(&mutex->ticket, 1); 
    while (mutex->turn != my_turn); // spin 
}

void unlock(lock_t *mutex) {
    // 每一个顾客(线程)用完之后就呼叫下一个号码。
    mutex->turn++;
}

CAS 自旋锁的问题 — 浪费 CPU

  • 由于自旋锁导致每个线程都在执行 while 操作,占用 CPU 时间
  • 如果进入临界区的线程执行的时间过长的话,那么等待的线程,一直占用着 CPU,导致极大的 CPU 资源浪费

解决方案:

  • ① 将没有拿到锁的线程,放入到等待队列中
  • ② 阻塞没有拿到锁的线程,放弃CPU执行权
  • ③ 当正在执行临界区的线程,离开临界区的时候,从等待队列中唤醒一个线程

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

#include <stdatomic.h> 
#include <sys/types.h>
#include <unistd.h> 
#include "queue.c"

typedef struct _lock_t {
    // guard 是 lock 和 unlock 过程的一个自旋锁
    atomic_flag guard;
    // 用于标记当前锁是否被一个线程获取
    // 0表示当前还没有线程获取这把锁
    // 1表示当前已经有一个线程获取这把锁了
    int flag;
    // 如果 flag==1,那么再来获取这把锁的线程
    // 将在这个等待队列中阻塞、等待
    queue_t *wait_queue;
} lock_t;

void init(lock_t *mutex) {
    queue_init(mutex->wait_queue);
    mutex->flag = 0;
}
void lock(lock_t *mutex) {
    while(atomic_flag_test_and_set(&mutex->guard)); // spin
    if (mutex->flag == 0) { 
        mutex->flag = 1;
        atomic_flag_clear(&mutex->guard); 
    } else {
        // 将当前线程 pid 放入到队列中
        queue_add(mutex->queue, gettid()); 
        atomic_flag_clear(&mutex->guard);
        // 伪代码,阻塞当前线程,需要发起一次系统调用
        park();
    }
} 
void unlock(lock_t *mutex) {
    while(atomic_flag_test_and_set(&mutex->guard)); // spin
    if (queue_empty(mutex->wait_queue)) { 
        mutex->flag = 0;
    } else {
        // 伪代码,唤醒队列中队头的线程,需要发起系统调用
        unpark(queue_remove(mutex->wait_queue));
        // flag 值本来就是 1,这里加不加没关系
        mutex->flag = 1;
    }
    atomic_flag_clear(&mutex->guard); 
}

相当于先用一把自旋锁来保证锁内全局共享变量的安全,这里设置flag和队列入队出队操作非常快,因此不会导致自旋锁执行while等待太久导致CPU浪费。

C语言中的互斥锁API:

#include <stdio.h>
#include <pthread.h> 
#include <unistd.h> 
#include <string.h> 
#include <stdlib.h> 
pthread_mutex_t mut; 

// 打印机
// 将字符串以单个字符循环输出
void my_print(char *str) 
{
    pthread_mutex_lock(&mut); 
    while(*str!=0)
    {
        printf("%c", *str++); 
        fflush(stdout); 
        sleep(1); 
    }
    printf("\n");
    pthread_mutex_unlock(&mut);
}
int main(int argc, char const *argv[]) 
{
    void *arg = NULL; //  1.初始化锁
    pthread_mutex_init(&mut, NULL); 
    pthread_t tid, tid2, tid3, tid4;
    pthread_create(&tid, NULL, print_word, "11111111"); 
    pthread_create(&tid2, NULL-, print_word, "22222222"); 
    pthread_create(&tid3, NULL, print_ word, "33333333"); 
    pthread_create(&tid4, NULL, print_word, "44444444"); 
    pthread_join(tid, &arg); // 等待,阻塞
    pthread_join(tid2, &arg); // 等待,阻塞 
    pthread_join(tid3, &arg); // 等待,阻塞 
    pthread_join(tid4, &arg); // 等待,阻塞 
    return 0;
} 

阻塞互斥锁 VS 自旋锁

阻塞互斥锁基于自旋锁实现,操作等待队列,执行的时间非常短。

  • 如果临界区执行的时间非常短,选择自旋锁

  • 如果临界区执行的时间比较长,选择阻塞互斥锁

阻塞时,需要发生系统调用,以及线程切换,需要开销。如果临界区执行的时间非常短,使用阻塞互斥锁,得不偿失。不管用什么锁,都需要系统开销,安全和性能需要权衡。

真实的互斥锁实现:先使用自旋锁,过了一段时间还没有拿到锁,此时再进行阻塞。 自旋+互斥启发式的锁。

总结:

  • CAS 实现自旋锁:使用一个atomic类型的全局变量flag表示临界区是否有线程在执行,

    ① 当flag==1时,说明临界区有一个线程在执行,此时while执行空转等待;
    ② 若flag==0时,说明临界区没有线程在执行,跳出while继续往下执行,并将flag设置为1,在退出临界区时解锁将flag置为0

  • CAS 自旋锁的饥饿问题:由于 CPU 调度不能保证先申请锁的一定先获得执行权,可能存在某个线程很久无法获得锁。

    解决方法:使用 FIFO 自旋锁,给每个线程编号,先叫到号的先执行

  • CAS 自旋锁的浪费 CPU 问题:由于自旋锁会导致每个线程都在不停的执行while空转操作,会占用 CPU,假如临界区的线程执行时间过长,那么其他等待的线程将会一直占用着 CPU,导致浪费。

    解决方法:基于自旋锁实现阻塞互斥锁,将没有拿到锁的线程加入等待队列中阻塞等待,让出 CPU 执行权,在临界区的线程执行完后,从等待队列中唤醒一个线程来执行。在实现时,需要一个标志位来标识临界区是否有 1 个线程在执行以及一个队列数据结构,另外需要一把自旋锁来保证锁内这两个全局变量的安全性。

  • 自旋锁 和 阻塞互斥锁该如何选择:根据临界区的执行时间长短来决定,时间短的使用自旋锁,时间长的使用阻塞互斥锁,因为阻塞时,需要发生系统调用 CPU 上下文切换需要开销,时间短的话再用阻塞得不偿失。

  • 真实的锁实现:先使用自旋锁,过了一段时间还没有拿到锁,此时再进行阻塞。

公平锁、非公平锁以及读写锁

公平锁 VS 非公平锁

  • 公平锁多个线程按照顺序去申请锁,线程会直接进入队列去排队,永远都是队列的第一位才能得到锁

    优点:所有的线程都有机会得到资源,不会饿死在队列中
    缺点:吞吐量会下降很多,只有队首的线程会执行,其他的线程排队阻塞,CPU 唤醒阻塞线程的开销会很大。

  • 非公平锁多个线程去获取锁的时候,会直接去尝试获取,获取不到进入等待队列,如果能获取到就执行。

    优点:减少了唤起线程的数量,从而可以减少 CPU 唤醒线程的开销,CPU 不必取唤醒所有线程,整体的吞吐效率会高点。
    缺点:可能某个线程一直获取不到锁,导致饿死。

读写锁(read-write lock)

主要是针对读多写少的场景:

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

如果是读多写少的情况下,使用互斥锁就太浪费了。因为读的话,可以多线程同时读取资源,没有线程安全问题。

  • 线程的时候,获取【读锁】,这个时候写线程不能进临界区。
  • 线程的时候,获取【写锁】,这个时候读/写线程都不能进临界区。

读锁是共享的,写锁是独占互斥的,如果是读,可以多个读线程同时读,如果是写,只能一个线程写,其他的读写线程都不能访问。

各个语言都会有自己实现的读写锁:

pthread_rwlock_t rwlock; // 声明一个读写锁

pthread_rwlock_rdlock(&rwlock); // 在读之前加读锁
...共享资源的读操作
pthread_rwlock_unlock(&rwlock); // 读完释放锁

pthread_rwlock_wrlock(&rwlock); // 在写之前加写锁 
...共享资源的写操作
pthread_rwlock_unlock(&rwlock); // 写完释放锁

pthread_rwlock_destroy(&rwlock); // 销毁读写锁

细化锁的粒度

在这里插入图片描述

上面代码中使用一把锁来保护两个资源,一个线程在取钱,而另一个线程却不能登录。说明锁的粒度太大,可以并发的两个操作变成串行了。

其实这两个操作可以并发执行,细化锁的粒度,可以提高并发能力:

在这里插入图片描述

例如下面代码,两个账户需要转账的时候,要拿到两个账户的锁后,才可以开始转账,提高了并发能力

在这里插入图片描述
在这里插入图片描述

死锁

发生死锁的四个条件:

  • 互斥在一个时间点上,需要保证一个线程访问共享资源

  • 占有且等待线程 1 已经取得共享资源 A 的锁,在等待另一个共享资源 B 的锁的时候,不会释放共享资源 A 的锁

  • 不可抢占如果线程 1 已经取得共享资源 A 的锁,其他的线程不能强行抢占线程 1 已经取得的锁

  • 循环等待线程 1 等待线程 2 占用的资源的锁,线程 2 等待线程 1 占有的资源的锁,这就是循环等待了

例如下面代码展示了出现死锁的情况,其中:

  • 线程 1 拿到了 A 账户的转账锁,现在需要 B 账户的转账锁
  • 线程 2 已经拿到了 B 账户的转账锁,现在需要 A 账户的转账锁

在这里插入图片描述

破坏【占有且等待】

在这里插入图片描述

破坏【占有且等待】的方法:线程一次申请需要的所有资源锁,这样这可以避免发生死锁了

转账的例子,假如一个线程执行 A 账户转账 100 元给 B 账户,只有当这个线程拿到了 A 和 B 的锁,才开始进行转账操作,线程要么拿到 A 和 B 的锁,要么 A 和 B 的锁都没拿到,不会出现拿到一个锁,而等待另一个锁的情况,从而避免了死锁。

在这里插入图片描述

思考:在使用 ResourceManager 破坏【占用且等待】时,下面的代码 ① 和代码 ② 是否多余?

在这里插入图片描述

如果只有转账功能的话,那么代码 ① 和代码 ② 是多余的,可以省略的。

这是因为,当 srctarget 有一把锁申请不到,或者两把锁都申请不到的时候,线程会在 while 代码处自旋,不会进入临界区,这样就可以保证临界区只会有一个线程执行,只有这个线程拿到了两把锁。

但是,当不仅仅只有转账功能,可能还有取钱、存钱等业务的时候,这些业务也会去操作账户中的 balance 了,那么上面的代码 ① 和代码 ② 就很有必要了,因为转账的同时,可能还会有其他的线程对转账的账户执行取钱,或者存钱。

也就是存在多线程同时更新一个账户的 balance ,所以,转账的时候还是得把两把锁锁上,才能保证 balance 的线程安全。

破坏【不可抢占】

在这里插入图片描述

破坏【不可抢占】的方法:已经占有部分资源的线程在进一步申请其他资源时,如果申请不到的话,可以主动释放它所占有的资源,这样不可抢占这个条件就破坏掉了。

可以通过设计一个带超时的锁来实现,也就是当线程获取不到锁,如果超过了指定的时间后,就主动放弃。

在这里插入图片描述

破坏【循环等待】

在这里插入图片描述

破坏【循环等待】的方法:如果我们先对资源排序,然后按照顺序来申请资源的话,就可以破坏【循环等待】的条件了。

假设每个账户都有一个序号,来标志唯一标志这个账号,我们可以使用 seqNo 来表示

在这里插入图片描述

总结

  • 破坏占用且等待 —— 先一次性申请到所有需要的资源,再进行操作,线程要么都拿到了 A 和 B 的锁,要么都没有拿到,不会出现拿到一个 A 锁,而等待另一个 B 锁的情况
  • 破坏不可抢占 —— 设计带超时的锁,一直获取不到锁时,超时自动放弃当前占用资源
  • 破坏循环等待 —— 先对资源进行排序,然后按顺序申请资源,释放时,按顺序释放锁

信号量

在这里插入图片描述
在这里插入图片描述

信号量的主要作用:

  • 1:限流(控制并发度)
  • 2:相当于互斥锁(将信号量初始值设置为1)

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

信号量实现生产者消费者模型的参考代码:

int main() {
    sem_init(&empty, 0,2); // 有两个食物窗口 
    sem_init(&full, 0, 0); // 一开始食物窗口中没有任何菜,所以这里初始化为 0

    pthread_t chefs[3]; 
    pthread_t waiters[2]; 

    // 3个厨师
    for (int i = 0; i< 3; i++) {
        pthread_create(&chefs[i], NULL, chef_thread, &i); 
    }
    // 2个服务员
    for (int i = 0; i<2; i++) {
        pthread_create(&waiters[i], NULL, waiter_thread, &i);
    }
    
    sleep(30);
    
    sem_destroy(&empty); 
    sem_destroy(&full); 
}
void *chef_thread(void *args) { 
    int chef id = *((int *)args); 
    while (1) {
        // 模拟做菜
        sleep(rand() % 5);
        produce(chef_id); 
    }
}
void *waiter_thread(void *args) { 
    int waiter id = *((int *)args); 
    while (1) {
        sleep(rand( ) % 3); 
        consume(waiter_id); 
    }
}

sem_t empty;
sem_t full;

// 厨师这个线程通过这个函数,不断的往食物窗口放置菜
void produce(int chef_id) {
    // 这里会判断窗口是否为空,如果不为空则等待
    sem_wait(&empty);
    
    // 临界区
    printf("chef %d add food to food window\n", chef_id); 
    fflush(stdout);

    // 厨师将菜放入窗口,则通知服务员来取菜
    sem_post (&full);
}
// 服务员这个线程通过这个函数,不断的从食物窗口中取菜
void consume(int waiter_id) {
    // 这里服务员判断窗口是否满的,如果不满,也就是没有食物,则等待
    sem_wait(&full);

    // 临界区
    printf("waiter %d remove food from food winddow\n", waiter_id); 
    fflush(stdout);

    // 当服务员拿完菜,则通知厨师往窗口中放菜
    sem_post(&empty);
} 

生产者用一个信号量empty表示队列是否满的资源,消费者用一个信号量full表示队列是否空的资源。empty==0时,表示没有空的资源可用,则表示队列已满,生产者等待消费者消费,消费者消费后通知信号量empty, 若empty > 0时,表示队列有空位,生产者继续,生产者生产之后,通知信号量fullfull == 0时,表示队列为空,消费者等待生产者生产,full > 0时,表示队列有资源,消费者从队列中取出资源消费,然后通知信号量empty

在这里插入图片描述

苹果橙子问题参考代码:

#include <pthread.h> 
#include <semaphore.h> 
#include <stdlib.h> 
#include <unistd.h> 
#include <stdio.h> 

sem_t empty;
sem_t apple; 
sem_t orange;

void *father_thread(void *args) { 
    while (1) {
        // 如果盘子不是空的就等待
        sem_wait(&empty);
        sleep(rand() % 3);
        printf("爸爸放入一个苹果!\n")// 通知儿子可以吃苹果了
        sem_post(&apple); 
    }
}

void *mother_thread(void *args) { 
    while (1) {
        // 如果盘子不是空的就等待
        sem_wait(&empty) ;
        sleep(rand() % 3);
        printf("妈妈放入一个橙子!\n");
        // 通知女儿可以吃橙子了
        sem_post(&orange); 
    }
}

void *son_thread(void *args) {
    while (1) {
        // 如果没有苹果,则等待
        sem_wait(&apple); 
        sleep(rand() % 5);
        printf("儿子吃了一个苹果!\n");
        // 通知爸爸妈妈可以放水果了
        sem_post(&empty); 
    }
}

void *daughter_thread(void *args) { 
    while (1) {
        // 如果没有橙子,则等待
        sem_wait(&orange); 
        sleep(rand() % 5);
        printf("女儿吃了一个橙子!\n");
        // 通知爸爸妈妈可以放水果了
        sem_post(&empty) ; 
    }
}

int main() {
    pthread_t father; // 定义线程
    pthread_t mother;
    pthread_t son; 
    pthread_t daughter;

    sem_init(&empty, 0, 3); //信号量初始化 
    sem_init(&apple, 0, 0);
    sem_init(&orange, 0, 0);

    pthread_create(&father, NULL, father_thread, NULL); // 创建线程 
    pthread_create(&mother, NULL, mother_thread, NULL);
    pthread_create(&daughter, NULL, daughter_thread, NULL); 
    pthread_create(&son, NULL, son_thread, NULL);

    sleep(100); 
    return 1; 
}

信号量总结:

  • 信号量:表示临界区的可用资源数量,进入临界区可用资源数量 - 1,退出临界区可用资源数量 + 1,临界区可以同时有多个线程执行,只要能获取到资源。可用资源数量 ≤ 0 时,线程等待,直到可用资源数量≥ 0 时,才能进入。

  • 信号量的值设置为 > 0时,可以用于限流,控制并发数,信号量的值设置为1时,可以当成互斥锁来使用。

  • 信号量可以用于线程同步控制,如生产者消费者模型

管程 (monitor)

管程和信号量是等价的,一般信号量能实现的功能,也是可以使用管程来实现的。信号量一开始提出来的时候,就是应用于操作系统中多线程同步互斥的实现。管程一开始提出来的时候,是用在编程语言这个层面上的,比如 Java 语言、C++ 语言等,这些语言通过设计实现的管程机制,可以简化它们实现多线程同步互斥的操作。

管程是包含一系列的共享变量,以及针对这些变量的操作函数的一个组合,在具体的设计中,管程包含:

  • 一个锁,这个锁是用来确保互斥的,也就是确保只能有一个线程可以进入管程执行操作
  • ② 0 个或者多个条件变量,用于实现条件同步

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

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

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

相关文章

如何扫描MSI安装文件的路径

今天有个需求&#xff0c;需要扫描已经安装应用, 其中有个华云桌面 其中的UninstallString 值是 MsiExec.exe /X{D20A661B-0CBA-4DE3-A1F6-353D8153725D} 无法直接获取其安装目录&#xff0c; MsiGetProductInfoW 等API INSTALLPROPERTY_INSTALLLOCATION 也不好使 自己写一个…

Supervisor进程管理

Supervisor进程管理 概述&#xff1a;supervisor 是一个用 python 语言编写的进程管理工具&#xff0c;它可以很方便的监听、启动、停止、重启一个或多个进程。当一个进程意外被杀死&#xff0c;supervisor 监听到进程死后&#xff0c;可以很方便的让进程自动恢复&#xff0c;…

区块链实验室(26) - 区块链期刊Blockchain: Research and Applications

Elsevier出版物“Blockchain: Research and Applications”是浙江大学编审的期刊。该期刊自2020年创刊&#xff0c;并出版第1卷。每年出版4期&#xff0c;最新期是第4卷第3期(2023年9月)。 目前没有官方的IF&#xff0c;Elsevier的引用因子Citescore是6.4。 虽然是新刊&#xf…

《开发实战》18 | 数据存储:NoSQL与RDBMS如何取长补短、相辅相成?

取长补短之 Redis vs MySQL 做一个简单测试&#xff0c;分别填充 10 万条数据到 Redis 和 MySQL 中。MySQL 中的 name字段做了索引&#xff0c;相当于 Redis 的 Key&#xff0c;data 字段为 100 字节的数据&#xff0c;相当于 Redis 的Value。在我的电脑上&#xff0c;使用 wr…

免费的视频剪辑素材,可商用。

找免费可商用的视频剪辑素材&#xff0c;就上这6个网站&#xff0c;强推&#xff0c;赶紧收藏&#xff01; 1、菜鸟图库 https://www.sucai999.com/video.html?vNTYxMjky 菜鸟图库网素材非常丰富&#xff0c;网站主要还是以设计类素材为主&#xff0c;高清视频素材也很多&am…

【软件测试】黑盒测试用例的四种设计方法

一、输入域测试用例设计方法 输入域测试法是一种综合考虑了等价类划分、边界值分析等方法的综合方法&#xff0c;针对输入域测试法中可能出现的各种情况&#xff0c;输入域测试法主要考虑三个方面&#xff1a;  (1)极端测试(ExtremalTesting)&#xff0c;要求在输入域中选择…

汽车数字化转型:存储驱动创新未来

通过在存储领域持续不断的技术创新&#xff0c;西部数据正在助力汽车行业打造更加辉煌灿烂的未来。 汽车数字化转型大会上的创新存储 近日&#xff0c;作为智能汽车领域的行业盛宴&#xff0c;2023第二届汽车数字化转型大会在上海揭幕。 本届汽车数字转型大会不但聚集了全球汽车…

Python+selenium自动化生成测试报告

批量执行完用例后&#xff0c;生成的测试报告是文本形式的&#xff0c;不够直观&#xff0c;为了更好的展示测试报告&#xff0c;最好是生成HTML格式的。 unittest里面是不能生成html格式报告的&#xff0c;需要导入一个第三方的模块&#xff1a;HTMLTestRunner 一、导入HTMLT…

springboot如何接入netty,实现在线统计人数?

springboot如何接入netty&#xff0c;实现在线统计人数&#xff1f; Netty 是 一个异步事件驱动的网络应用程序框架 &#xff0c;用于快速开发可维护的高性能协议服务器和客户端。 Netty ​ 是一个 NIO 客户端服务器框架 ​&#xff0c;可以快速轻松地开发协议服务器和客户端等…

使用富斯i6遥控器设置6种飞行模式

使用富斯i6遥控器设置6种飞行模式 将富斯i6遥控器的SWC和SWD分别设置为ch5和ch6,然后使用混控功能设置6段开关,以实现6种飞行模式 一、设置辅助通道 进入系统菜单,选择 Functions Setup 选项,进入 Aux. channels 进行设置。将 Channel 5设置为 SwC,Channel 6 设置为 Sw…

CompletableFuture-CompletionStage接口源码分析和四大静态方法初讲

2.3 CompletableFuture对Future的改进 2.3.1 CompletableFuture为什么会出现 get()方法在Future计算完成之前会一直处在阻塞状态下&#xff0c;阻塞的方式和异步编程的设计理念相违背。 isDene()方法容易耗费cpu资源&#xff08;cpu空转&#xff09;&#xff0c; 对于真正的…

SpringBoot @value注解动态刷新

参考资料 Spring系列第25篇&#xff1a;Value【用法、数据来源、动态刷新】【基础系列】SpringBoot配置信息之配置刷新【基础系列】SpringBoot之自定义配置源的使用姿势【基础系列】SpringBoot应用篇Value注解支持配置自动刷新能力扩展Spring Boot 中动态更新 Value 配置 一. …

完成“重大项目”引进签约,美创科技正式落户中国(南京)软件谷

近日&#xff0c;美创科技正式入驻中国&#xff08;南京&#xff09;软件谷&#xff0c;并受邀出席中国南京“金洽会"之“雨花台区数字经济创新发展大会”。美创科技副总裁罗亮亮作为代表&#xff0c;在活动现场完成“重大项目”引进签约。 作为国家重要的软件产业与信息服…

Redis之set类型

文章目录 Redis之set类型1. 添加元素/获取集合中的所有元素/获取集合中元素个数2. 删除元素3. 判断元素是否在集合中3. 从集合中随机弹出一个元素&#xff0c;元素不删除4. 从集合中随机弹出元素&#xff0c;出一个删一个5. 将元素从一个集合转移到另外一个集合6. 集合的差集7.…

周记之重新开始

对于这周的学习&#xff0c;我进行了深刻的反思&#xff1a; 先来说说每天做了什么&#xff1a; 9.18号&#xff1a;把这个顶部的个人信息画出来了&#xff1b;然后记了两个单词&#xff08;这是能说的吗&#xff0c;这两个单词还是之前复习的&#xff09;现在都记忆犹新&…

31.下一个排列

方法&#xff1a;两遍扫描 举例&#xff1a; 4 5 2 6 3 1排列中较小数为2&#xff0c;较大数为3&#xff0c;交换两者得&#xff1a;4 5 3 6 2 1&#xff0c;将[i1,n)区间改成升序&#xff1a;得下一个排列&#xff1a; 4 5 3 1 2 6。 若第一步找不到较小数&#xff0c;即当前排…

基础算法--区间合并

区间合并简介 区间合并模型是一种竞赛里比较常见的模型&#xff0c;他的含义是&#xff0c;给你n个区间&#xff0c;要你合并所有有交集的区间&#xff0c;并求出合并后剩下的区间个数&#xff0c;如区间[1, 4]和[2, 3]可以合并成[1, 4]&#xff0c;但是[1, 2] 和 [3, 4] 不可…

SLAM从入门到精通(rviz的使用)

【 声明&#xff1a;版权所有&#xff0c;欢迎转载&#xff0c;请勿用于商业用途。 联系信箱&#xff1a;feixiaoxing 163.com】 在ros开发当中&#xff0c;rviz和tf都是用的比较多的一个工具。前者是为了实现传感器数据和计算结果的可视化&#xff0c;后者主要是为了显示各个传…

深度学习中什么是embedding

使用One-hot 方法编码的向量会很高维也很稀疏。假设我们在做自然语言处理(NLP)中遇到了一个包含2000个词的字典&#xff0c;当使用One-hot编码时&#xff0c;每一个词会被一个包含2000个整数的向量来表示&#xff0c;其中1999个数字是0&#xff0c;如果字典再大一点&#xff0c…