tiny-redis 项目可能的问题

https://build-your-own.org/redis/

事件循环怎么实现的

首先我将连接包装为一个 Connect 类,它包含了 socket fd,读写缓冲区,连接状态(这个连接是发送数据还是接收数据)等成员属性

我会在全局维护一个从 socket fd 到它对应的 Connect 对象指针的 map,叫做 fd2conn

在 main 函数中首先 socket、bind、listen 创建一个 listen fd,然后创建一个 vector<struct pollfd> poll_args,接着进入事件循环(一个死循环)

在事件循环中首先将 poll_args 清空,然后压入 listen fd 对应的 pollfd;然后遍历 fd2conn,根据每个 Connect 对象的状态(发送状态还是接收状态)决定 pollfd 中要监听的事件(POLLIN | POLLOUT),把 fd2conn 中的每一项都压入 poll_args 数组,然后调用 poll,传入 poll_args 监听这些文件描述符

等到 poll 返回后,再遍历 poll_args,根据其 fd 在 fd2conn 中找到对应的 Connect 对象,执行 IO 操作。如果 IO 操作结束后 Connect 的状态变为 END,则关闭这个连接并从 map 中删除

最后再判断一下 0 号 poll_args 是否有就绪事件(listen fd),如果有,则说明有新连接到来,再去调用 accept 函数创建新的连接。accept 的结果会在 fd2conn 这个 map 中放入对应的 socket fd 和 Conn 对象

连接定时器是如何实现的

连接定时器是通过双向循环链表串起来的,我在 Connect 结构中还会维护一个 us 级的时间戳 idle_start,它的意义是“上一次操作这个连接的时刻”,也就是开始空闲的时刻,在 Connect 的构造函数里初始化为当前时刻,构造完成后把它挂到链表末尾

idle_start 在每一次处理 IO 时都会被刷新为当前时刻(说明它当前活跃),然后从链表中取下来,再挂到末尾,这样就保证了双向链表自动是递增有序的,这个序就是每个连接的空闲开始时刻,越位于前面的,空闲开始的越早,越可能超时

在每一次 poll 之前,我都去从链表首部获取最小的那个时间戳,这意味着这个连接空闲开始的最早,拥有最小的超时时刻,把这个时间戳加上一个 timeout 值,例如 5ms,然后求这个值减去当前时刻,就是需要最小阻塞等待的时长,然后 poll 的第三个超时参数传入这个值即可

在每一次事件循环的末尾,我都会去从前往后处理这个双向链表管理的定时器:如果空闲开始时间戳 + timeout < 当前时刻,说明超时了,就把这个定时器取下,然后关闭这个连接。直到遇到一个不超时的连接,此时退出循环即可,因为链表是自增有序的

支持哪些命令

set k1 v1
get k1
del k1
keys# zadd zset_name score name
zadd zs1 1.0 abc
zadd zs1 1.5 def
zrem zs1 def
zscore zs1 def
zquery zs1 1.5 def $offset $limitpexpire k1 5000
pttl k1

解释客户端命令发送到服务端的过程

客户端从命令行参数获取到 “set k1 v1” 这个命令,然后调用 socket、connect 建立连接;服务端在本轮事件循环末尾 accept 这个连接,将它放入全局的 fd2conn 的 map 中,设置 idle_start 为当前时刻,把它插入 idle_list 链表的末尾

客户端建立连接后,紧接着调用 write 发送数据,也就是 “set k1 v1” 这个字符串;服务端这时已经进入到下一轮事件循环,在这轮循环中,poll 监听到 fd 可读,返回,然后轮询 poll_args,轮询到该客户端 fd 的可读,进而处理该客户端发来的数据

解释 set 命令的执行过程

客户端发来 “set k1 v1”

服务端首先从 fd 读取数据到 read buffer 中,然后将这3个单词解析到一个 vector<string>,然后根据第一个单词是 set,进入到 do_set 函数

在 do_set 中首先去全局的 hashmap 中查找对应的 key 是否存在,查找的过程是在2个 hashtable 都查询(因为渐进式哈希用到 2 个哈希表),在 hashtable 中查询的过程是先根据哈希值 % 哈希桶长度定位到哈希桶(实际上哈希桶长度永远是 2 ^ n,故只需要 hashcode & (n - 1) 即可),然后遍历哈希桶后面的链表,如果存在 hashcode 一样且判等函数返回 true 的节点,则返回该节点

如果在全局 hashmap 中查到了 k1 已经存在,则将它对应的 value 改为 v1;否则新建一个 Entry,把它的挂到全局 hashmap

HashMap 怎么实现的

// hashtable node, should be embedded into the payload
struct HNode {HNode *next = NULL;uint64_t hcode = 0;
};// a simple fixed-sized hashtable
struct HTab {HNode **tab = NULL;size_t mask = 0;size_t size = 0;
};// the real hashtable interface.
// it uses 2 hashtables for progressive resizing.
struct HMap {HTab ht1;   // newerHTab ht2;   // oldersize_t resizing_pos = 0;
};

HashMap 也采用侵入式容器,我采用拉链法解决哈希冲突,每个链表节点(HNode)只存储一个 hashcode 和指向下一个节点的指针,并不存储真正的 key、value 数据

然后定义一个 HTable 结构,它有3个成员:HNode* 的数组 tab;数组长度 - 1 的 mask,因为限制了数组长度只能为 2 ^ n,所以定位哈希桶时就避免了 mod 运算;还有标志着当前哈希表总节点数量的 size

最后定义真正的 HMap 结构,它采用渐进式哈希的策略,拥有 2 个 HTable,还有一个标志着 rehash 进度位置的 pos 域

解释 HashMap 的插入过程

insert() 的参数是 HNode*,一个 HashMap 会管理2个哈希桶,ht1 和 ht2,插入操作都是在 ht1 中进行

插入 ht1 的过程是先用 hnode 的 hashcode & mask 求得哈希桶的位置,然后执行单链表头插,最后 size++

在 ht1 中插入之后判断 ht2 是否为 null,若为 null,再判断 ht1 的负载因子是否过高,如果过高,则将 ht1 转移给 ht2,ht1.resize,然后设置 HashMap 的 resizing_pos = 0,这个过程可以看作是开启 rehash 过程

解释 rehash 的过程

渐进式 rehash 就是不一次性 rehash 全部节点,而是只在每一步操作 HashTable 的过程中 rehash 固定数目的节点

rehash 开始的标志是在向 ht1 中插入一个节点后,判断 ht2 为空且负载因子 > 8,此时将 ht1 转移给 ht2,ht1.resize,然后设置 HashMap 的 resizing_pos = 0,resize_pos 表示在 ht2 中 rehash 的进度

然后在每一次操作哈希表的函数中,包括查询,新增,删除,都会执行一次 rehash 函数,它会循环执行 128 次:从 ht2 的哈希桶的 resizing_pos 位置摘下头节点,将其 detach 下来,再插入到 ht1 的 HashTable。直到循环次数满或 ht2 被摘空

一旦 ht2 被摘空,就意味着 rehash 终止(因为他是作为 rehash 的循环条件之一)

rehash 的过程中,对哈希表的插入照常在 ht1 中执行,且只有在 ht2 被摘空后,才会再次计算负载因子,开始下一次 rehash

解释 HashMap 的删除过程

pop 传入的是一个 HNode* key 和一个判等函数 eq。首先按需执行 rehash,结束后先在 ht1 中 lookup:先根据 key 中的 hcode & mask 定位到哈希桶,然后在它后面的单链表中遍历查找,遇到 hcode 一致且 eq 函数返回 true 的节点,将其返回

如果在 ht1 中查找结果不为空,则将其 detach 返回(从链表中摘下);否则在 ht2 中执行同样的步骤

如果 ht1 和 ht2 查找都为空,说明待删除的节点不存在,返回 null

哈希冲突你怎么解决的

我会在每一个涉及到在 HashTab 中查找操作的函数都传入一个判等函数 eq,它的参数是2个 HNode*,用于真正判断这两个节点是否相等,例如当使用 HashMap 存储 KV 时,它会根据 HNode* 调用 container_of,找到对应的 Entry 结构(真正存储 string KV)的,然后判断 string key 是否相等

HashMap 如何是如何管理 KV 的

struct Entry {HNode node;std::string key;std::string val;uint32_t type = 0;ZSet* zset = NULL;size_t heap_idx = -1;Entry(const std::string& k) {key = k;node.hcode = str_hash(k);}
};

我定义了一个 Entry 结构,它包含 string 类型的 KV,同时包含了一个 HNode 类型(就是哈希表中单链表的节点类型)

HNode 中只有一个 hachode 和指向下一个 HNode 的指针,hashcode 在 Entry 构造时被初始化

这样,查找某个 string key 时,只需要将 key 构造为 Entry(栈内存),然后把这个 Entry 的 hnode 交给 HMap 的 lookup 方法查找即可。lookup 返回的是一个 HNode*,就是真正存储了这个 key 的 hashcode 的 HNode,然后使用 container_of 即可获得包裹了这个 HNode 的 Entry 指针

set k v 时,也是先将 key 构造为 Entry,然后把这个 Entry 的 hnode 交给 HMap 的 lookup 方法查找。如果返回 NULL,就说明这对 KV 不存在,需要 new Entry,然后将它的 hnode 成员插入全局的 HashMap 中;否则就使用 container_of 定位到包含这个 hnode 的 Entry,然后修改它的 value

换句话说,KV 数据只是被简单地存储在堆内存中,将它的 HNode 域挂到 HashMap 上进行管理和查找

哈希函数如何实现的

哈希函数的作用是将 string 转化为 int64,采用的策略是先初始化一个较大的素数h,然后不断地将字符串中的字节数据累加到 h 上,并乘以一个魔法值,最终得到一个 int64 的魔法值

解释 get 命令的执行过程

客户端发来“get k”命令,服务器将其解析为 vector<string>,判断其第一个单词是 get,调用 do_get 函数进行处理

在 do_get 中,首先用发来的 k 构造一个 Entry(初始化 Entry 中 HNode 的 hcode),然后将这个 Entry 的 HNode 丢给 HashMap 的 lookup 方法进行查找(和判等函数),如果未找到,序列化一个 nil 给客户端

如果找到节点,调用 container_of,定位到包裹这个 HNode 的 Entry 对象指针,获取其 string val 字段,序列化给客户端

解释 del 命令的执行过程(异步)

客户端发来“del k”命令,服务器将其解析为 vector<string>,判断其第一个单词是 del,调用 do_del 函数进行处理

在 do_del 中,首先用发来的 k 构造一个 Entry(初始化 Entry 中 HNode 的 hcode),然后将这个 Entry 的 HNode 丢给 HashMap 的 pop 方法,在 HashMap 中删除这个 HNode

如果这个 HNode 在哈希表中存在,则使用 container_of 找到包裹该 HNode 的对象指针,执行删除

在删除函数中,判断该对象是否为 ZSet,如果是,再判断它是否 too_big,如果是,将其压入线程池的任务队列执行异步删除;否则执行同步删除即可(因为线程调度是有开销的,所以只有对大块数据才执行异步删除)

解释 keys 命令的执行过程

使用 h_scan 函数扫描哈希桶,然后在扫描每个哈希桶后面的单链表,在每个节点上调用 container_of 获取包裹这个 HNode 的节点,然后获取它的 key 值,序列化给客户端

解释一下侵入式数据结构(以双向链表为例)

侵入式双向链表节点只包含 prev 和 next 两个指针,没有数据域。然后基于这个节点定义实现链表的操作,比如 insert 和 detach

对比传统容器,将被容器管理的对象“塞入”容器(也就是 STL 的风格);侵入式容器需要在被管理的对象内增加一个数据域,也就是链表节点,相当于被管理的对象包裹住了链表节点

我在项目中是用双向链表管理连接对象 Connect 的,所以我的 Connect 类里面有一个 DList 类型的数据成员 idle_list(因为链表管理的是 Connect 的超时时长),向容器中插入对象就是 new Connect,然后将这个对象的链表节点数据域挂到链表上;从容器中删除对象就是将这个对象的链表节点从链表上摘除,然后 delete 这个对象

侵入式容器的工作过程需要一个 container_of 指针计算函数,一般用宏函数实现(因为涉及到传入类型、字段名作为参数,不符合 C++ 的函数语法)
它的参数有3个:链表节点指针,包裹链表节点的数据类型(也就是 Connect 类型),链表节点在被管理对象中的字段名字(也就是 idle_list)
它的功能是根据这三者定位到包裹链表节点的数据对象的指针
它的原理是用链表节点指针减去链表节点数据域在整个 Connect 对象中的 offset,就得到了 Connect 对象的地址
这个 container_of 函数在所有的侵入式容器中都是通用的

怎么用 HashMap + AVLTree 实现的 ZSet

struct AVLNode {uint32_t depth;uint32_t cnt;AVLNode *left;AVLNode *right;AVLNode *parent;AVLNode(): depth(1), cnt(1), left(nullptr), right(nullptr), parent(nullptr) {}void init() {depth = cnt = 1;left = right = parent = nullptr;}void update();AVLNode *rot_left();AVLNode *rot_right();AVLNode *fix_left();AVLNode *fix_right();
};AVLNode *avl_fix(AVLNode *node);
AVLNode *avl_del(AVLNode *node);
AVLNode *avl_offset(AVLNode *node, int64_t offset);struct ZNode {AVLNode tree;HNode hmap;double score = 0;std::string name;ZNode(std::string name, double score) {this->name = name;hmap.hcode = str_hash(name);this->score = score;}ZNode* offset(int64_t offset) {auto tnode = avl_offset(&tree, offset);return tnode ? container_of(tnode, ZNode, tree) : nullptr;}
};struct ZSet {AVLNode* tree = NULL;HMap hmap;public:bool add(const std::string& name, double score);ZNode* look_up(const std::string& name);ZNode* pop(const std::string& name);ZNode* query(double score, const std::string& name);~ZSet();private:void tree_add(ZNode* node);void update(ZNode* node, double score);
};

解释 zadd 命令的执行过程

// the structure for the key
struct Entry {HNode node;std::string key;std::string val;uint32_t type = 0;ZSet* zset = NULL;size_t heap_idx = -1;Entry(const std::string& k) {key = k;node.hcode = str_hash(k);}
};

在 Entry 中存储了一个 type 用于区分 key 代表的是 KV 键值对还是 ZSet

在 do_zadd 函数中,首先使用 zset 的名字构造一个 Entry(初始化它的 HNode 中的 hcode),然后在全局的 HashMap 中查找这个 Entry 中的 HNode。如果未找到,则 new 一个 Entry,设置它的 type 为 ZSET,并将它的 HNode 插入到 HashMap;否则如果能找到,则使用 container_of 找到包裹这个 HNode 的 Entry*,判定其为 ZSET

ZSet 有一个 AVL 树结构和一个 HMap,前者用于存储 score,后者用于存储 name;还有增加、查询、删除、范围查询方法

然后执行 ZSet 的 add 方法,它的参数是 (name, score)

在 add 方法中,首先在 ZSet 的 HMap 中查询 name 是否存在,若存在,则执行更新操作:将 ZNode 中的 AVLNode 节点从 AVL 树上摘除,将 score 改为新值,再插入到 AVL 树;否则说明 name 不存在,则 new ZNode,将它的 name 插入到当前 ZSet 持有的 HMap,再把当前节点的 AVLNode 成员挂到 AVL 树上

解释 zrem 的执行过程

zrem zs1 kk1

首先在全局的 HashMap 中根据 zset_name 查找对应的 Entry 是否存在,若存在,再判定其 type == ZSET

然后执行 ZSet 的 pop 方法:首先根据 name 构造一个 HKey(类似于 Entry),在当前 ZSet 管理的 HMap 中 pop

若 pop 返回非 null,说明该节点存在,则使用 container_of 找到包裹它的 ZNode,再将它的 AVLNode tree 从树上删除

解释 zscore 的执行过程

zscore zs1 kk1

首先在全局的 HashMap 中根据 zset_name 查找对应的 Entry 是否存在,若存在,再判定其 type == ZSET

然后执行 ZSet 的 lookup 方法:根据 name 构造一个 HKey(类似于 Entry),在当前 ZSet 管理的 HMap 中 lookup,最后使用 container_of 返回包裹它的 ZNode

如果返回非 null,则解引用 ZNode 的 score 字段将其作为 double 序列化给客户端;否则序列化给客户端 nil

解释 zquery 的执行过程(范围查询)

小顶堆是怎么实现的

// heap.h
struct HeapItem {uint64_t val = 0;size_t *ref = NULL;
};
// 节点上滤或下滤
void heap_update(HeapItem *a, size_t pos, size_t len);// 在 server.cpp 中定义一个全局的 heap
std::vector<HeapItem> heap;

由于堆在逻辑上是一颗完全二叉树,所以可以借助一个数组来实现

解释 pexpire 的执行过程(如何使用小顶堆实现 TTL)

pexpire kk1 5000

pexpire 命令的第二个参数为一个 int 类型的毫秒值,代表过期时长

在 Entry 中,存储了一个 heap_idx,作为堆的数组下标

在 do_expire 中,首先根据传入的 kk1 构造一个 Entry,将其 HNode 丢给全局的 HMap 的 lookup 查找,如果存在,再调用 container_of 找到包裹这个 HNode 的 Entry 对象指针,再将这个指针和 ttl_ms 传入 entry_set_ttl 设置 TTL

在 entry_set_ttl 中,首先根据 ent->heap_idx 定位到堆中的 HeapItem,如果 pos = heap_idx == -1,则需要新建一个 HeapItem,让其 ref 指向 ent->heap_idx,然后将其 pushback 进堆末尾,设置 pos = heap.size() - 1

然后 heap[pos].val = get_monotonic_usec() + (uint64_t)ttl_ms * 1000;,再调整堆

解释 pttl 的执行过程

pttl kk1

在 Entry 中,存储了一个 heap_idx,作为堆的数组下标

在 do_ttl 中,首先根据传入的 kk1 构造一个 Entry,将其 HNode 丢给全局的 HMap 的 lookup 查找,如果存在,再调用 container_of 找到包裹这个 HNode 的 Entry 对象指针 ent

然后根据 ent->heap_idx 在堆中定位到 HeapItem,读出它的 val 字段,也就是过期时刻 expire_at

然后用这个时刻减去当前时刻,就是 ttl,将它序列化给客户端即可

过期 key 是怎么实现自动删除的

我会在每个事件循环的末尾处理定时器,包括连接的定时器还有堆定时器

在处理堆定时器时,因为它是小顶堆,所以堆顶的元素是过期时刻最小的,是最可能超时的

while 循环判断堆顶定时器超时时刻是否小于当前时刻,如果小于,则在全局的 HMap 中将这个 key pop 掉,然后在 entry_set_ttl 中将其 ttl 设置为 -1,一旦设置为 -1,就会被从堆中删除

数据序列化和反序列化如何实现的

tiny-redis 支持多种数据类型的序列化,比如 nil、str、int64、double、err 等

先定义一系列表示数据类型的枚举

enum {SER_NIL = 0,SER_ERR = 1,SER_STR = 2,SER_INT = 3,SER_DBL = 4,SER_ARR = 5,
};

序列化时首先 push_back 数据类型枚举值,然后 append 进真正需要序列化的值,对于字符串需要先 append 进字符串长度

static void out_nil(std::string &out) {out.push_back(SER_NIL);
}static void out_str(std::string &out, const std::string &val) {out.push_back(SER_STR);uint32_t len = (uint32_t)val.size();out.append((char *)&len, 4);out.append(val);
}static void out_int(std::string &out, int64_t val) {out.push_back(SER_INT);out.append((char *)&val, 8);
}static void out_dbl(std::string &out, double val) {out.push_back(SER_DBL);out.append((char *)&val, 8);
}static void out_err(std::string &out, int32_t code, const std::string &msg) {out.push_back(SER_ERR);out.append((char *)&code, 4);uint32_t len = (uint32_t)msg.size();out.append((char *)&len, 4);out.append(msg);
}

客户端遵循同样的协议进行反序列化:先读取数据类型,然后读取数据内容

线程池是怎么实现的

struct Work {void (*f)(void *) = NULL;void *arg = NULL;
};struct ThreadPool {std::vector<pthread_t> threads;std::deque<Work> queue;pthread_mutex_t mu;pthread_cond_t not_empty;ThreadPool(size num_threads);void enqueue(void (*f)(void *), void *arg);
}

线程池中包含如下的成员:

  • 包含所有线程 id 的数组
  • 任务队列,其中每一项任务都是一个函数指针 + 泛型参数指针
  • 互斥锁
  • 标志任务队列不为空的条件变量

创建线程池时 pthread_create() 创建指定数目的线程,并初始化锁和条件变量

向任务队列添加任务时,先获取锁,然后 push_back,然后唤醒条件变量,最后解锁

每个任务是一个死循环:先解锁,判断任务队列是否为空,若为空则循环等待条件变量;否则从任务队列中取出任务,然后解锁,最后执行任务

压测结果如何

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

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

相关文章

003 选择排序

文章目录 先挑最值&#xff0c;再把剩下的挑最值&#xff0c;再把剩下的挑最值。。。 -- 排序函数 function selectionSort(arr) -- 外层循环&#xff0c;从数组的第一个元素开始&#xff0c;对每个元素进行排序 for i 1, #arr do -- 假设当前位置的元素是最小的 local …

LCR 060. 前 K 个高频元素

给定一个整数数组 nums 和一个整数 k &#xff0c;请返回其中出现频率前 k 高的元素。可以按 任意顺序 返回答案。 示例 1: 输入: nums [1,1,1,2,2,3], k 2 输出: [1,2] 示例 2: 输入: nums [1], k 1 输出: [1] 提示&#xff1a; 1 < nums.length < 105k 的取值范…

【SQL Server点滴积累】Setup SQL Server 2008 Database Mirror (二)

【SQL Server点滴积累】Setup SQL Server 2008 Database Mirror (一)-CSDN博客今天分享SQL Server 2008 R2搭建数据库镜像(Database Mirror)https://blog.csdn.net/ncutyb123/article/details/139749117?spm1001.2014.3001.5501本篇Blog基于以上Blog步骤进行SQL Server 2008 R…

python03——文件操作(new)

“变量”open&#xff08;‘文件路径’&#xff0c;‘模式’&#xff09; //注意加引号 “变量”.write( ) //write函数是写的是字符串&#xff0c;如果你写的东西不是字符串&#xff0c;要写成write&#xff08;str&#xff08;。。&#xff09;&#xff09; “变量”.read…

vue3学习教程第四十节(pinia的用法注意事项解构store)

pinia 主要包括以下五部分&#xff0c;经常用到的是 store、state、getters、actions 以下使用说明&#xff0c;注意事项&#xff0c;仅限于 vue3 setup 语法糖中使用&#xff0c;若使用选项式 API 请直接查看官方文档&#xff1a; 一、前言&#xff1a; pinia 是为了探索 vu…

03_意向锁

意向锁&#xff08;Intention Lock&#xff09; 文章目录 意向锁&#xff08;Intention Lock&#xff09;简介类型原理意向锁加锁流程锁兼容矩阵使用场景示例总结扩展&#xff1a;意向锁和共享锁排他锁的加锁流程假设的场景和前提已加锁的情况新的加锁请求加锁流程锁的兼容性矩…

力扣算法-9.回文数

9.回文数 个人思考 首先从示例2可以看出符号也算在整数这个整体内&#xff0c;可以先判断整数若为负数则返回false其次很容易就会想到遍历两次&#xff0c;从头以及从尾&#xff0c;遍历得到的结果相比较&#xff0c;相同则为回文数 public class Alee9 {public static void …

OpenResty的安装及高级使用

OpenResty的安装及高级使用 1. OpenResty的安装1.1. 二进制版本安装1.2. 源码方式安装2. 日志打印header和body3. 替换body体字符串1. OpenResty的安装 OpenResty的中文站点:https://openresty.org/cn/    OpenResty的英文站点:https://openresty.org/en/ 1.1. 二进制版本…

【linux基础】后台执行命令,防止中断nohup

前台运行与后台运行&#xff1a;前台运行&#xff0c;就是运行过程一直在屏幕输出。 目的&#xff1a;1. 提交至后台 & 2.防止中断 nohup 1.终端上不要有大量的log出现&#xff0c;后台运行 (1) & 程序后台运行 #脚本、修改权限 vi test.sh chmod 777 test.sh#后…

ArcGIS Pro SDK (三)Addin控件 3 事件功能类

22 ArcGIS Pro 放置处理程序 目录 22 ArcGIS Pro 放置处理程序22.1 添加控件22.2 Code 23 ArcGIS Pro 构造工具23.1 添加控件23.2 Code 24 ArcGIS Pro 表构造工具24.1 添加控件24.2 Code 22.1 添加控件 22.2 Code 放置处理程序可以实现文件拖动放置、TreeVIew、ListBox等控件拖…

极速安装的艺术:使用 Mamba 革新你的 Conda 环境管理

标题&#xff1a;极速安装的艺术&#xff1a;使用 Mamba 革新你的 Conda 环境管理 引言 在数据科学和机器学习领域&#xff0c;Conda 是一个广受欢迎的包管理器和环境管理器。然而&#xff0c;随着项目规模的增长&#xff0c;Conda 在处理大量依赖时可能会显得缓慢。Mamba&am…

水下机器人ArduSub 固件常用参数

目前最新版的ArduSub 固件是4.1.2&#xff0c;本文的参数是基于这个版本的固件 SURFACE_DEPTH&#xff1a;水表深度读数 当水下机器人在水面时&#xff0c;水压传感器将读取的深度数据&#xff08;以厘米为单位&#xff09;&#xff0c;这个相当于抵消零偏 单位&#xff1a;…

ArcGIS批量设置多图层的三调地类符号

​​ 点击下方全系列课程学习 点击学习—>ArcGIS全系列实战视频教程——9个单一课程组合系列直播回放 01需求说明 这次我们要实现的是将多个地类图层批量符号化。比如将多个三调地类图斑批量符号化。 ​ 有什么好方法呢 &#xff1f; 我们可以将一个图层利用三调符号库进行…

android 从应用中打开第三方应用

打开第三方应用之前需要先判断该应用是否存在&#xff0c;代码如下&#xff1a; public boolean isAppInstalled(Context context, String packageName) {PackageManager packageManager context.getPackageManager();try {packageManager.getPackageInfo(packageName, Packa…

Stable Diffusion 3 正式开源,超强文生图模型 SD3-M 上线,赶紧来试试吧!

前言 我们都知道 Stable Diffusion 3 是一款强大的文生图模型&#xff0c;拥有20亿参数&#xff0c;因其高效的推理速度和卓越的生成效果而备受瞩目。 近日&#xff0c;Stability AI在推特上宣布正式开源了 Stable Diffusion 3 Medium&#xff08;SD3-M&#xff09; 权重&…

Dooprime外汇:如何高效规划家庭理财?从哪里开始?

摘要&#xff1a; 家庭理财是每个家庭都必须面对的重要课题。合理的理财规划不仅能提高家庭的生活质量&#xff0c;还能为未来的生活提供保障。然而&#xff0c;许多人在面对复杂的理财选项和信息时感到无从下手。本文将从不同角度详细分析如何进行高效的家庭理财规划&#xf…

【Playwright+Python】手把手带你写一个自动化测试脚本

如何使用代理方式打开网页 在 playwright.chromium.launch() 中传入 proxy 参数即可&#xff0c;示例代码如下&#xff1a; 1、同步写法&#xff1a; from playwright.sync_api import sync_playwrightproxy {server: http:/127.0.0.1:8080}def run():with sync_playwright(…

Kafka精要

Apach Kafka 是一款分布式流处理框架&#xff0c;用于实时构建流处理应用。它有一个核心 的功能广为人知&#xff0c;即 作为企业级的消息引擎被广泛使用 kafka设计 Kafka 将消息以 topic 为单位进行归纳 将向 Kafka topic 发布消息的程序成为 producers. 将预订 topics 并消…

Linux内核开发-编写一个proc文件

0.前言 上一章&#xff08;点击返回上一章&#xff09;完成了一个内核模块的编写&#xff0c;实现了在内核运行时的动态加载和卸载。 在模块的开发调测过程中或者模块运行过程中&#xff0c;可能需要打印内核模块的变量的值或者想要动态开关模块的运行日志打印&#xff0c;那么…

小盒子跑大模型!基于算能BM1684X+FPGA平台实现大模型私有化部署

当前&#xff0c;在人工智能领域&#xff0c;大模型在丰富人工智能应用场景中扮演着重要的角色&#xff0c;经过不断的探索&#xff0c;大模型进入到落地的阶段。而大模型在落地过程中面临两大关键难题&#xff1a;对庞大计算资源的需求和对数据隐私与安全的考量。为应对这些挑…