C++并发:设计无锁数据结构

只要摆脱锁,实现支持安全并发访问的数据结构,就有可能解决大粒度锁影响并发程度以及错误的加锁方式导致死锁的问题。这种数据结构称为无锁数据结构

在了解本文时,务必读懂内存次序章节。

在设计无锁数据结构时,需要极为小心谨慎,因为它们的正确实现相当不容易。导致代码出错的情形难以复现。

1 定义和推论

算法和数据结构中,只要采用了互斥,条件变量或future进行同步操作,就称之为阻塞型算法和阻塞型数据结构

如果应用程序调用某些库函数,发起调用的线程便会暂停运行,即在函数的调用点阻塞,等到另一线程完成某项相关操作,阻塞才会解除,前者才会继续运行。这种库函数的调用被命名为阻塞型调用

1.1 非阻塞型数据结构

在实践中,我们需要参考下列定义,根据适用的条款,分辨该型别/函数属于哪一类:

1 无阻碍:假定其他线程全部暂停,则目标线程将在有限步骤内完成自己的操作。

2 无锁:如果多个线程共同操作一份数据,那么在有限步骤内,其中某一线程能够完成自己的操作。

3 免等:在某份数据上,每个线程经过有限步骤就能完成自己的操作,即使该份数据同时被其他多个线程所操作。

绝大多数时候无障碍算法并不切实有用,因为其他线程全部暂停这对于一个项目来说是一个非常匪夷所思的场景。

1.2 无锁数据结构

免等和无锁数据结构能够避免线程受饿问题,也就是说,两个并发执行的线程,其中一个按部就班的执行操作,另一个总是在错误的时机开始执行操作,导致被迫中止,反复开始,试图完成操作。

1.3 免等的数据结构

具备额外功能的无锁数据结构,如果它被多个线程访问,不论其他线程上发生了什么,每个线程都能在有限的步骤内完成自己的操作。若多个线程之间存在冲突,导致某算法无限制地反复尝试执行操作,那它就是免等算法(比如使用while循环进行的一些操作,在里面执行比较-交换操作)。

1.4 无锁数据结构的优点和缺点

1.4.1 优点

本质上,使用无锁数据结构的首要原因是:最大限度地实现并发

基于锁的实现往往导致线程需要阻塞,在无锁数据结构上,总是存在某个线程能执行下一步操作。免等数据结构则完全无需等待,但是难以实现,很容易写成自旋锁。

还有一点是,代码健壮性

假设数据结构的写操作受锁保护,如果线程在持锁期间终止,那么该数据结构仅完成了部分改动,且此后无从修补。但是,若某线程操作无锁数据结构时意外终结,则丢失的数据仅限于它持有的部分,其他数据依然完好,能被别的线程进行处理(不会阻塞等待锁)

1.4.2 缺点。需要注意的地方

1.4.2.1 不变量相关

力求保持不变量成立,或选取别的可以一直成立的不变量作为替代。

1.4.2.2 留心内存次序约束

1.4.2.3 数据修改使用原子操作

1.4.2.4 就其他线程所见,各项修改步骤次序正确

1.4.2.5 避免活锁

假设两个线程同时更改同一份数据结构,若它们所做的改动都导致对方从头开始操作,那双方就会反复循环,不断重试,这种现象称为活锁。它的出现与否完全取决于现成的调度次序,故往往只会短暂存在。因此它们仅降低程序性能,不会导致严重问题。也因此可能会导致提高了操作同一个数据结构的并发程度,缩短了单个线程因等待消耗的时间,却降低了整体的性能。

1.4.2.6 缓存乒乓

并且,如果多个线程访问相同的原子变量,硬件必须在线程之间同步数据,这还会造成缓存乒乓现象,导致严重的性能损耗。

2 无锁数据结构范例

无锁数据结构依赖原子操作和内存次序约束(作用是令其他线程按正确的内存次序见到数据操作的过程),默认内存次序std::memory_order_seq_cst最易于分析和推理(全部该次序的操作形成确定且唯一的总序列)

2.1 实现线程安全的无锁栈

需要保证:

1 一旦某线程将一项数据加入栈容器,就能立即安全地被另一线程取出

2 只有唯一一个线程能获取该项数据

#include <atomic>
#include <memory>template<typename T>
class lock_free_stack {
private:struct node {std::shared_ptr<T> data;node* next;node(T const& data_) : data(std::make_shared<T>(data_)) {}};std::atomic<node*> head;
public:void push(T const& data) {node* const new_node = new node(data);new_node->next = head.load();while (!head.compare_exchange_weak(new_node->next, new_node));}std::shared_ptr<T> pop() {node* old_head = head.load();while(old_head && !head.compare_exchange_weak(old_head, old_head->next));return old_head ? old_head->data : std::shared_ptr<T>();}
};

比较交换操作:如果head指针与第一个参数new_node->next所存储值相同,将head改为指向第二个参数new_node,compare_exchange_weak返回true。如果head指针与第一个参数new_node->next所存储值不同,表示head指针被其他线程修改过,第一个参数new_node->next就被更新成head指针的当前值,并且compare_exchange_weak返回false,让循环继续。

上述代码虽然是无锁实现,但是却是非免等的,如果compare_exchange_weak总是false,理论上push和pop中的while循环要持续进行。

2.2 制止内存泄漏:在无锁数据结构中管理内存

本质问题是:若要删除某节点,必须先行确认,其他线程并未持有指向该节点的指针。

对于上述实现,若有多个线程同时调用pop,需要采取措施判断何时删除节点。

可以维护一个等待删除链表。

#include <atomic>
#include <memory>template<typename T>
class lock_free_stack {
private:struct node {std::shared_ptr<T> data;node* next;node(T const& data_) : data(std::make_shared<T>(data_)) {}};std::atomic<node*> head;std::atomic<unsigned> threads_in_pop;std::atomic<node *> to_be_deleted;static void delete_nodes(node* nodes) {while (nodes) {node* next = nodes->next;delete nodes;nodes = next;}}void try_reclaim(node* old_head) {if (threads_in_pop == 1) {node* nodes_to_delete = to_be_deleted.exchange(nullptr);if (!--threads_in_pop) {delete_nodes(nodes_to_delete);} else if (nodes_to_delete) {chain_pending_nodes(nodes_to_delete);}delete old_head;} else {chain_pending_node(old_head);--threads_in_pop;}}void chain_pending_nodes(node* nodes) {node* last = nodes;while (node* const next = last->next) {last = next;}chain_pending_nodes(nodes, last);}void chain_pending_nodes(node* first, node* last) {last->next = to_be_deleted;while (!to_be_deleted.compare_exchange_weak(last->next, first));}void chain_pending_node(node* n) {chain_pending_nodes(n, n);}  
public:void push(T const& data) {node* const new_node = new node(data);new_node->next = head.load();while (!head.compare_exchange_weak(new_node->next, new_node));}std::shared_ptr<T> pop() {++threads_in_pop;node* old_head = head.load();while(old_head && !head.compare_exchange_weak(old_head, old_head->next));std::shared_ptr<T> res;if (old_head) {res.swap(old_head->data);}try_reclaim(old_head);return res;}
};

2.3 运用风险指针检测无法回收的节点

术语“风险指针”是一种技法,得名缘由是:若某节点仍被其他线程指涉,而我们依然删除它,此举便成了“冒险”动作。删除目标节点后,别的线程还持有指向它的引用,还通过这一引用对其进行访问,便会导致程序产生未定义行为。

上述机制产生的基本思想:假设当前线程要访问某对象,而它却即将被别的线程删除,那就让当前线程设置指涉目标对象的风险指针,以通知其他线程删除该对象将产生实质风险。若程序不再需要该对象,风险指针被清零。

#include <atomic>
#include <thread>
unsigned const max_hazard_pointers=100;
struct hazard_pointer {std::atomic<std::thread::id> id;std::atomic<void*> pointer;
};hazard_pointer hazard_pointers[max_hazard_pointers];class hp_owner {hazard_pointer* hp;public:hp_owner(hp_owner const&)=delete;hp_owner operator=(hp_owner const&)=delete;hp_owner(): hp(nullptr) {for (unsigned i = 0; i < max_hazard_pointers; ++i) {std::thread::id old_id;if (hazard_pointers[i].id.compare_exchange_strong(old_id, std::this_thread::get_id())) {hp = &hazard_pointers[i];break;}}if (!hp) {throw std::runtime_error("No hazard");}}std::atomic<void*>& get_pointer() {return hp->pointer;}~hp_owner() {hp->pointer.store(nullptr);hp->id.store(std::thread::id());}
};std::atomic<void*>& get_hazard_pointer_for_current_thread() {thread_local static hp_owner hazard;return hazard.get_pointer();
}

2.4 借引用计数检测正在使用中的节点

#include <atomic>
#include <memory>template<typename T>
class lock_free_stack {
private:struct node {std::shared_ptr<T> data;node* next;node(T const& data_) : data(std::make_shared<T>(data_)) {}};std::shared_ptr<node> head;
public:void push(T const& data) {std::shared_ptr<node> const new_node = std::make_shared<node>(data);new_node->next = std::atomic_load(&head);while (!std::atomic_compare_exchange_weak(&head, &new_node->next, new_node));}std::shared_ptr<T> pop() {std::shared_ptr<node> old_head = std::atomic_load(&head);while (old_head && std::atomic_compare_exchange_weak(&head, &old_head, std::atomic_load(&old_head->next)));if (old_head) {std::atomic_store(&old_head->next, std::shared_ptr<node>());return old_head->next;}return std::shared_ptr<T>();}~lock_free_stack() {while (pop());}
};

引用计数针对各个节点分别维护一个计数器,随时了解访问它的线程数目。

std::shared_ptr的引用计数在这里无法借鉴,因为他的原子特性不一定通过无锁机制实现,若强行按照无锁方式实现该指针类的原子操作,很可能造成额外开销。

2.4.1 std::experimental::atomic_shared_ptr<T>

std::shared_ptr无法结合std::atomic<>使用,原因是std::shared_ptr<T>并不具备平实拷贝语义。但是std::experimental::atomic_shared_ptr<T>支持,因此可以正确的处理引用计数,同时令操作原子化。

#include <atomic>
#include <memory>template<typename T>
class lock_free_stack {
private:struct node {std::shared_ptr<T> data;std::experimental::atomic_shared_ptr<node> next;node(T const& data_) : data(std::make_shared<T>(data_)) {}};std::experimental::atomic_shared_ptr<node> head;
public:void push(T const& data) {std::shared_ptr<node> const new_node = std::make_shared<node>(data);new_node->next = std::atomic_load(&head);while (!std::atomic_compare_exchange_weak(&head, &new_node->next, new_node));}std::shared_ptr<T> pop() {std::shared_ptr<node> old_head = std::atomic_load(&head);while (old_head && std::atomic_compare_exchange_weak(&head, &old_head, std::atomic_load(&old_head->next)));if (old_head) {std::atomic_store(&old_head->next, std::shared_ptr<node>());return old_head->data;}return std::shared_ptr<T>();}~lock_free_stack() {while (pop());}
};

2.4.2 内、外部计数器进行引用计数

一种经典的实现是,使用两个计数器:内、外部计数器各一。两个计数器之和即为节点的总引用数目。

外部计数器与节点的指针组成结构体,每当指针被读取,外部计数器自增。

内部计数器位于节点之中,随着节点读取完成自减。

#include <atomic>
#include <memory>template<typename T>
class lock_free_stack {
private:struct node;struct counted_node_ptr {int external_count;node* ptr;};struct node {std::shared_ptr<T> data;std::atomic<int> internal_count;counted_node_ptr next;node(T const& data_) : data(std::make_shared<T>(data_)), internal_count(0) {}};std::atomic<counted_node_ptr> head;void increase_head_count(counted_node_ptr& old_counter) {counted_node_ptr new_counter;do {new_counter = old_counter;++new_counter.external_count;} while (!head.compare_exchange_strong(old_counter, new_counter));old_counter.external_count = new_counter.external_count;}
public:std::shared_ptr<T> pop() {counted_node_ptr old_head = head.load();for (;;) {increase_head_count(old_head);node* const ptr = old_head.ptr;if (!ptr) {return std::shared_ptr<T>();}if (head.compare_exchange_strong(old_head, ptr->next)) {std::shared_ptr<T> res;res.swap(ptr->data);int const count_increase = old_head.external_count - 2;if (ptr->internal_count.fetch_add(count_increase) == -count_increase) {delete ptr;}return res;} else if (ptr->internal_count.fetch_sub(1) == 1) {delete ptr;}}}void push(T const& data) {counted_node_ptr new_node;new_node.ptr = new node(data);new_node.external_count = 1; // head指针本身算作一个外部引用new_node.ptr->next = head.load();while (!head.compare_exchange_weak(new_node.ptr->next, new_node));}~lock_free_stack() {while (pop());}
};

结构体counted_node_ptr的尺寸足够小,如果硬件平台支持双字 比较-交换 操作,那么std::atomic<counted_node_ptr>就属于无锁数据。若不支持,那么std::atomic<>涉及的结构体的尺寸过大,无法直接通过原子指令操作,便会采用互斥来保证操作原子化。使得“无锁”数据结构和算法成为基于锁的实现。

如果想要缩小结构体counted_node_ptr的尺寸,可以采取另一种方法替代:假定在硬件平台上,指针型别有空余的位。(例如,硬件寻址空间只有48位,指针型别的大小是64位),他们可以用来放置计数器,借此将counted_node_ptr结构体缩成单个机器字长。

使用分离引用计数的原因:我们通过外部引用计数的自增来保证,在访问目标节点的过程中,其指针依然安全有效。(先自增,再读取,被指涉后自增的值保护了节点不被删除)

具体流程详解见《cpp 并发实战》p236

以上实例使用的内存次序是std::memory_order_seq_cst。同步开销较大,下面对于内存次序进行优化。

2.5 为无锁栈容器施加内存模型

需要先确认各项操作哪些存在内存次序关系。

1 next指针只是一个未被原子化的普通对象,所以为了安全读取其值,存储操作必须再载入操作发生之前,前者由压入数据的线程执行,后者由弹出数据的线程执行。

#include <atomic>
#include <memory>template<typename T>
class lock_free_stack {
private:struct node;struct counted_node_ptr {int external_count;node* ptr;};struct node {std::shared_ptr<T> data;std::atomic<int> internal_count;counted_node_ptr next;node(T const& data_) : data(std::make_shared<T>(data_)), internal_count(0) {}};std::atomic<counted_node_ptr> head;void increase_head_count(counted_node_ptr& old_counter) {counted_node_ptr new_counter;do {new_counter = old_counter;++new_counter.external_count;} while (!head.compare_exchange_strong(old_counter, new_counter, std::memory_order_acquire, std::memory_order_relaxed));old_counter.external_count = new_counter.external_count;}
public:std::shared_ptr<T> pop() {counted_node_ptr old_head = head.load(std::memory_order_relaxed);for (;;) {increase_head_count(old_head);node* const ptr = old_head.ptr;if (!ptr) {return std::shared_ptr<T>();}if (head.compare_exchange_strong(old_head, ptr->next, std::memory_order_relaxed)) {std::shared_ptr<T> res;res.swap(ptr->data);int const count_increase = old_head.external_count - 2;if (ptr->internal_count.fetch_add(count_increase, std::memory_order_relaxed) == -count_increase) { // ?delete ptr;}return res;} else if (ptr->internal_count.fetch_add(-1, std::memory_order_relaxed) == 1) {ptr->internal_count.load(std::memory_order_acquire);delete ptr;}}}void push(T const& data) {counted_node_ptr new_node;new_node.ptr = new node(data);new_node.external_count = 1;new_node.ptr->next = head.load(std::memory_order_relaxed);while (!head.compare_exchange_weak(new_node.ptr->next, new_node, std::memory_order_release, std::memory_order_relaxed));}~lock_free_stack() {while (pop());}
};

这里的push,唯一的原子操作是compare_exchange_weak,如果需要在线程间构成先行关系,则代码需要一项释放操作,因此compare_exchange_weak必须采用std::memory_order_release或者更严格的内存次序。

若compare_exchange_weak执行失败,则指针head和new_node均无变化,代码继续执行,这种情况下使用memory_order_relaxed即可。

没懂,为什么是这两个次序

这里的pop,访问next指针前进行了额外操作。也就是先调用了increase_head_count(),该操作收到memory_order_acquire或者更严格的内存次序约束。因为这里通过原子操作获取的head指针旧值访问next指针。对原子操作失败的情况则使用宽松次序。

因为push中的存储行为是释放操作,pop,increase_head这里的是获取操作,因此存储行为和载入操作同步,构成先行关系。因此,对于push中的成员指针ptr的存储操作先行发生,然后pop才会在increase_head_count()中访问ptr->next,代码符合线程安全。

(push中的head.load不影响上述内存次序关系的分析)

剩余的很多没看懂,在《cpp并发实战》p240,有空多看看。

2.6 实现线程安全的无锁队列

对于队列,其pop和push分别访问不同的部分。

#include <atomic>
#include <memory>
#include <mutex>template<typename T>
class lock_free_queue {
private:struct node {std::shared_ptr<T> data;node* next;node() : next(nullptr) {}};std::atomic<node*> head;std::atomic<node*> tail;std::unique_ptr<node> pop_head() {node* const old_head = head.load();if (head.get() == tail) {return nullptr;}head.store(old_head->next);return old_head;}public:lock_free_queue() : head(new node), tail(head.load()) {}lock_free_queue(const lock_free_queue& other) = delete;lock_free_queue& operator=(const lock_free_queue& other) = delete;~lock_free_queue() {while (node* const old_head = head.load()) {head.store(old_head->next);delete old_head;}}std::shared_ptr<T> pop() {node* old_head = pop_head();if (!old_head) {return std::shared_ptr<T>();}std::shared_ptr<T> const res(old_head->data);delete old_head;return res;}void push(T new_value) {std::shared_ptr<T> new_data(std::make_shared<T>(std::move(new_value)));node* p = new node;node* const old_tail = tail.load();old_tail->data.swap(new_data);old_tail->next = p;tail.store(p);}
};

tail指针的存储操作store(push的最后一句)和载入操作load(pop_head的if里面的条件判断)存在同步。

但是若是由多个线程进行操作,上述代码就不可行。

其余细节具体实现见书。

3 实现无锁数据结构的原则

3.1 在原型设计中使用std::memory_order_seq_cst次序

它令全部操作形成一个确定的总序列,比较好分析。在这种意义上,使用其他内存次序就成为了一种优化。

3.2 使用无锁的内存回收方案

无所代码中的内存管理很难。最基本的要求是,只要目标对象仍然有可能背其他线程指涉,就不删除。

在这里介绍了三种方法来满足及时删除无用对象:

1 暂缓所有删除对象的动作,等到五线程访问再删除(类似gc)

2 风险指针

3 引用计数

3.3 防范ABA问题

在所有设计 比较-交换 的算法中,都要防范ABA问题。

该问题产生的过程如下:

步骤1:线程甲读取原子变量x,得知其值为A。

步骤2:线程甲根据A执行某项操作,比如查找,或如果x是指针,则依据它提取出相关值。

步骤3:线程甲因操作系统调度而发生阻塞。

步骤4:另一线程对原子变量x执行别的操作,将其值改成B。

步骤5:又有线程改变了与A相关的数据,使得线程甲原本持有的值失效(步骤2中的相关值)。这种情形也许是A表示某内存地址,而改动操作则是释放指针的目标内存,或变更目标数据,最后将产生严重后果。

步骤6:原子变量x再次被某线程改动,重新变回A。若x属于指针型别,其指向目标可能在步骤5被改换程一个新对象。

步骤7:线程甲继续运行,在原子变量x上执行 比较-交换 操作,与A进行对比。因此 比较-交换 操作成功执行(因为x的值仍然为A),但A的关联数据却不再有效,即原本在步骤2取的相关值已经失效,但是线程甲却无从分辨,这将破坏数据结构。

该问题最常见的解决方法之一是,在原子变量x中引入一个ABA计数器。将变量x和计数器组成单一结构,作为一个整体执行 比较-交换 操作。

3.4 找出忙等循环,协助其他线程

若两个线程同时执行push操作,那么必须等另一个结束,才可以继续运行。这是忙等,浪费cpu。在本例中将非原子变量的数据成员改为原子变量,并采用 比较-交换 操作设置其值。

4 小结

这节很难

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

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

相关文章

好用的翻译工具

最近看到个好用的翻译工具&#xff0c;叫沉浸式翻译 沉浸式翻译 - 双语对照网页翻译插件 | PDF翻译 | 视频字幕翻译 我下载的是谷歌插件 点击下载插件会跳转到使用文档&#xff0c;跟着一步步操作即可 翻译的效果&#xff0c;我这里用的是免费版的&#xff0c;如果需要加强&…

信息学奥赛一本通 ybt 1608:【 例 3】任务安排 3 | 洛谷 P5785 [SDOI2012] 任务安排

【题目链接】 ybt 1608&#xff1a;【 例 3】任务安排 3 洛谷 P5785 [SDOI2012] 任务安排 【题目考点】 1. 动态规划&#xff1a;斜率优化动规 2. 单调队列 3. 二分答案 【解题思路】 与本题题面相同但问题规模不同的题目&#xff1a; 信息学奥赛一本通 1607&#xff1a…

LabVIEW无线齿轮监测系统

本案例介绍了基于LabVIEW的无线齿轮监测系统设计。该系统利用LabVIEW编程语言和改进的天牛须算法优化支持向量机&#xff0c;实现了无线齿轮故障监测。通过LabVIEW软件和相关硬件&#xff0c;可以实现对齿轮箱振动信号的采集、传输和故障识别&#xff0c;集远程采集、数据库存储…

Doki Doki Mods Maker小指南

-*- 做都做了&#xff0c;那就做到底吧。 -*- 前言&#xff1a; 项目的话&#xff0c;在莫盘里&#xff0c;在贴吧原帖下我有发具体地址。 这里是Doki Doki Mods Maker&#xff0c;是用来做DDLC Mods的小工具。 说是“Mods”&#xff0c;实则不然&#xff0c;这个是我从零仿…

Node.js——body-parser、防盗链、路由模块化、express-generator应用生成器

个人简介 &#x1f440;个人主页&#xff1a; 前端杂货铺 &#x1f64b;‍♂️学习方向&#xff1a; 主攻前端方向&#xff0c;正逐渐往全干发展 &#x1f4c3;个人状态&#xff1a; 研发工程师&#xff0c;现效力于中国工业软件事业 &#x1f680;人生格言&#xff1a; 积跬步…

三、js笔记

(一)JavaScript概述 1、发展历史 ScriptEase.(客户端执行的语言):1992年Nombas开发出C-minus-minus(C--)的嵌入式脚本语言(最初绑定在CEnvi软件中).后将其改名ScriptEase.(客户端执行的语言)Javascript:Netscape(网景)接收Nombas的理念,(Brendan Eich)在其Netscape Navigat…

JavaScript作用域详解

前言 作用域是JavaScript中一个重要的概念&#xff0c;它决定了变量和函数在代码中的可访问性和可见性。了解JavaScript的作用域对于编写高效、可维护的代码至关重要。本文将深入介绍JavaScript作用域相关的知识点&#xff0c;其中包括作用域类型&#xff0c;作用域链&#xff…

如何使用SliverList组件

文章目录 1 概念介绍2 使用方法3 示例代码 我们在上一章回中介绍了沉浸式状态栏相关的内容&#xff0c;本章回中将介绍SliverList组件.闲话休提&#xff0c;让我们一起Talk Flutter吧。 1 概念介绍 我们在这里介绍的SliverList组件是一种列表类组件&#xff0c;类似我们之前介…

vsnprintf() 将可变参数格式化输出到字符数组

vsnprintf{} 将可变参数格式化输出到一个字符数组 1. function vsnprintf()1.1. const int num_bytes vsnprintf(NULL, 0, format, arg); 2. Parameters3. Return value4. Example5. llama.cppReferences 1. function vsnprintf() https://cplusplus.com/reference/cstdio/vs…

一文大白话讲清楚webpack基本使用——17——Tree Shaking

文章目录 一文大白话讲清楚webpack基本使用——17——Tree Shaking1. 建议按文章顺序从头看&#xff0c;一看到底&#xff0c;豁然开朗2. 啥叫Tree Shaking3. 什么是死代码&#xff0c;怎么来的3. Tree Shaking的流程3.1 标记3.2 利用Terser摇起来 4. 具体使用方式4.1 适用前提…

仿真设计|基于51单片机的温湿度、一氧化碳、甲醛检测报警系统

目录 具体实现功能 设计介绍 51单片机简介 资料内容 仿真实现&#xff08;protues8.7&#xff09; 程序&#xff08;Keil5&#xff09; 全部内容 资料获取 具体实现功能 &#xff08;1&#xff09;温湿度传感器、CO传感器、甲醛传感器实时检测温湿度值、CO值和甲醛值进…

几种K8s运维管理平台对比说明

目录 深入体验**结论**对比分析表格**1. 功能对比****2. 用户界面****3. 多租户支持****4. DevOps支持** 细对比分析1. **Kuboard**2. **xkube**3. **KubeSphere**4. **Dashboard****对比总结** 深入体验 KuboardxkubeKubeSphereDashboard 结论 如果您需要一个功能全面且适合…

GenAI 在金融服务领域的应用:2025 年的重点是什么

作者&#xff1a;来自 Elastic Karen Mcdermott GenAI 不是魔法 我最近参加了 ElasticON&#xff0c;我们与纽约 Elastic 社区一起度过了一天&#xff0c;讨论了使用检索增强生成 (retrieval augmented generation - RAG) 为大型语言模型 (large language models - LLMs) 提供…

如何对系统调用进行扩展?

扩展系统调用是操作系统开发中的一个重要任务。系统调用是用户程序与操作系统内核之间的接口,允许用户程序执行内核级操作(如文件操作、进程管理、内存管理等)。扩展系统调用通常包括以下几个步骤: 一、定义新系统调用 扩展系统调用首先需要定义新的系统调用的功能。系统…

LightM-UNet(2024 CVPR)

论文标题LightM-UNet: Mamba Assists in Lightweight UNet for Medical Image Segmentation论文作者Weibin Liao, Yinghao Zhu, Xinyuan Wang, Chengwei Pan, Yasha Wang and Liantao Ma发表日期2024年01月01日GB引用> Weibin Liao, Yinghao Zhu, Xinyuan Wang, et al. Ligh…

Cubemx文件系统挂载多设备

cubumx版本&#xff1a;6.13.0 芯片&#xff1a;STM32F407VET6 在上一篇文章中介绍了Cubemx的FATFS和SD卡的配置&#xff0c;由于SD卡使用的是SDIO通讯&#xff0c;因此具体驱动不需要自己实现&#xff0c;Cubemx中就可以直接配置然后生成SDIO的驱动&#xff0c;并将SD卡驱动和…

电子电气架构 --- 汽车电子拓扑架构的演进过程

我是穿拖鞋的汉子&#xff0c;魔都中坚持长期主义的汽车电子工程师。 老规矩&#xff0c;分享一段喜欢的文字&#xff0c;避免自己成为高知识低文化的工程师&#xff1a; 简单&#xff0c;单纯&#xff0c;喜欢独处&#xff0c;独来独往&#xff0c;不易合同频过着接地气的生活…

2025 年,链上固定收益领域迈向新时代

“基于期限的债券市场崛起与Secured Finance的坚定承诺” 2025年&#xff0c;传统资产——尤其是股票和债券——大规模涌入区块链的浪潮将创造历史。BlackRock 首席执行官 Larry Fink 近期在彭博直播中表示&#xff0c;代币化股票和债券将逐步融入链上生态&#xff0c;将进一步…

数据密码解锁之DeepSeek 和其他 AI 大模型对比的神秘面纱

本篇将揭露DeepSeek 和其他 AI 大模型差异所在。 目录 ​编辑 一本篇背景&#xff1a; 二性能对比&#xff1a; 2.1训练效率&#xff1a; 2.2推理速度&#xff1a; 三语言理解与生成能力对比&#xff1a; 3.1语言理解&#xff1a; 3.2语言生成&#xff1a; 四本篇小结…

Ollama部署指南

什么是Ollama&#xff1f; Ollama是一个专为在本地机器上便捷部署和运行大型语言模型&#xff08;LLM&#xff09;而设计的开源工具。 如何部署Ollama&#xff1f; 我是使用的云平台&#xff0c;大家也可以根据自己的云平台的特点进行适当的调整。 使用系统&#xff1a;ubun…