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

用户态抢占和内核态抢占

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

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

  • ① 当前运行的进程陷阱程序(系统调用)故障程序(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)); // spinif (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)); // spinif (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); // 一开始食物窗口中没有任何菜,所以这里初始化为 0pthread_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.mzph.cn/news/87254.shtml

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

相关文章

关于安卓SVGA浅尝(一)svgaplayer库的使用

关于安卓SVGA浅尝&#xff08;一&#xff09;使用 相关链接 SVGA官网 SVGA-github说明文档 背景 项目开发&#xff0c;都会和动画打交道&#xff0c;动画的方案选取&#xff0c;就有很多选择。如Json动画&#xff0c;svga动画&#xff0c;gif等等。各有各的优势。目前项目中…

【PCIE702-1】基于Kintex UltraScale系列FPGA的高性能PCIe总线数据预处理载板

PCIE702-1是一款基于PCIE总线架构的高性能数据预处理FMC载板&#xff0c;板卡采用Xilinx的高性能Kintex UltraScale系列FPGA作为实时处理器&#xff0c;实现各个接口之间的互联。板卡具有1个FMC&#xff08;HPC&#xff09;接口&#xff0c;1路PCIe x8主机接口&#xff0c;板载…

AxureRP制作静态站点发布互联网,实现公网访问【内网穿透】

AxureRP制作静态站点发布互联网&#xff0c;内网穿透实现公网访问 文章目录 AxureRP制作静态站点发布互联网&#xff0c;内网穿透实现公网访问前言1.在AxureRP中生成HTML文件2.配置IIS服务3.添加防火墙安全策略4.使用cpolar内网穿透实现公网访问4.1 登录cpolar web ui管理界面4…

【实战项目之个人博客】

目录 项目背景 项目技术栈 项目介绍 项目亮点 项目启动 1.创建SSM&#xff08;省略&#xff09; 2.配置项目信息 3.将前端页面加入到项目中 4.初始化数据库 5.创建标准分层的目录 6.创建和编写项目中的公共代码以及常用配置 7.创建和编写业务的Entity、Mapper、…

认识HTTP和HTTPS协议

HTTPS 是什么 HTTPS 也是一个应用层协议. 是在 HTTP 协议的基础上引入了一个加密层. 为什么要引入加密层呢&#xff1f; HTTP 协议内容都是按照文本的方式明文传输的. 这就导致在传输过程中出现一些被篡改的情况. HTTPS就是在HTTP的基础上进行了加密&#xff0c;进一步的保…

Qt QCustomPlot介绍

介绍 主要介绍qcustomplot及其用法 最新版本:QCustomPlot Patch Release 2.1.1//November 6, 2022 下载:https://www.qcustomplot.com/index.php/download 官网:https://www.qcustomplot.com/index.php 简单使用 mainwindow.h /**************************************…

2023年8月京东洗烘套装行业品牌销售排行榜(京东数据开放平台)

鲸参谋监测的京东平台8月份洗烘套装市场销售数据已出炉&#xff01; 根据鲸参谋平台的数据显示&#xff0c;今年8月份&#xff0c;京东平台洗烘套装的销量为1.1万&#xff0c;同比增长约218%&#xff1b;销售额约为1.2亿&#xff0c;同比增长约279%。可以看到&#xff0c;洗烘…

清华用7个ChatGPT模拟《狼人杀》,结果出乎意料!

为了验证大语言模型的沟通、规划、反思等拟人化能力&#xff0c;清华研究团队发布了一篇名为“探索大语言模型在交流游戏中的应用&#xff1a;《狼人杀》实验”的研究论文。 结果显示&#xff0c;通过ChatGPT&#xff08;GPT -turbo-0301&#xff09;构建的7个玩家&#xff0c…

HEC-RAS 1D/2D水动力与水环境模拟从小白到精通

专题一 水动力模型基础 1.水动力模型的本质 2.水动力模型的基本方程与适用范围 3.模型建模要点 4.注意事项与建模经验 专题二 恒定流模型(1D/2D) 1.恒定流及其适用范围 2.水面线分析及其数据要求 3.曼宁公式与恒定流&#xff0c;后处理 4.HEC-RA的水工建筑物&#xff…

【计算机网络】IP协议第二讲(Mac帧、IP地址、碰撞检测、ARP协议介绍)

IP协议第二讲 1.IP和Mac帧2.碰撞检测2.1介绍2.2如何减少碰撞发生2.3MTU2.4一些补充 3.ARP协议3.1协议介绍3.2报文格式分析 1.IP和Mac帧 IP&#xff08;Internet Protocol&#xff09;和MAC&#xff08;Media Access Control&#xff09;帧是计算机网络中两个不同层次的概念&am…

Swift SwiftUI 隐藏键盘

如果仅支持 iOS 15 及更高版本&#xff0c;则可以通过聚焦和取消聚焦来激活和关闭文本字段的键盘。 在最简单的形式中&#xff0c;这是使用 FocusState 属性包装器和 focusable() 修饰符完成的-第一个存储一个布尔值&#xff0c;用于跟踪第二个当前是否被聚焦。 Code struct C…

视频直播美颜sdk与计算机视觉的奇妙结合

在数字时代&#xff0c;视频直播已经成为了人们分享生活、娱乐互动的重要方式之一。而随着社交媒体和在线直播平台的不断发展&#xff0c;用户们对于直播质量和体验提出了越来越高的要求。其中之一就是美颜效果。美颜不仅仅是为了矫正自身缺陷&#xff0c;它更是一种增强直播吸…

牛客练习赛116

(0条未读通知) 牛客练习赛116_ACM/NOI/CSP/CCPC/ICPC算法编程高难度练习赛_牛客竞赛OJ (nowcoder.com) A.等差数列 对于此题可以分为两类&#xff1a; 1.当k 0&#xff0c;此时A1,A2...值都为a 2.当k ! 0,此时又分为两大类&#xff1a; 1.平局&#xff08;发现A1,A2,A3等连…

Automation Anywhere推出新的生成式AI自动化平台,加速提高企业生产力

在9 月 19 日的Imagine 2023 大会上&#xff0c;智能自动化领域的领导者 Automation Anywhere 宣布对其自动化平台进行扩展。推出了新的 Responsible AI Layer&#xff0c;并宣布了四项关键产品更新&#xff0c;包括全新的 Autopilot&#xff0c;它可以利用生成式 AI &#xff…

堆的介绍与堆的实现和调整

个人主页&#xff1a;Lei宝啊 愿所有美好如期而遇 目录 ​​堆的介绍&#xff1a; 关于堆的实现及相关的其他问题&#xff1a; 堆的初始化&#xff1a; 堆的销毁&#xff1a; 插入建堆&#xff1a; 堆向上调整&#xff1a; 交换两个节点的值&#xff1a; 堆向下调整&a…

邓俊辉《数据结构》→ “2.6.5 二分查找(版本A)”之“成功查找长度”递推式推导

【问题描述】 邓俊辉的《数据结构&#xff08;C语言版&#xff09;&#xff08;第3版&#xff09;》&#xff08;ISBN&#xff1a;9787302330646&#xff09;中&#xff0c;开始于第48页的“2.6.5 二分查找&#xff08;版本A&#xff09;”内容在第50页详述了“成功查找长度”的…

【数据结构】排序合集(万字详解)

文章目录 前言插入排序希尔排序选择排序堆排序快速排序hoare原生版本挖坑法前后指针法三数取中优化随机数取key优化三路划分版非递归 归并排序递归非递归调整边界单次归并单次拷贝 总结 前言 排序&#xff0c;以字面意思来说就是通过特定的算法将一组或多组无序或者接近有序的…

02Redis的命令行客户端和桌面客户端的下载和安装

Redis桌面客户端 安装完成Redis服务,我们就可以在Redis的客户端操作Redis的数据库实现数据的CRUD了,客户端分为三类命令行客户端, 图形化桌面客户端,编程客户端 命令行客户端 Redis安装完成后就自带了命令行客户端: redis-cli [options] [commonds] -h选项&#xff1a;指定…

矢量图形编辑软件illustrator 2023 mac软件特点

illustrator 2023 mac是一款矢量图形编辑软件&#xff0c;用于创建和编辑排版、图标、标志、插图和其他类型的矢量图形。 illustrator mac软件特点 矢量图形&#xff1a;illustrator创建的图形是矢量图形&#xff0c;可以无限放大而不失真&#xff0c;这与像素图形编辑软件&am…

计算机网络 实验二 交换机的基本配置

实验二 交换机的基本配置 实验目的 • 掌握交换机的配置方式及切换命令&#xff1b; • 掌握交换机端口的基本配置&#xff1b; • 掌握交换机mac地址的查看与管理方法。 实验设备 以太网交换机一台服务器一台PC机五台配置电缆、网线若干 网络拓扑及IP地址分配 给计算…