当前位置: 首页 > news >正文

C++并发:设计基于锁的并发数据结构

(上一篇主要讲了底层逻辑,比较晦涩难懂,这一章讲解数据结构设计)

对于并发场景,数据保护的形式主要如下:

1 采用独立互斥和外部锁

2 专门为并发访问自行设计的数据结构

1 并发设计的内涵

互斥保护数据结构的方式是明令阻止真正的并发访问。以上行为称为串行化:每个线程轮流访问受互斥保护的数据,他们只能先后串行依次访问,而非并发访问。

因此在设计数据结构时必须深思熟虑,力求实现真正的并发访问。

相对而言,某些数据结构更有潜质支持真正的并发访问,但万变不离其宗:保护的范围越小,需要的串行化操作就越少,并发程度就越高。

1.1 设计并发数据结构的指引

在设计支持并发访问的数据结构时,需要考虑两点:

1 确保访问安全,通常,构造函数和析构函数都要按排他的方式执行。在构造前和析构后不会有访问发生。赋值,内部数据互换,拷贝构造等操作与其他操作一起并发调用是否安全,以及这些函数是否要求使用者保证以排他的方式访问。

2 实现真正的并发访问,可以考虑一下问题:

能否限制锁的作用域,从而让操作的某些部分在锁的保护意外执行

数据结构内部的不同部分能否采用不同的互斥

是否所有操作都需要相同程度的保护

能否通过简单地改动,提高数据结构的并发程度,为并发操作增加机会,而不影响操作语义。

这些问题全部都归结为一个核心问题:如何才能只保留最必要的串行操作,最大限度地实现并发。

2 基于锁的并发数据结构(注意使用多个互斥容易死锁)

2.1 采用锁实现线程安全的栈容器

#include <exception>
#include <memory>
#include <mutex>
#include <stack>struct empty_stack: std::exception {const char* what() const throw();
};template<typename T>
class threadsafe_stack {
private:std::stack<T> data;mutable std::mutex m;
public:threadsafe_stack() {}threadsafe_stack(const threadsafe_stack& other) {std::lock_guard<std::mutex> lock(other.m);data = other.data;}threadsafe_stack& operator=(const threadsafe_stack&) = delete;void push(T new_value) {std::lock_guard<std::mutex> lock(m);data.push(std::move(new_value));}std::shared_ptr<T> pop() {std::lock_guard<std::mutex> lock(m);if (data.empty()) {throw empty_stack();}std::shared_ptr<T> const res(std::make_shared<T>(std::move(data.top())));data.pop();return res;}void pop(T& value) {std::lock_guard<std::mutex> lock(m);if (data.empty()) {throw empty_stack();}value = std::move(data.top());data.pop();}bool empty() const {std::lock_guard<std::mutex> lock(m);return data.empty();}
};

2.2 采用锁和条件变量实现线程安全的队列容器

template<typename T>
class threadsafe_queue {
private:mutable std::mutex mut;std::queue<T> data_queue;std::condition_variable data_cond;public:threadsafe_queue() {}void push(T new_value) {std::lock_guard<std::mutex> lk(mut);data_queue.push(std::move(new_value));data_cond.notify_one();}void wait_and_pop(T& value) {std::unique_lock<std::mutex> lk(mut);data_cond.wait(lk, [this]{return !data_queue.empty();});value = std::move(data_queue.front());data_queue.pop();}std::shared_ptr<T> wait_and_pop() {std::unique_lock<std::mutex> lk(mut);data_cond.wait(lk, [this]{return !data_queue.empty();});std::shared_ptr<T> res(std::make_shared<T>(std::move(data_queue.front())));data_queue.pop();return res;}bool try_pop(T& value) {std::lock_guard<std::mutex> lk(mut);if (data_queue.empty()) {return false;}value = std::move(data_queue.front());data_queue.pop();return true;}std::shared_ptr<T> try_pop() {std::lock_guard<std::mutex> lk(mut);if (data_queue.empty()) {return std::shared_ptr<T>();}std::shared_ptr<T> res(std::make_shared<T>(std::move(data_queue.front())));data_queue.pop();return res;}bool empty() const {std::lock_guard<std::mutex> lk(mut);return data_queue.empty();}
};

与2.1的不同之处在于对于notify_one()的使用。支持了等待-通知功能。弹出的功能不再需要连续地对empty进行判断,只需要使用条件变量进行wait,并且等待期间由于互斥已经加锁,因此数据仍然受到保护(使得pop和empty等操作不会有数据竞争)。

2.2.1 使用std::shared_ptr避免唤醒线程时抛出异常导致无线程唤醒

为了避免之前的代码在wait_and_pop时抛出异常(比如新指针shared_ptr在构建时抛出了异常),导致不会有任何线程被唤醒,因此这里选择了一个处理方法:使用std::shared_ptr<>,将其初始化语句移到push()的调用处,令队列容器改为存储std::shared_ptr<>,而不再直接存储数据的值。这样从内部std::queue<>复制std::shared_ptr实例的操作不会抛出异常,所以此时wait_and_pop是异常安全的。

#include <condition_variable>
#include <mutex>
#include <queue>template<typename T>
class threadsafe_queue {
private:mutable std::mutex mut;std::queue<std::shared_ptr<T>> data_queue;std::condition_variable data_cond;public:threadsafe_queue() {}void wait_and_pop(T& value) {std::unique_lock<std::mutex> lk(mut);data_cond.wait(lk, [this]{return !data_queue.empty();});value = std::move(*data_queue.front());data_queue.pop();}bool try_pop(T& value) {std::lock_guard<std::mutex> lk(mut);if (data_queue.empty()) {return false;}value = std::move(*data_queue.front());data_queue.pop();return true;}std::shared_ptr<T> wait_and_pop() {std::unique_lock<std::mutex> lk(mut);data_cond.wait(lk, [this]{return !data_queue.empty();});std::shared_ptr<T> res = data_queue.front();data_queue.pop();return res;}std::shared_ptr<T> try_pop() {std::lock_guard<std::mutex> lk(mut);if (data_queue.empty()) {return std::shared_ptr<T>();}std::shared_ptr<T> res = data_queue.front();data_queue.pop();return res;}void push(T new_value) {std::shared_ptr<T> data(std::make_shared<T>(std::move(new_value)));std::lock_guard<std::mutex> lk(mut);data_queue.push(data);data_cond.notify_one();}bool empty() const {std::lock_guard<std::mutex> lk(mut);return data_queue.empty();}
};

将创建shared_ptr需要的内存分配的步骤从锁保护的范围移出,还有一个好处就是,内存分配是一个开销很大的操作,将他放在所保护的范围之外,减少了锁持有的时长。

缺点是,都是由唯一的互斥保护整个数据结构,它所支持的并发程度因此受限。事实上首先的原因是每次只允许一个线程操作队列数据。我们对于std::queue这一个整体施加了保护

2.3 采用精细粒度的锁和条件变量实现线程安全的队列容器

在队列和栈的实现代码中,我们以queue为最小内部结构进行了数据保护,但是这样的粒度其实是较大的,在这里我们使用单向链表来实现队列,减小粒度。

#include <memory>template<typename T>
class queue {
private:struct node {std::shared_ptr<T> data;std::unique_ptr<node> next;};std::unique_ptr<node> head;node* tail;public:queue() : head(new node), tail(head.get()) {}queue(const queue& other) = delete;queue& operator=(const queue& other) = delete;std::shared_ptr<T> try_pop() {if (head.get()==tail) {return std::shared_ptr<T>();}std::shared_ptr<T> const res(head->data);std::unique_ptr<node> const old_head = std::move(head);head = std::move(old_head->next);return res;}void push(T new_value) {std::shared_ptr<T> new_data(std::make_shared<T>(std::move(new_value)));std::unique_ptr<node> p(new node);tail->data = new_data;node* const new_tail = p.get();tail->next = std::move(p);tail = new_tail;}
};

head是头节点,不保有业务数据,保证了在push的时候不会出现head和tail指向同一业务节点造成的数据冲突。也就是使用了虚位节点(head对应的节点),导致head和tail不再同时指向相同的节点(pop处理head,push处理tail),因此为head和tail各用一个互斥提供了可能。

下面提供了精细粒度的线程安全队列:

#include <memory>
#include <mutex>template<typename T>
class threadsafe_queue {
private:struct node {std::shared_ptr<T> data;std::unique_ptr<node> next;};std::mutex head_mutex;std::unique_ptr<node> head;std::mutex tail_mutex;node* tail;node* get_tail() {std::lock_guard<std::mutex> tail_lock(tail_mutex);return tail;}std::unique_ptr<node> pop_head() {std::lock_guard<std::mutex> head_lock(head_mutex);if (head.get() == get_tail()) {return nullptr;}std::unique_ptr<node> old_head = std::move(head);head = std::move(old_head->next);return old_head;}public:threadsafe_queue() : head(new node), tail(head.get()) {}threadsafe_queue(const threadsafe_queue& other) = delete;threadsafe_queue& operator=(const threadsafe_queue& other) = delete;std::shared_ptr<T> try_pop() {std::unique_ptr<node> old_head = pop_head();return old_head ? old_head->data : std::shared_ptr<T>();}void push(T new_value) {std::shared_ptr<T> new_data(std::make_shared<T>(std::move(new_value)));std::unique_ptr<node> p(new node);node* const new_tail = p.get();std::lock_guard<std::mutex> tail_lock(tail_mutex);tail->data = new_data;tail->next = std::move(p);tail = new_tail;}
};

虽然保证了head和tail不会指向相同的节点,但是对于push和try_pop操作,它们仍然能对相同的节点进行操作。因此这里的实现在try_pop上也给tail互斥加上了锁,来保证不同线程执行push和try_pop操作,能够有确定的先后顺序。

并且tail的互斥锁在head互斥之内,保证了不会有死锁的发生,以及tail和head的指向均为正确的尾节点,头节点。

这样实现的方法将很多内存分配步骤移到了锁外,让上锁的范围更精细。

2.3.1 等待数据弹出

#include <condition_variable>
#include <memory>
#include <mutex>template<typename T>
class threadsafe_queue {
private:struct node {std::shared_ptr<T> data;std::unique_ptr<node> next;};std::mutex head_mutex;std::unique_ptr<node> head;std::mutex tail_mutex;node* tail;std::condition_variable data_cond;node* get_tail() {std::lock_guard<std::mutex> tail_lock(tail_mutex);return tail;}std::unique_ptr<node> pop_head() {std::unique_ptr<node> old_head = std::move(head);head = std::move(old_head->next);return old_head;}std::unique_lock<std::mutex> wait_for_data() {std::unique_lock<std::mutex> head_lock(head_mutex);data_cond.wait(head_lock, [&]{return head.get() != get_tail();});return std::move(head_lock);}std::unique_ptr<node> wait_pop_head() {std::unique_lock<std::mutex> head_lock(wait_for_data());return pop_head();}std::unique_ptr<node> wait_pop_head(T& value) {std::unique_lock<std::mutex> head_lock(wait_for_data());value = std::move(*head->data);return pop_head();}std::unique_ptr<node> try_pop_head() {std::lock_guard<std::mutex> head_lock(head_mutex);if (head.get() == get_tail()) {return std::unique_ptr<node>();}return pop_head();}std::unique_ptr<node> try_pop_head(T& value) {std::lock_guard<std::mutex> head_lock(head_mutex);if (head.get() == get_tail()) {return std::unique_ptr<node>();}value = std::move(*head->data);return pop_head();}public:std::shared_ptr<T> wait_and_pop() {std::unique_ptr<node> const old_head = wait_pop_head();return old_head->data;}void wait_and_pop(T& value) {std::unique_ptr<node> const old_head = wait_pop_head(value);}std::shared_ptr<T> try_pop() {std::unique_ptr<node> old_head = try_pop_head();return old_head ? old_head->data : std::shared_ptr<T>();}bool try_pop(T& value) {std::unique_ptr<node> const old_head = try_pop_head(value);return old_head != nullptr;}bool empty() {std::lock_guard<std::mutex> head_lock(head_mutex);return head.get() == get_tail();}void push(T new_value) {std::shared_ptr<T> new_data(std::make_shared<T>(std::move(new_value)));std::unique_ptr<node> p(new node);{node* const new_tail = p.get();std::lock_guard<std::mutex> tail_lock(tail_mutex);tail->data = new_data;tail->next = std::move(p);tail = new_tail;}data_cond.notify_one();}
};

这里加上了基于条件变量实现的等待和通知,避免了外部循环调用等待互斥。

3 设计更复杂的基于锁的并发数据结构

查找表(lookup table),又称字典,也是cpp里面的各种map,从并发的角度来讲,std::map<>接口中最大问题是迭代器。有一种思路是通过迭代器访问容器内部,即便有其他线程访问并改动容器,迭代器所提供的访问依然安全。

但是如果这个时候一个线程要删除某个元素,而它却正被迭代器引用,就容易触发问题。所以本章先将迭代器从线程安全的map接口中剔除。

3.1 采用锁编写线程安全的查找表

#include <bitset>
#include <list>
#include <shared_mutex>
#include <vector>template<typename Key, typename Value, typename Hash=std::hash<Key>>
class threadsafe_lookup_table {
private:class bucket_type {private:typedef std::pair<Key, Value> bucket_value;typedef std::list<bucket_value> bucket_iterator;bucket_data data;mutable std::shared_mutex mutex;bucket_iterator find_entry_for(Key const& key) const {return std::find_if(data.begin(), data.end(), [&](bucket_value const& item){return item.first == key;});}public:Value value_for(Key const& key, Value const& default_value) const {std::shared_lock<std::shared_mutex> lock(mutex);bucket_iterator const found_entry = find_entry_for(key);return (found_entry == data.end()) ? default_value : found_entry->second;}void add_or_update_mapping(Key const& key, Value const& value) {std::unique_lock<std::shared_mutex> lock(mutex);bucket_iterator const found_entry = find_entry_for(key);if (found_entry == data.end()) {data.push_back(bucket_value(key, value));} else {found_entry->second = value;}}void remove_mapping(Key const& key) {std::unique_lock<std::shared_mutex> lock(mutex);bucket_iterator const found_entry = find_entry_for(key);if (found_entry != data.end()) {data.erase(found_entry);}}};std::vector<std::unique_ptr<bucket_type>> buckets;Hash hasher;bucket_type& get_bucket(Key const& key) const {std::size_t const bucket_index = hasher(key) % buckets.size();return *buckets[bucket_index];}
public:typedef Key key_type;typedef Value mapped_type;typedef Hash hash_type;threadsafe_lookup_table(unsigned num_buckets=19, Hash const& hasher_ = Hash()): buckets(num_buckets), hasher(hasher_) {for (unsigned i = 0; i < num_buckets; ++i) {buckets[i].reset(new bucket_type);}}threadsafe_lookup_table(threadsafe_lookup_table const& other)=delete;threadsafe_lookup_table operator=(threadsafe_lookup_table const& other)=delete;Value value_for(Key const& key, Value const& default_value = Value()) const {return get_bucket(key).value_for(key, default_value);}void add_or_update_mapping(Key const& key, Value const& value) {get_bucket(key).add_or_update_mapping(key, value);}void remove_mapping(Key const& key) {get_bucket(key).remove_mapping(key);}
};

使用散列表,以桶为单位加锁。

#include <bitset>
#include <list>
#include <map>
#include <shared_mutex>
#include <vector>template<typename Key, typename Value, typename Hash=std::hash<Key>>
class threadsafe_lookup_table {
private:class bucket_type {private:typedef std::pair<Key, Value> bucket_value;typedef std::list<bucket_value> bucket_iterator;bucket_data data;mutable std::shared_mutex mutex;bucket_iterator find_entry_for(Key const& key) const {return std::find_if(data.begin(), data.end(), [&](bucket_value const& item){return item.first == key;});}public:Value value_for(Key const& key, Value const& default_value) const {std::shared_lock<std::shared_mutex> lock(mutex);bucket_iterator const found_entry = find_entry_for(key);return (found_entry == data.end()) ? default_value : found_entry->second;}void add_or_update_mapping(Key const& key, Value const& value) {std::unique_lock<std::shared_mutex> lock(mutex);bucket_iterator const found_entry = find_entry_for(key);if (found_entry == data.end()) {data.push_back(bucket_value(key, value));} else {found_entry->second = value;}}void remove_mapping(Key const& key) {std::unique_lock<std::shared_mutex> lock(mutex);bucket_iterator const found_entry = find_entry_for(key);if (found_entry != data.end()) {data.erase(found_entry);}}};std::vector<std::unique_ptr<bucket_type>> buckets;Hash hasher;bucket_type& get_bucket(Key const& key) const {std::size_t const bucket_index = hasher(key) % buckets.size();return *buckets[bucket_index];}
public:typedef Key key_type;typedef Value mapped_type;typedef Hash hash_type;threadsafe_lookup_table(unsigned num_buckets=19, Hash const& hasher_ = Hash()): buckets(num_buckets), hasher(hasher_) {for (unsigned i = 0; i < num_buckets; ++i) {buckets[i].reset(new bucket_type);}}threadsafe_lookup_table(threadsafe_lookup_table const& other)=delete;threadsafe_lookup_table operator=(threadsafe_lookup_table const& other)=delete;Value value_for(Key const& key, Value const& default_value = Value()) const {return get_bucket(key).value_for(key, default_value);}void add_or_update_mapping(Key const& key, Value const& value) {get_bucket(key).add_or_update_mapping(key, value);}void remove_mapping(Key const& key) {get_bucket(key).remove_mapping(key);}std::map<Key, Value> get_map() const {std::vector<std::unique_lock<std::shared_mutex>> locks;for (unsigned i = 0; i < buckets.size(); ++i) {locks.push_back(std::unique_lock<std::shared_mutex>(buckets[i]->mutex));}std::map<Key, Value> res;for (unsigned i = 0; i < buckets.size(); ++i) {for (bucket_iterator it = buckets[i]->data.begin(); it != buckets[i]->data.end();++it) {res.insert(*it);}}return res;}
};

添加数据快照功能,这是一项锁住全部桶的操作,只要每次都按照相同的顺序对每个桶加锁,就没有机会发生死锁(这里指两次快照功能加锁顺序一致)。

3.2 采用多种锁编写线程安全的链表

链表若要具备精细粒度的锁操作,基本思想是让每个节点都具备自己的互斥。

#include <memory>
#include <mutex>template<typename T>
class threadsafe_list {struct node {std::mutex m;std::shared_ptr<T> data;std::unique_ptr<node> next;node() : next() {}node(T const& value) : data(std::make_shared<T>(value)) {}};node head;
public:threadsafe_list() {}~threadsafe_list() {std::remove_if([](node const&) {return true;});}threadsafe_list(threadsafe_list const& other) = delete;threadsafe_list& operator=(threadsafe_list const& other) = delete;void push_front(T const& value) {std::unique_ptr<node> new_node(new node(value));std::lock_guard<std::mutex> lk(head.m);new_node->next = std::move(new_node);}template<typename Function>void for_each(Function f) {node* current = &head;std::unique_lock<std::mutex> lk(head.m);while (node* const next = current->next.get()) {std::unique_lock<std::mutex> next_lk(next->m);lk.unlock();f(*next->data);current=next;lk = std::move(next_lk);}}template<typename Predicate>std::shared_ptr<T> find_first_if(Predicate p) {node* current = &head;std::unique_lock<std::mutex> lk(head.m);while (node* const next = current->next.get()) {std::unique_lock<std::mutex> next_lk(next->m);lk.unlock();if (p(*next->data)) {return next->data;}current=next;lk = std::move(next_lk);}return std::shared_ptr<T>();}template<typename Predicate>void remove_if(Predicate p) {node* current = &head;std::unique_lock<std::mutex> lk(head.m);while (node* const next = current->next.get()) {std::unique_lock<std::mutex> next_lk(next->m);if (p(*next->data)) {std::unique_ptr<node> old_next = std::move(current->next);current->next = std::move(next->next);next_lk.unlock();} else {lk.unlock();current=next;lk = std::move(next_lk);}}}
};

添加Predicate,Function,对应传入的函数对象,以T为入参,处理和断言。比较好理解。

相关文章:

C++并发:设计基于锁的并发数据结构

&#xff08;上一篇主要讲了底层逻辑&#xff0c;比较晦涩难懂&#xff0c;这一章讲解数据结构设计&#xff09; 对于并发场景&#xff0c;数据保护的形式主要如下&#xff1a; 1 采用独立互斥和外部锁 2 专门为并发访问自行设计的数据结构 1 并发设计的内涵 互斥保护数据结…...

数据分析系列--⑥RapidMiner构建决策树(泰坦尼克号案例含数据)

一、资源下载 二、数据处理 1.导入数据 2.数据预处理 三、构建模型 1.构建决策树 2.划分训练集和测试集 3.应用模型 4.结果分析 一、资源下载 点击下载数据集 二、数据处理 1.导入数据 2.数据预处理 三、构建模型 1.构建决策树 虽然决策树已经构建,但对于大多数初学者或…...

leetcode 844 比较含退格的字符串

leetcode 844 比较含退格的字符串 题目描述 给定 s 和 t 两个字符串&#xff0c;当它们分别被输入到空白的文本编辑器后&#xff0c;如果两者相等&#xff0c;返回 true 。# 代表退格字符。 注意&#xff1a;如果对空文本输入退格字符&#xff0c;文本继续为空。 示例 1&#…...

【ArcGIS遇上Python】批量提取多波段影像至单个波段

本案例基于ArcGIS python,将landsat影像的7个波段影像数据,批量提取至单个波段。 相关阅读:【ArcGIS微课1000例】0141:提取多波段影像中的单个波段 文章目录 一、数据准备二、效果比对二、python批处理1. 编写python代码2. 运行代码一、数据准备 实验数据及完整的python位…...

10.6 LangChain提示工程终极指南:从基础模板到动态生成的工业级实践

LangChain提示工程终极指南:从基础模板到动态生成的工业级实践 关键词: LangChain提示模板、动态Prompt生成、少样本学习、结构化输出、Prompt工程 一、Prompt Engineering 的本质:用设计模式重构大模型交互 传统Prompt的局限性: 硬编码文本:修改需求需重构代码缺乏结构…...

DeepSeek-R1 论文解读 —— 强化学习大语言模型新时代来临?

近年来&#xff0c;人工智能&#xff08;AI&#xff09;领域发展迅猛&#xff0c;大语言模型&#xff08;LLMs&#xff09;为通用人工智能&#xff08;AGI&#xff09;的发展开辟了道路。OpenAI 的 o1 模型表现非凡&#xff0c;它引入的创新性推理时缩放技术显著提升了推理能力…...

联想拯救者R720笔记本外接显示屏方法,显示屏是2K屏27英寸

晚上23点10分前下单&#xff0c;第二天上午显示屏送到&#xff0c;检查外包装没拆封过。这个屏幕左下方有几个按键&#xff0c;按一按就开屏幕、按一按就关闭屏幕&#xff0c;按一按方便节省时间&#xff0c;也支持阅读等模式。 显示屏是 &#xff1a;AOC 27英寸 2K高清 100Hz…...

编译安装PaddleClas@openKylin(失败,安装好后报错缺scikit-learn)

编译安装 前置需求&#xff1a; 手工安装swig和faiss-cpu pip install swig pip install faiss-cpu 小技巧&#xff0c;pip编译安装的时候&#xff0c;可以加上--jobs64来多核编译。 注意先升级pip版本&#xff1a;pip install pip -U pip3 install faiss-cpu --config-s…...

传输层协议TCP与UDP:深入解析与对比

传输层协议TCP与UDP&#xff1a;深入解析与对比 目录 传输层协议TCP与UDP&#xff1a;深入解析与对比引言1. 传输层协议概述2. TCP协议详解2.1 TCP的特点2.2 TCP的三次握手与四次挥手三次握手四次挥手 2.3 TCP的流量控制与拥塞控制2.4 TCP的可靠性机制 3. UDP协议详解3.1 UDP的…...

《解码AI大模型涌现能力:从量变到质变的智能跃迁》

在当今科技飞速发展的时代&#xff0c;人工智能大模型的涌现能力成为了众人瞩目的焦点。从ChatGPT与用户的流畅对话&#xff0c;到GPT-4在复杂任务中的出色表现&#xff0c;这些大模型仿佛一夜之间解锁了超乎想象的技能&#xff0c;那么&#xff0c;这种神奇的涌现能力究竟是如…...

【数据结构】_C语言实现带头双向循环链表

目录 1. 单链表与双链表 1.1 单链表的结构及结点属性 1.2 双链表的结构及结点属性 2. 单链表与双链表的区别 3. 双链表的实现 3.1 List.h 3.2 List.c 3.3 Test_List.c 注&#xff1a;部分方法的实现细节注释 1. 双链表结点前驱、后继指针域的初始化 2. 各种增、删结…...

优盘恢复原始容量工具

买到一个优盘&#xff0c;显示32mb&#xff0c;我见过扩容盘&#xff0c;但是这次见到的是缩容盘&#xff0c;把2g的容量缩成32MB了&#xff0c;首次见到。。用芯片查询工具显示如下 ChipsBank(芯邦) CBM2199E 使用以下工具&#xff0c;恢复原始容量。。 其他CMB工具可能不行…...

JVM的GC详解

获取GC日志方式大抵有两种 第一种就是设定JVM参数在程序启动时查看&#xff0c;具体的命令参数为: -XX:PrintGCDetails # 打印GC日志 -XX:PrintGCTimeStamps # 打印每一次触发GC时发生的时间第二种则是在服务器上监控:使用jstat查看,如下所示&#xff0c;命令格式为jstat -gc…...

反转单向链表以及单链表添加节点、遍历单链表

反转1个单向链表 /*** 节点类*/ class ListNode {public int val;public ListNode next;public ListNode(int val) {this.val val;}Overridepublic String toString() {return "ListNode{" "val" val ", next" next };} }借助一个pre来存储每…...

ZZNUOJ(C/C++)基础练习1021——1030(详解版)

目录 1021 : 三数求大值 C语言版 C版 代码逻辑解释 1022 : 三整数排序 C语言版 C版 代码逻辑解释 补充 &#xff08;C语言版&#xff0c;三目运算&#xff09;C类似 代码逻辑解释 1023 : 大小写转换 C语言版 C版 1024 : 计算字母序号 C语言版 C版 代码逻辑总结…...

Linux学习笔记——系统维护命令

一、进程管理 1、ps命令&#xff08;查&#xff09; 来自process缩写&#xff0c;显示当前的进程状态。包括&#xff1a;进程的号码&#xff0c;发起者&#xff0c;系统资源&#xff0c;使用占比&#xff0c;运行状态等等。 语法格式&#xff1a;ps 参数 实例&#x…...

Harbor 部署

harbor镜像仓库搭建 版本v2.10.3 文章目录 一. docker 安装 harbor1. harbor 配置http访问1.1 下载harbor二进制包1.2 修改配置文件1.3 运行1.4 访问 2.【可选】harbor 配置https访问2.1 自签证书2.1 修改配置文件2.3 修改hosts文件2.4 运行2.5 访问 二. k8s 安装harbor1 .安装…...

three.js+WebGL踩坑经验合集(6.1):负缩放,负定矩阵和行列式的关系(2D版本)

春节忙完一轮&#xff0c;总算可以继续来写博客了。希望在春节假期结束之前能多更新几篇。 这一篇会偏理论多一点。笔者本没打算在这一系列里面重点讲理论&#xff0c;所以像相机矩阵推导这种网上已经很多优质文章的内容&#xff0c;笔者就一笔带过。 然而关于负缩放&#xf…...

开源的瓷砖式图像板系统Pinry

简介 什么是 Pinry &#xff1f; Pinry 是一个开源的瓷砖式图像板系统&#xff0c;旨在帮助用户轻松保存、标记和分享图像、视频和网页。它提供了一种便于快速浏览的格式&#xff0c;适合喜欢整理和分享多种媒体内容的人。 主要特点 图像抓取和在线预览&#xff1a;支持从网页…...

LabVIEW透镜多参数自动检测系统

在现代制造业中&#xff0c;提升产品质量检测的自动化水平是提高生产效率和准确性的关键。本文介绍了一个基于LabVIEW的透镜多参数自动检测系统&#xff0c;该系统能够在单一工位上完成透镜的多项质量参数检测&#xff0c;并实现透镜的自动搬运与分选&#xff0c;极大地提升了检…...

socket实现HTTP请求,参考HttpURLConnection源码解析

背景 有台服务器&#xff0c;网卡绑定有2个ip地址&#xff0c;分别为&#xff1a; A&#xff1a;192.168.111.201 B&#xff1a;192.168.111.202 在这台服务器请求目标地址 C&#xff1a;192.168.111.203 时必须使用B作为源地址才能访问目标地址C&#xff0c;在这台服务器默认…...

反向代理模块jmh

1 概念 1.1 反向代理概念 反向代理是指以代理服务器来接收客户端的请求&#xff0c;然后将请求转发给内部网络上的服务器&#xff0c;将从服务器上得到的结果返回给客户端&#xff0c;此时代理服务器对外表现为一个反向代理服务器。 对于客户端来说&#xff0c;反向代理就相当…...

安卓(android)实现注册界面【Android移动开发基础案例教程(第2版)黑马程序员】

一、实验目的&#xff08;如果代码有错漏&#xff0c;可查看源码&#xff09; 1.掌握LinearLayout、RelativeLayout、FrameLayout等布局的综合使用。 2.掌握ImageView、TextView、EditText、CheckBox、Button、RadioGroup、RadioButton、ListView、RecyclerView等控件在项目中的…...

RubyFPV开源代码之系统简介

RubyFPV开源代码之系统简介 1. 源由2. 工程架构3. 特性介绍&#xff08;软件&#xff09;3.1 特性亮点3.2 数字优势3.3 使用功能 4. DEMO推荐&#xff08;硬件&#xff09;4.1 天空端4.2 地面端4.3 按键硬件Raspberry PiRadxa 3W/E/C 5. 软件设计6. 参考资料 1. 源由 RubyFPV以…...

解锁维特比算法:探寻复杂系统的最优解密码

引言 在复杂的技术世界中&#xff0c;维特比算法以其独特的魅力和广泛的应用&#xff0c;成为通信、自然语言处理、生物信息学等领域的关键技术。今天&#xff0c;让我们一同深入探索维特比算法的奥秘。 一、维特比算法的诞生背景 维特比算法由安德鲁・维特比在 1967 年提出…...

Unity游戏(Assault空对地打击)开发(2) 基础场景布置

目录 导入插件 文件夹整理 场景布置 山地场景 导入插件 打开【My Assets】&#xff08;如果你刚进行上篇的操作&#xff0c;该窗口默认已经打开了&#xff09;。 找到添加的几个插件&#xff0c;点击Download并Import x.x to...。 文件夹整理 我们的目录下多了两个文件夹&a…...

Office / WPS 公式、Mathtype 公式输入花体字、空心字

注&#xff1a;引文主要看注意事项。 1、Office / WPS 公式中字体转换 花体字 字体选择 “Eulid Math One” 空心字 字体选择 “Eulid Math Two” 使用空心字时&#xff0c;一般不用斜体&#xff0c;取消勾选 “斜体”。 2、Mathtype 公式输入花体字、空心字 2.1 直接输…...

代码随想录算法训练营第三十九天-动态规划-213. 打家劫舍 II

与上一题基本一样&#xff0c;只不过房间形成一个环&#xff0c;就需要在首尾考虑状况多一些这不是多一些状况的问题&#xff0c;是完全不知道如何选择的问题这种状况详细分析一下就是要分成三种情况 第一种&#xff1a;不考虑首元素&#xff0c;也不考虑尾元素&#xff0c;只考…...

自然语言处理-词嵌入 (Word Embeddings)

词嵌入&#xff08;Word Embedding&#xff09;是一种将单词或短语映射到高维向量空间的技术&#xff0c;使其能够以数学方式表示单词之间的关系。词嵌入能够捕捉语义信息&#xff0c;使得相似的词在向量空间中具有相近的表示。 &#x1f4cc; 常见词嵌入方法 基于矩阵分解的方…...

Redis 数据备份与恢复

Redis 数据备份与恢复 引言 Redis 是一款高性能的键值对存储系统,广泛应用于缓存、消息队列、分布式锁等领域。为了保证数据的安全性和可靠性,定期对 Redis 数据进行备份与恢复是至关重要的。本文将详细介绍 Redis 数据备份与恢复的方法,帮助您更好地管理和维护 Redis 数据…...

【leetcode】T541 (两点反思)

解题反思 闷着头往&#xff0c;往往会写成一团浆糊&#xff0c;还推倒重来&#xff0c;谋划好全局思路再开始很重要。 熟悉C的工具库很重要&#xff0c;一开始看到反转就还想着用stack来着&#xff0c;后面突然想起来用reverse函数刚好可以用哇&#xff0c;这题也就迎刃而解了…...

《STL基础之vector、list、deque》

【vector、list、deque导读】vector、list、deque这三种序列式的容器&#xff0c;算是比较的基础容器&#xff0c;也是大家在日常开发中常用到的容器&#xff0c;因为底层用到的数据结构比较简单&#xff0c;笔者就将他们三者放到一起做下对比分析&#xff0c;介绍下基本用法&a…...

嵌入式系统|DMA和SPI

文章目录 DMA&#xff08;直接内存访问&#xff09;DMA底层原理1. 关键组件2. 工作机制3. DMA传输模式 SPI&#xff08;串行外设接口&#xff09;SPI的基本原理SPI连接示例 DMA与SPI的共同作用 DMA&#xff08;直接内存访问&#xff09; 类型&#xff1a;DMA是一种数据传输接口…...

LevelDB 源码阅读:写入键值的工程实现和优化细节

读、写键值是 KV 数据库中最重要的两个操作&#xff0c;LevelDB 中提供了一个 Put 接口&#xff0c;用于写入键值对。使用方法很简单&#xff1a; leveldb::Status status leveldb::DB::Open(options, "./db", &db); status db->Put(leveldb::WriteOptions…...

寒假刷题Day18

一、16. 最接近的三数之和 这一题有负数&#xff0c;没有单调性&#xff0c;不能“大了右指针左移&#xff0c;小了左指针右移&#xff0c;最后存值域求差绝对值”。 class Solution { public:int threeSumClosest(vector<int>& nums, int target) {ranges::sort(n…...

力扣219.存在重复元素每日一题(大年初一)

以一道简单题开启全新的一年 哈希表&#xff1a;我们可以使用 哈希表 来存储数组元素及其对应的索引。通过遍历数组&#xff0c;我们可以检查当前元素是否已经存在于哈希表中&#xff0c;并判断索引差是否满足条件。 具体步骤如下&#xff1a; 创建一个哈希表 map&#xff0c…...

Midjourney中的强变化、弱变化、局部重绘的本质区别以及其有多逆天的功能

开篇 Midjourney中有3个图片“微调”&#xff0c;它们分别为&#xff1a; 强变化&#xff1b;弱变化&#xff1b;局部重绘&#xff1b; 在Discord里分别都是用命令唤出的&#xff0c;但如今随着AI技术的发达在类似AI可人一类的纯图形化界面中&#xff0c;我们发觉这样的逆天…...

Blazor-选择循环语句

今天我们来说说Blazor选择语句和循环语句。 下面我们以一个简单的例子来讲解相关的语法&#xff0c;我已经创建好了一个Student类&#xff0c;以此类来进行语法的运用 因为我们需要交互性所以我们将类创建在*.client目录下 if 我们做一个学生信息的显示&#xff0c;Gender为…...

根据每月流量和市场份额排名前20 的AI工具列表

ChatGPT&#xff1a;由Open AI研发&#xff0c;是一款对话式大型语言模型。它能够理解自然语言输入&#xff0c;生成连贯且符合逻辑的回复。可用于文本创作&#xff0c;如撰写文章、故事、诗歌&#xff1b;还能解答各种领域的知识问题&#xff0c;提供翻译、代码解释等服务&…...

关于安卓greendao打包时报错问题修复

背景 项目在使用greendao的时候&#xff0c;debug安装没有问题&#xff0c;一到打包签名就报了。 环境 win10 jdk17 gradle8 项目依赖情况 博主的greendao是一个独立的module项目&#xff0c;项目目前只适配了java&#xff0c;不支持Kotlin。然后被外部集成。greendao版本…...

前端面试笔试题目(一)

以下模拟了大厂前端面试流程&#xff0c;并给出了涵盖HTML、CSS、JavaScript等基础和进阶知识的前端笔试题目&#xff0c;以帮助你更好地准备面试。 面试流程模拟 1. 自我介绍&#xff08;5 - 10分钟&#xff09;&#xff1a;面试官会请你进行简单的自我介绍&#xff0c;包括…...

网络工程师 (10)设备管理

前言 设备管理中的数据传输控制方式是确保设备与内存&#xff08;或CPU&#xff09;之间高效、准确地进行数据传送的关键。 一、程序直接控制方式 1.工作原理&#xff1a; 由CPU发出I/O指令&#xff0c;直接控制数据的传输过程。CPU需要不断查询外设的状态&#xff0c;以确定数…...

如何让一个用户具备创建审批流程的权限

最近碰到一个问题&#xff0c;两个sandbox&#xff0c;照理用户的权限应该是一样的&#xff0c;结果开发环境里面我可以左右的做各种管理工作&#xff0c;但是使用change set上传后&#xff0c;另一个环境的同一个用户&#xff0c;没有相对于的权限&#xff0c;权限不足。 当时…...

unity学习23:场景scene相关,场景信息,场景跳转

目录 1 默认场景和Assets里的场景 1.1 scene的作用 1.2 scene作为project的入口 1.3 默认场景 2 场景scene相关 2.1 创建scene 2.2 切换场景 2.3 build中的场景&#xff0c;在构建中包含的场景 &#xff08;否则会认为是失效的Scene&#xff09; 2.4 Scenes in Bui…...

【Java高并发】基于任务类型创建不同的线程池

文章目录 一. 按照任务类型对线程池进行分类1. IO密集型任务的线程数2. CPU密集型任务的线程数3. 混合型任务的线程数 二. 线程数越多越好吗三. Redis 单线程的高效性 使用线程池的好处主要有以下三点&#xff1a; 降低资源消耗&#xff1a;线程是稀缺资源&#xff0c;如果无限…...

全网首发,MacMiniA1347安装飞牛最新系统0.8.36,改造双盘位NAS,超详细.36,改造双盘位nas,超详细

全网首发&#xff0c;MacMiniA1347安装飞牛最新系统0.8.36&#xff0c;改造双盘位NAS&#xff0c;超详细 小伙伴们大家好呀&#xff0c;勤奋的凯尔森同学又双叒叕来啦&#xff0c;今天这一期也是有点特别&#xff0c;我们把MacMiniA1347安装飞牛最新系统0.8.36&#xff0c;并且…...

简要介绍C++中的 max 和 min 函数以及返回值

简要介绍C中的 max 和 min 函数 在C中&#xff0c;std::max 和 std::min 是标准库 <algorithm> 中提供的函数&#xff0c;用于比较两个或多个值并返回最大值或最小值。这些函数非常强大且灵活&#xff0c;支持多种数据类型&#xff08;如整数、浮点数、字符串等&#xff…...

【基于SprintBoot+Mybatis+Mysql】电脑商城项目之用户注册

&#x1f9f8;安清h&#xff1a;个人主页 &#x1f3a5;个人专栏&#xff1a;【计算机网络】【Mybatis篇】 &#x1f6a6;作者简介&#xff1a;一个有趣爱睡觉的intp&#xff0c;期待和更多人分享自己所学知识的真诚大学生。 目录 &#x1f3af;项目基本介绍 &#x1f6a6;项…...

记忆化搜索(5题)

是什么&#xff1f; 是一个带备忘录的递归 如何实现记忆化搜索 1.添加一个备忘录&#xff08;建立一个可变参数和返回值的映射关系&#xff09; 2.递归每次返回的时候把结果放到备忘录里 3.在每次进入递归的时候往备忘录里面看看。 目录 1.斐波那契数列 2.不同路径 3.最…...

强化学习笔记——4策略迭代、值迭代、TD算法

基于策略迭代的贝尔曼方程和基于值迭代的贝尔曼方程&#xff0c;关系还是不太理解 首先梳理一下&#xff1a; 通过贝尔曼方程将强化学习转化为值迭代和策略迭代两种问题 求解上述两种贝尔曼方程有三种方法&#xff1a;DP&#xff08;有模型&#xff09;&#xff0c;MC&#xff…...