lab8 lock

news2024/11/17 1:37:11

image-20230827174215466

PreRead

  1. 第六章
  2. 3.5节:物理内存分配器
  3. 8.1-8.3

文章目录

  • PreRead
  • Memory allocator
    • tasks
    • hints
    • 思路
  • Buffer cache
    • task
    • hints
    • 思路
    • 实现

这次的lab,本质上都是通过将锁的粒度减小来获得性能的提升

  1. 第一个task,可以简单地按cpu划分,因为本来就是空闲页面,谁拥有都一样
  2. 第二个task,本质上也可以简单地按某种性质划分,但是因为我们不只需要分配,我们 还需要查找。如果随便分成若干部分,那么查找起来就非常慢了。所以这也是为什么hints里提示我们用哈希表来划分

Memory allocator

tasks

  1. 你的任务是去实现per-cpu空闲链表,并且在一个cpu的空闲链表空着的时候去偷另一个cpu的空闲链表

  2. 你的所有锁的名字都应该以kmem开头,即在initlock中设置

  3. 你必须通过

    kalloctest,make grade会提醒你它通过了

    usertests,可以先检查一下sbrkmuch

hints

  1. 你可以使用kernel/param.h中的NCPU常数

  2. freerange将所有的空闲内存都给正在运行的free range

  3. cpuid函数会返回当前的cpu号,但是它必须在中断被关闭的时候使用

    因此你需要使用push_offpop_off

  4. 看一下snprintf,学习怎么格式化字符

思路

首先,我们需要以不同的cpu号去访问不同的freelist,最方便的方法就是用一个数组,如下所示。其中count是为了借空闲页面准备的。

struct {
    struct spinlock lock;
    struct run *freelist;
    int count;
} kmem[NCPU];

然后,我们应该在kinit中先初始化各种cpu对应的lock,然后将所有空闲页面都放到运行kinit的cpu上。

这里有几个细节

  1. 首先,我是希望kinit只被一个cpu执行,这样才能保证freerange将所有页面都放到这个cpu上,因此,我需要使用push_offpop_off将kinit包围起来
  2. 对于b_lock这个锁,也是为了借空闲页面准备的,否则可能发生死锁
void kinit() {
    push_off();
    for (int i = 0; i < NCPU; i++) {
        initlock(&kmem[i].lock, "kmem");
        kmem[i].count = 0;
    }
    initlock(&b_lock, "borrow");

    freerange(end, (void *)PHYSTOP);
    pop_off();
}

freerange函数不需要修改

在kfree函数中,当我们准备将这个空闲页面加入到一个freelist时,先关闭中断,然后获取当前cpu号,加入到对应的freelist,还是比较简单的。

其中,如果是freerange调用的kfree,可能会有push_off的嵌套,不过这没关系,只要pop_off成对出现即可

void kfree(void *pa) {
    struct run *r;
    if (((uint64)pa % PGSIZE) != 0 || (char *)pa < end || (uint64)pa >= PHYSTOP)
        panic("kfree");
    // Fill with junk to catch dangling refs.
    memset(pa, 1, PGSIZE);
    r = (struct run *)pa;

    push_off();

    int id = cpuid();
    acquire(&kmem[id].lock);
    r->next = kmem[id].freelist;
    kmem[id].freelist = r;
    kmem[id].count++;
    release(&kmem[id].lock);

    pop_off();
}

kalloc函数,如果当前cpu有空闲页面,则正常操作,否则的话,需要去借页面。我这里采用的借的策略是遍历所有cpu,如果某个cpu有空闲页面,那我就借一半,如果有3个,那我就借2个

// Allocate one 4096-byte page of physical memory.
// Returns a pointer that the kernel can use.
// Returns 0 if the memory cannot be allocated.
void *
kalloc(void) {
    struct run *r;

    push_off();
    int id = cpuid();
    acquire(&kmem[id].lock);
    r = kmem[id].freelist;
    if (r) {
        kmem[id].freelist = r->next;
        kmem[id].count--;
    }
    release(&kmem[id].lock);
    if (!r) {
        acquire(&b_lock);
        r = borrow(id);
        release(&b_lock);
    }
    if (r)
        memset((char *)r, 5, PGSIZE); // fill with junk
    pop_off();
    return (void *)r;
}

具体的borrow函数的实现如下

可以发现,在进入borrow函数之前,我就将当前cpu的freelist的锁给释放了。这是因为我进入borrow之后,会去获取其他freelist的锁,假如我是cpu a,我在borrow里要获取cpu b的锁。而b此时也在运行borrow,那它可能也在获取我的锁。如果我和b在进入borrow前都没有释放自己的锁,那必然就死锁了

另外,为什么在borrow之前要获得一个borrow的大锁呢?这是因为如果我在borrow里如果找到了一个可以借的freelist,那么我还是会获取两个锁,这也是有可能造成问题的,因为我们没有限制获取锁的顺序。为了避免可能的情况,我是用这个大锁来保平安,不过好像不会出现这种情况

void *borrow(int id) {
    for (int i = 0; i < NCPU; i++) {
        acquire(&kmem[i].lock);
        if (kmem[i].count != 0) {
            int b_count = (kmem[i].count + 1) / 2;
            struct run *r = kmem[i].freelist;
            struct run *temp = r;
            for (int i = 0; i < b_count - 1; i++) {
                temp = temp->next;
            }
            kmem[i].freelist = temp->next;
            kmem[i].count -= b_count;
            acquire(&kmem[id].lock);
            if (b_count != 1) {
                temp->next = kmem[id].freelist;
                kmem[id].freelist = r->next;
                kmem[id].count += b_count - 1;
            }
            release(&kmem[id].lock);
            release(&kmem[i].lock);
            return r;
        }
        release(&kmem[i].lock);
    }
    return 0;
}

image-20230827120002931

Buffer cache

task

  1. 修改bgetbrelse,使得对磁盘块的查找和释放在lock上等待的时间越少越好
  2. 通过bcachetestusertests
  3. 请给你的所有lock一个以bcache开头的名字,在initlock中实现它
  4. 这玩意比kalloc要难太多,因为buffer是必须被所有cpu共享的,不能每个cpu一份,因此建议使用一个哈希表,给哈希表的每个桶都设置一个锁
  5. 以下情况发生冲突是没关系的,因为测试不会有这些情况
    1. 两个进程访问同一个磁盘block
    2. 两个进程同时miss然后需要找到一个没用过的block
    3. 两个进程同时操作block,但是它们恰好在你的hash策略中碰撞了,那么你应该避免这种情况,比如调大你的哈希表的size

hints

  1. 阅读xv6的8.1到8.3
  2. 你可以使用固定长度的哈希表,同时选择一个质数去做哈希,比如13
  3. 在哈希表中查找一个buffer和为这个buffer分配一个entry必须是原子性的
  4. 删除所有缓存的链表(bcache.head),时间戳缓存使用它们上一次使用的时间(trap.c中的ticks)。有了这个改变之后,brelse不需要获得bcache的lock,bget可以基于时间戳选择最近最少使用的块
  5. 在bget中使用顺序查找实现LRU是可以的
  6. 你有时可能需要持有两个锁,即bcache锁和每个bucket的锁,保证你可以避免死锁
  7. 当你替换某一块的内容时,需要将buf从一个bucket移到另一个,记得处理这两个bucket相同的情况,否则就死锁了

思路

hints里其实就提供了一个思路,用哈希表去存可用的buf。但是到底怎么实现呢?我觉得这里的思路应该有很多,这里提供一种。

  1. 首先,我们通过blockno % prime为key,构造一个哈希表,其中prime可以取hints里的13
    1. 每一个哈希表的表槽都是一个buf链表+一个表槽锁
    2. 这个链表的结构可以按照原来的bcache里那个head来
    3. 表槽锁就是保护这个表槽里的这个链表
  2. 然后,我们在binit中先将所有的空闲buf都放到key=0的链表中,其实放到哪都可以,平均放到每个表槽也行
  3. bget的时候,先根据blockno计算出key,然后去对应的表槽里找是否这个block已经被取出来了
    1. 如果已经取出来了,则直接返回buf指针,这一个逻辑和原来的bget很像
    2. 如果这个block还没有被取出来,那么我们就去找一个引用数为0的buf,将这个buf的内容换成我们这个block。这里又有两种情况,因此我们需要遍历整个哈希表的表槽,并遍历每个表槽的链表,在链表上执行lru算法,找到一个buf,将这个buf修改为我们的内容,然后移动到key对应的表槽
      1. 这个引用数为0的buf在我们这个表槽的链表里
      2. 这个引用数为0的buf在别的表槽里
  4. brelse中,就很简单,只需要将refcnt减1就行了,都不用将这个buf移动

思路就是这样,不过有一个关键点没有涉及,那就是锁,该如何安排锁呢?

首先,锁肯定是要去保护一些东西的,之前的bcache的那个大锁,是因为保护的东西太多了,所有buf都是被它保护着,这就导致很慢了,因为可能不同的cpu没有冲突,但依然要等很久。

因此,我们这里采用一种哈希表的方法,使得锁管理的范围变小。对于某个key对应的表槽的那个锁,它只需要管理blockno%prime==key的block,也就是说,我们将原来的一个锁,变成了prime个锁,使得它们管理的范围缩小了prime倍。当然了,这是对于那些存储了某些block的内容的buf而言的,如果它存储了,那么它肯定就在对应的表槽中。至于那些没有存储的,或者说引用数为0的,我们可以称为空闲buf,它们按什么方式组织都行,甚至可以专门搞一个空闲链表都可以。但是这里采用的方式比较偷懒,也比较巧,即没有存储的一开始就放在key=0的表槽链表,引用计数为0的,直接不处理,反正它们都可能在bget中被访问到

最后,锁的作用呢?我们这里有两个锁,一个锁是表槽对应的锁,一个是每个buf对应的锁,它们分别保护了什么?

  1. 表槽锁当然是保护了表槽里的那个链表,也就是保护了链表的每个节点,即一个个buf,使得链表或者每个buf在被修改时,只会有一个线程对它们进行修改
  2. 而每个buf对应的锁,它的作用是使得,在某一刻,它永远只会被一个线程所拥有,不会同时被多个线程拥有。所以这个锁使用起来非常简单,我们只需要在我们找到了一个正确的buf,将它作为res在bget中返回之前调用这个buf的锁即可

实现

首先是整体的布局

  1. 这里的bcache最好不删,因为这个变量默认就开辟了NBUFstruct buf,省的我们自己申请空间创造了
  2. 哈希表有prime个表槽,每个表槽一个链表+一个锁,链表的结构和之前的一样,一个head作为dummynode,方便操作
  3. 一些宏,主要是方便,省的后面输入一大串代码来获取锁和释放锁
#define prime 13

struct {
    struct buf buf[NBUF];
} bcache;

struct {
    struct spinlock lock;
    struct buf head;
} ht[prime];

#define LOCK(i) (acquire(&ht[i].lock));
#define UNLOCK(i) (release(&ht[i].lock));

binit函数

  1. 首先给每个表槽的锁给初始化,然后初始化这个head
  2. 将所有的buf都放到key=0的表槽中

这个过程很像之前binit,抄就完事了

void binit(void) {
    struct buf *b;

    char a[20];
    for (int i = 0; i < prime; i++) {
        snprintf(a, sizeof(a), "bcache_%d", i);
        initlock(&ht[i].lock, a);
        ht[i].head.prev = &ht[i].head;
        ht[i].head.next = &ht[i].head;
    }

    // Create linked list of buffers
    for (b = bcache.buf; b < bcache.buf + NBUF; b++) {
        initsleeplock(&b->lock, "buffer");
        insert_into_ht(b, 0);
    }
}

可以发现,这里用到了一个insert_into_ht的操作,定义如下

  1. 可以从原来的brelse
void insert_into_ht(struct buf *b, int key) {
    b->next = ht[key].head.next;
    b->prev = &ht[key].head;
    ht[key].head.next->prev = b;
    ht[key].head.next = b;
}

void delete_from_ht(struct buf *b) {
    b->next->prev = b->prev;
    b->prev->next = b->next;
}

brelse函数的实现也非常简单

  1. 释放这个buf的锁,其实这个释放放在哪一行都没问题
    1. 因为它的refcnt还没减1,就注定了它不会被别人给夺舍
    2. 只要unlock不取消掉,就没有人能够访问到它
void brelse(struct buf *b) {
    releasesleep(&b->lock);
    int key = b->blockno % prime;
    LOCK(key);
    b->refcnt -= 1;
    UNLOCK(key);
}

bpinbunpin的实现也很简单

  1. 首先,这两个函数肯定是在一个buf已经有了一个block,并且refcnt不为0的情况下调用的
  2. 我们只需要先获得对应表槽的锁,即获得对这个buf的修改权,然后修改,就可以了
void bpin(struct buf *b) {
    int key = b->blockno % prime;
    LOCK(key);
    b->refcnt++;
    UNLOCK(key);
}

void bunpin(struct buf *b) {
    int key = b->blockno % prime;
    LOCK(key);
    b->refcnt--;
    UNLOCK(key);
}

大头戏bget来了

  1. 首先通过search_in_ht尝试去找找这个block是不是已经被读入了某个buf里,这种情况如果成功,那就和之前bget前一部分逻辑一模一样
  2. 如果失败了,那么就需要通过search_in_other去整个哈希表中找一个空闲的buf,这个操作一定会成功,否则在xv6里就直接给它来一个panic,原函数也是这么写的
static struct buf *
bget(uint dev, uint blockno) {
    struct buf *b;
    int key = blockno % prime;
    // 尝试去对应的哈希表槽查找
    LOCK(key);
    b = search_in_ht(dev, blockno, key);
    if (b) {
        UNLOCK(key);
        acquiresleep(&b->lock);
        return b;
    }
    // 至此,没有在对应的表槽找到,遍历所有哈希表的表槽,不过优先处理自己表槽的
    // 这里是带着key对应的锁去查找的
    b = search_in_other(dev, blockno, key);
    // 这个b不可能为0,否则直接panic了
    UNLOCK(key);
    acquiresleep(&b->lock);
    return b;
}

search_in_ht的实现如下所示,就是遍历链表,如果找到了,更新属性,然后返回。其中更新属性会用到update_time

void update_time(struct buf *b) {
    acquire(&tickslock);
    b->timestamp = ticks;
    release(&tickslock);
}
struct buf *search_in_ht(uint dev, uint blockno, int key) {
    struct buf *b;
    for (b = ht[key].head.next; b != &ht[key].head; b = b->next) {
        if (b->dev == dev && b->blockno == blockno) {
            b->refcnt++;
            update_time(b);
            return b;
        }
    }
    return 0;
}

search_in_other就比较复杂

  1. 这里采取的遍历顺序是从自己这里开始遍历,用一个cycle来控制遍历prime次,之所以这样做,是为了避免每次都是0开始遍历。这样操作相对来说会提高点性能,不会出现前面的表槽没有空闲的,后面的表槽全是空闲的

  2. 如果我们要进入的某个表槽不是自己,那么就需要获取那个表槽的锁

    1. 这里是有可能死锁的
      1. 因为我们进入这个函数的时候,是带着key对应的锁的,现在又去请求i对应的锁
      2. 假如某个cpu是带着i对应的锁进入这个函数,正在请求key对应的锁,岂不是就死锁了?
      3. 感觉是自带的评测没有检查出来,这里还是有点问题的。不过懒得改了
  3. 接下来就是通过search_lru_free_in_ht去这个兄弟那里找一找有没有空闲的

    struct buf *search_lru_free_in_ht(uint dev, uint blockno, int key) {
        struct buf *b;
        struct buf *lru_b = 0;
        for (b = ht[key].head.next; b != &ht[key].head; b = b->next) {
            if (b->refcnt == 0 && (lru_b == 0 || lru_b->timestamp > b->timestamp)) {
                lru_b = b;
            }
        }
        return lru_b;
    }
    
  4. 如果没有,那么视情况释放锁,然后continue

  5. 如果有的话

    1. 更新各种属性
    2. 如果这个buf是别的表槽,将这个buf挪到key对应的表槽
    3. 最后视情况释放这个兄弟锁,返回答案
struct buf *search_in_other(uint dev, uint blockno, int key) {
    struct buf *b;
    for (int i = key, cycle = 0; cycle < prime; cycle++, i = (i + 1) % prime) {
        // 如果不是自己,则给这个兄弟上个锁
        if (i != key) {
            LOCK(i);
        }
        // 在这个兄弟里去找一下
        b = search_lru_free_in_ht(dev, blockno, i);
        // 这个兄弟里没有空闲页面
        if (!b) {
            if (i != key) {
                UNLOCK(i);
            }
            continue;
        }
        // 在这个兄弟里找到了空闲页面
        // 先更新属性
        b->dev = dev;
        b->blockno = blockno;
        b->valid = 0;
        b->refcnt = 1;
        update_time(b);
        // 如果不是自己的哈希槽里的,将这个页面放到自己哈希表槽中
        if (i != key) {
            delete_from_ht(b);
            insert_into_ht(b, key);
        }
        // 释放哈希表的锁
        if (i != key) {
            UNLOCK(i);
        }
        return b;
    }
    panic("no free buf");
}

整体代码如下

// Buffer cache.
//
// The buffer cache is a linked list of buf structures holding
// cached copies of disk block contents.  Caching disk blocks
// in memory reduces the number of disk reads and also provides
// a synchronization point for disk blocks used by multiple processes.
//
// Interface:
// * To get a buffer for a particular disk block, call bread.
// * After changing buffer data, call bwrite to write it to disk.
// * When done with the buffer, call brelse.
// * Do not use the buffer after calling brelse.
// * Only one process at a time can use a buffer,
//     so do not keep them longer than necessary.

#include "types.h"
#include "param.h"
#include "spinlock.h"
#include "sleeplock.h"
#include "riscv.h"
#include "defs.h"
#include "fs.h"
#include "buf.h"
#include <x86_64-linux-gnu/sys/types.h>

#define prime 13

struct {
    struct buf buf[NBUF];
} bcache;

struct {
    struct spinlock lock;
    struct buf head;
} ht[prime];

#define LOCK(i) (acquire(&ht[i].lock));
#define UNLOCK(i) (release(&ht[i].lock));

void update_time(struct buf *b) {
    acquire(&tickslock);
    b->timestamp = ticks;
    release(&tickslock);
}

void insert_into_ht(struct buf *b, int key) {
    b->next = ht[key].head.next;
    b->prev = &ht[key].head;
    ht[key].head.next->prev = b;
    ht[key].head.next = b;
}

void delete_from_ht(struct buf *b) {
    b->next->prev = b->prev;
    b->prev->next = b->next;
}

void binit(void) {
    struct buf *b;

    char a[20];
    for (int i = 0; i < prime; i++) {
        snprintf(a, sizeof(a), "bcache_%d", i);
        initlock(&ht[i].lock, a);
        ht[i].head.prev = &ht[i].head;
        ht[i].head.next = &ht[i].head;
    }

    // Create linked list of buffers
    for (b = bcache.buf; b < bcache.buf + NBUF; b++) {
        initsleeplock(&b->lock, "buffer");
        insert_into_ht(b, 0);
    }
}

struct buf *search_in_ht(uint dev, uint blockno, int key) {
    struct buf *b;
    for (b = ht[key].head.next; b != &ht[key].head; b = b->next) {
        if (b->dev == dev && b->blockno == blockno) {
            b->refcnt++;
            update_time(b);
            return b;
        }
    }
    return 0;
}

struct buf *search_lru_free_in_ht(uint dev, uint blockno, int key) {
    struct buf *b;
    struct buf *lru_b = 0;
    for (b = ht[key].head.next; b != &ht[key].head; b = b->next) {
        if (b->refcnt == 0 && (lru_b == 0 || lru_b->timestamp > b->timestamp)) {
            lru_b = b;
        }
    }
    return lru_b;
}

struct buf *search_in_other(uint dev, uint blockno, int key) {
    struct buf *b;
    for (int i = key, cycle = 0; cycle < prime; cycle++, i = (i + 1) % prime) {
        // 如果不是自己,则给这个兄弟上个锁
        if (i != key) {
            LOCK(i);
        }
        // 在这个兄弟里去找一下
        b = search_lru_free_in_ht(dev, blockno, i);
        // 这个兄弟里没有空闲页面
        if (!b) {
            if (i != key) {
                UNLOCK(i);
            }
            continue;
        }
        // 在这个兄弟里找到了空闲页面
        // 先更新属性
        b->dev = dev;
        b->blockno = blockno;
        b->valid = 0;
        b->refcnt = 1;
        update_time(b);
        // 如果不是自己的哈希槽里的,将这个页面放到自己哈希表槽中
        if (i != key) {
            delete_from_ht(b);
            insert_into_ht(b, key);
        }
        // 释放哈希表的锁
        if (i != key) {
            UNLOCK(i);
        }
        return b;
    }
    panic("no free buf");
}

static struct buf *
bget(uint dev, uint blockno) {
    struct buf *b;
    int key = blockno % prime;
    // 尝试去对应的哈希表槽查找
    LOCK(key);
    b = search_in_ht(dev, blockno, key);
    if (b) {
        UNLOCK(key);
        acquiresleep(&b->lock);
        return b;
    }
    // 至此,没有在对应的表槽找到,遍历所有哈希表的表槽,不过优先处理自己表槽的
    // 这里是带着key对应的锁去查找的
    b = search_in_other(dev, blockno, key);
    // 这个b不可能为0,否则直接panic了
    UNLOCK(key);
    acquiresleep(&b->lock);
    return b;
}

// Return a locked buf with the contents of the indicated block.
struct buf *
bread(uint dev, uint blockno) {
    struct buf *b;

    b = bget(dev, blockno);
    if (!b->valid) {
        virtio_disk_rw(b, 0);
        b->valid = 1;
    }
    return b;
}

// Write b's contents to disk.  Must be locked.
void bwrite(struct buf *b) {
    if (!holdingsleep(&b->lock))
        panic("bwrite");
    virtio_disk_rw(b, 1);
}

// Release a locked buffer.
// Move to the head of the most-recently-used list.
void brelse(struct buf *b) {
    releasesleep(&b->lock);
    int key = b->blockno % prime;
    LOCK(key);
    b->refcnt -= 1;
    UNLOCK(key);
}

void bpin(struct buf *b) {
    int key = b->blockno % prime;
    LOCK(key);
    b->refcnt++;
    UNLOCK(key);
}

void bunpin(struct buf *b) {
    int key = b->blockno % prime;
    LOCK(key);
    b->refcnt--;
    UNLOCK(key);
}

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

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

相关文章

Python Opencv实践 - Sobel边缘检测

import cv2 as cv import numpy as np import matplotlib.pyplot as pltimg cv.imread("../SampleImages/pomeranian.png", cv.IMREAD_GRAYSCALE) print(img.shape)#Sobel边缘检测 #cv.sobel( src, ddepth, dx, dy[,ksize[, scale[, delta[, borderType]]]] ) #src:…

当面临在职备考不确定性的结果时,你可能需要闭着眼冲一下

提前批面试在某种程度上像是联考分流幕后的那只无形之手&#xff0c;既助长了拿到优秀资格考生的备考热情&#xff0c;又打击了提面落榜考生的笔试自信心。就在这样的局面下&#xff0c;使得项目最终完成了联考前的分流操作。但如果你还是遵从自己的本心的话&#xff0c;就应该…

算法-图BFS/DFS-单词接龙

算法-图BFS/DFS-单词接龙 1 题目概述 1.1 题目出处 https://leetcode-cn.com/problems/number-of-islands 1.2 题目描述 给定两个单词&#xff08;beginWord 和 endWord&#xff09;和一个字典&#xff0c;找到从 beginWord 到 endWord 的最短转换序列的长度。转换需遵循如…

2023年6月GESP C++ 三级试卷解析

2023年6月GESP C 三级试卷解析 一、单选题&#xff08;每题2分&#xff0c;共30分&#xff09; 1.高级语言编写的程序需要经过以下&#xff08; &#xff09;操作&#xff0c;可以生成在计算机上运行的可执行代码。 A.编辑 B.保存 C.调试 D.编译 【答案】D 【考纲知识点…

LeetCode-406-根据身高重建队列

题目描述&#xff1a; 假设有打乱顺序的一群人站成一个队列&#xff0c;数组 people 表示队列中一些人的属性&#xff08;不一定按顺序&#xff09;。每个 people[i] [hi, ki] 表示第 i 个人的身高为 hi &#xff0c;前面 正好 有 ki 个身高大于或等于 hi 的人。 请你重新构造…

IDEA项目启动的时候找不到类

IDEA项目启动的时候找不到类 我在运行微服务的项目的时候启动多个项目由于存在依赖关系&#xff0c;但是我确实是引入了对应的依赖的地址但是就是找不到对应的类。 解决的方法&#xff1a;

代码随想录算法训练营第四十八天|LeetCode 583,72,编辑距离总结篇

目录 LeetCode 583.两个字符串的删除操作 动态规划五步曲&#xff1a; 1.确定dp[i][j]的含义 2.找出递推公式 3.初始化dp数组 4.确定遍历方向 5.打印dp数组 LeetCode 72.编辑距离 动态规划五步曲&#xff1a; 1.确定dp[i][j]的含义 2.找出递推公式 3.初始化dp数组 4.确定遍历方…

模2运算规则

模2加法 模2加法没有进位&#xff0c;等同于异或运算。一位数的模2加法规则如下&#xff1a; 0 0 0 0 1 1 1 0 1 1 1 0 多位数的模2加法中&#xff0c;每一位都按照上面的规则进行&#xff0c;例如: 当多个数相加&#xff0c;对应位置上如果有偶数个1&#xff0c;…

【OpenCV实战】3.OpenCV颜色空间实战

OpenCV颜色空间实战 〇、Coding实战内容一、imread1.1 函数介绍1.2 Flags1.3 Code 二. 色彩空间2.1 获取单色空间2.2. HSV、YUV、RGB2.3. 不同颜色空间应用场景 〇、Coding实战内容 OpenCV imread()方法不同的flags差异性获取单色通道【R通道、G通道、B通道】HSV、YUV、RGB 一…

基于学生心理学算法优化的BP神经网络(预测应用) - 附代码

基于学生心理学算法优化的BP神经网络&#xff08;预测应用&#xff09; - 附代码 文章目录 基于学生心理学算法优化的BP神经网络&#xff08;预测应用&#xff09; - 附代码1.数据介绍2.学生心理学优化BP神经网络2.1 BP神经网络参数设置2.2 学生心理学算法应用 4.测试结果&…

证券低延时环境设置并进行性能测试

BIOS设置BIOS参考信息 关闭 logical Process Virtualization Technology 在System Profiles Settings 中System Profile 选择Performance Workload Profile 选择HPC Profile OS中信息参考在/etc/default/grub文件中添加 intel_idle.max_cstate=0 processor.max_cstate=0 idle=p…

使用Easy Chm制作chm文档步骤

前言 软件发布后需要相应的文档说明&#xff0c;CHM是微软新一代的帮助文件格式&#xff0c;利用HTML作源文&#xff0c;把帮助内容以类似数据库的形式编译储存。因为使用方便&#xff0c;形式多样也常被采用作为电子书的格式&#xff1b; 制作类似的chm文档可以使用Easy Chm软…

pygame实现物体运动拖尾尾迹

文章目录 前言主要内容讲解&#xff1a;代码 总结更多宝藏 前言 &#x1f60e;&#x1f973;&#x1f60e;&#x1f920;&#x1f916;&#x1f648;&#x1f4ad;&#x1f373;&#x1f371; 本文我们来讲一下如何使用pygame实现一个拖尾特效。 主要内容 &#x1f99e;&am…

Day42|leetcode 416. 分割等和子集

01背包问题&#xff08;二维&#xff09; 视频讲解&#xff1a;带你学透0-1背包问题&#xff01;| 关于背包问题&#xff0c;你不清楚的地方&#xff0c;这里都讲了&#xff01;| 动态规划经典问题 | 数据结构与算法_哔哩哔哩_bilibili 01背包问题&#xff08;一维、滚动数组…

Linux(多进程与多线程)

目录 1、进程与线程概念 1.1 进程 1.2 线程 1.3 进程与线程区别 2、多进程 2.1多进程概念 2.2 进程相关API 2.3 多进程编程 3、多线程 3.1 多线程概念 3.2 多线程相关API 3.3 多线程编程 1、进程与线程概念 1.1 进程 在计算机科学中&#xff0c;进程是正在执行中…

【PyQt】QGraphicsView场景导出为图片

1 需求 需要将用户绘制的场景导出为图片。即 QGraphicsView中的Scene导出为图片。 2 代码 # 提示&#xff1a;此函数应能访问 QGraphicsView 对象。 # 参考&#xff1a;作者的项目中&#xff0c;此函数在某个QMainWindow类中&#xff0c;作为导出按钮的槽函数。import sys …

QGIS学习2-QGIS设置中文界面、导出地图、修改显示投影、自定义投影等

1、设置中文界面 参照官方给的提示&#xff1a; https://qgis.org/en/site/getinvolved/translate.html 2、QGIS功能介绍 QGIS支持功能还是很全面的。 而且提供了很全面的插件库 https://plugins.qgis.org/plugins/ 3、工程文档介绍 可以直接从菜单栏对工程文档进行操作…

SpringBoot在IDEA里实现热部署

使用步骤 1.引入依赖 <!--devtools热部署--> <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-devtools</artifactId><optional>true</optional><scope>true</scope><versi…

QChart——折线

Qchart的图形显示依附于QChartView&#xff0c;创建一个QChartView继承类&#xff0c;通过窗口部件的提升进行图表的显示 一、简单认识QLineSeries QLineSeries属于折线类&#xff0c;它继承于QXYSeries类&#xff0c;可以使用QXYSeries类所有方法&#xff0c;对折线进行属性设…

Jmeter(二十八):beanshell的使用

Beanshell 是一种轻量级的 Java 脚本,纯 Java 编写的,能够动态的执行标准 java 语法及一些扩展脚本语法,类似于 javaScript,在工作中可能用的多的就是: Beanshell 取样器:跟Http取样器并列Beanshell前置处理器:一般放在Http请求下,在请求前处理一些数据Beanshell后置处…