C++并发编程实战——07.设计无锁的并发数据结构

news2025/1/22 18:48:13

文章目录

  • 设计无锁的并发数据结构
    • 定义及意义
      • 无阻塞数据结构
      • 无锁数据结构
      • 无等待数据结构
      • 无锁结构的利弊
    • 无锁数据结构的例子
      • 无锁线程安全栈
      • 使用风险指针检测不可回收的节点
      • 使用引用计数
      • 无锁栈上的内存模型
      • 实现一个无锁的线程安全队列
    • 设计无锁数据结构的指导建议

设计无锁的并发数据结构

细粒度有锁并发结构可能会导致死锁问题,为了避免死锁问题。使用内存序来设计并发结构是另一种解决方案。

定义及意义

mutex, condition variable, future去同步的数据结构与算法被称为阻塞数据结构与算法。程序调用库函数使线程阻塞(block),OS将线程挂起,直至被另一线程解除阻塞(unblock)

当程序不使用这些库函数使线程阻塞和解除阻塞,称为无阻塞结构。但是,无阻塞结构并不是都是无锁的!!!

无阻塞数据结构

使用std::atomic_flag制作自旋锁

class spinlock_mutex{
 	std::atomic_flag flag;
public:
 	spinlock_mutex():
 	flag(ATOMIC_FLAG_INIT){  }
 	void lock(){ while(flag.test_and_set(std::memory_order_acquire)); }
 	void unlock(){ flag.clear(std::memory_order_release); }
};

自旋锁没调用阻塞函数,但是并不是无锁结构,仍然是个锁。

无锁结构需要更具体的定义:

  • 无阻碍(Obstruction-Free)——如果其他线程都暂停了,任何给定线程将在有限步骤内完成操作。
  • 无锁(Lock-Free)——如果多个线程对一个数据结构进行操作,其中一个线程将在有限步骤内完成其操作。
  • 无等待(Wait-Free)——即使有其他线程也在对该数据结构进行操作,每个线程都将在有限步骤内完成操作。

由于所有其他线程不是都暂停,通常无阻塞算法被视为失败的无锁结构。

无锁数据结构

无锁结构意味着多线程可以并发的访问数据结构做不同的操作,但不能做相同的操作。如果有线程被调度器挂起,其他线程无需等待仍然能够继续完成操作。

无锁数据结构的算法常在循环中使用**“比较/交换”操作。如果有其他线程完成了对数据的修改,那该线程需要在“比较/交换”操作之前重做部分操作。如果其他线程挂起而此线程的“比较/交换”操作成功执行,这段代码是无锁**的;反之,需要需要使用自旋锁,这段代码是“无阻塞-有锁”的。

无锁算法中的循环会让一些线程处于“饥饿”状态。如有线程在“错误”时间执行,那么第一个线程将会不停的尝试所要完成的操作(其他程序继续执行)。“无锁-无等待”数据结构的出现,就为了避免这种问题。

无等待数据结构

无等待数据结构是一个无锁数据结构,且每个线程的数据访问均在有限步骤内完成。

由于可能会和其他线程的行为冲突,算法会进行了若干次尝试,因此无法做到无等待。本章的大多数例子都有一种特性——对compare_exchange_weak或compare_exchange_strong操作进行循环,并且循环次数没有上限。操作系统对线程进行进行管理,有些线程的循环次数非常多,有些线程的循环次数就非常少。因此,这些操作是无等待的。

因此,真正实现无锁无等待结构十分困难,需要有足够的理由且收益>成本,再去实现。

无锁结构的利弊

使用无锁结构的主要原因:最大化并发;鲁棒性;没有任何锁(可能存在活锁)。另外,当不能限制访问数据结构的线程数量时,就需要注意不变量的状态,或选择替代品来保持不变量的状态。同时,还需要注意操作的顺序。为了避免未定义行为,及相关的数据竞争,必须使用原子操作对修改操作进行限制。不过,仅使用原子操作是不够的,需要确定其他线程看到的修改,是否遵循正确的顺序。

缺点:虽提高了并发访问的能力,减少了单个线程的等待时间,但是其可能会将整体性能拉低。原子操作的无锁代码要慢于无原子操作的代码,原子操作就相当于无锁数据结构中的锁。硬件必须通过同一个原子变量对线程间的数据进行同步。

提交代码之前,无论是基于锁的数据结构,还是无锁的数据结构,对性能的检查都很重要(最坏的等待时间,平均等待时间,整体执行时间或者其他指标)。

无锁数据结构的例子

无锁线程安全栈

这里使用链表作为栈的底层。

push节点步骤:

  1. 创建新节点node
  2. node->next指向head->next
  3. head->next指向node

问题:多线程时,步骤2和3存在条件竞争。

解决方案:步骤3使用循环+“比较/交换”操作替代。

pop节点步骤:

  1. 获取head
  2. 读取head->next指向的结点node
  3. 设置head->next指向node->next
  4. 通过node返回携带的数据data
  5. 删除node节点。

问题:

  • 线程a和b同时运行步骤1;下一时刻线程a运行步骤2而线程b运行步骤5,导致线程b解引用悬空指针。
  • 两个线程同时读取并返回同一个head值。
  • 空链表时,head->next将引起未定义行为。
  • 返回值的异常安全性。

解决方案:

  • 由于push()在存入栈后就不会访问节点,因此只要关注pop()的多线程访问问题。步骤5将在“待删除”链表(to_be_deleted)中添加要pop的元素,使用原子计数器记录pop()的次数(入口+1,出口-1),计数器为1时可安全释放元素;反之,在链表尾部再添加节点。
  • 操作2和3使用**“比较/交换”**操作更新head,失败时返回步骤1。
  • 步骤1确定head不是nullptr,返回异常或bool类型。
  • 返回数值使用智能指针。
template<typename T>
class lock_free_stack{
private:
 	struct node{
 		T data;
 		node* next;
 		node(T const& data_):data(std::make_shared<T>(data_)){}//智能指针分配
 	};
 	std::atomic<node*> head;
    std::atomic<unsigned> threads_in_pop; // 调用pop线程计数器
    std::atomic<node*> to_be_deleted; //待删除链表
    void try_reclaim(node* old_head); //调用时计数器-1
    static void delete_nodes(node* nodes);
    void chain_pending_nodes(node* nodes);
    void chain_pending_nodes(node* first,node* last);
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; // 在做事之前,计数器+1
 		node* old_head=head.load();
 		while(old_head != nullptr && // 在解引用前检查old_head是否为空指针
              !head.compare_exchange_weak(old_head,old_head->next));
        std::shared_ptr<T> res;
 		if(old_head) res.swap(old_head->data);//swap移动节点(而非只是拷贝指针)
        try_reclaim(old_head); // 删除数据,回收空间
        return res;
 	}
};
void lock_free_stack::try_reclaim(node* old_head){
    if(threads_in_pop==1){ // 计数器为1时
        /* 声明“可删除”链表(此时,可能又有线程调用pop(),threads_in_pop+1)
         * nodes_to_delete=to_be_deleted;to_be_deleted=nullptr;为一步操作 */
        node* nodes_to_delete=to_be_deleted.exchange(nullptr);
        if(!--threads_in_pop) // 当只有一个线程调用pop(),计数器-1
            delete_nodes(nodes_to_delete); // 删除“可删除”链表的节点
        else if(nodes_to_delete) // 不止一个线程调用pop()且nodes_to_delete非空
            chain_pending_nodes(nodes_to_delete); // 节点放回到“待删除”链表
        delete old_head; // 安全删除旧的头节点
    }
    else{ // 计数器不为1时
        chain_pending_node(old_head); // 向“可删除”链表添加节点
        --threads_in_pop; // 计数器-1
    }
}
void lock_free_stack::delete_nodes(node* nodes){
 	while(nodes){//删除链表每个节点
        node* next=nodes->next;
        delete nodes;
        nodes=next;
    }
}
void lock_free_stack::chain_pending_nodes(node* nodes){
    node* last=nodes;
    while(node* const next=last->next) last=next; // 让next指针指向链表的末尾
    chain_pending_nodes(nodes,last);
}
void lock_free_stack::chain_pending_nodes(node* first,node* last){
    //“可删除”链表(nodes_to_delete)末尾挂载“待删除”链表(to_be_deleted)的头指针
    last->next=to_be_deleted;
    // 循环保证last->next正确,正确时将to_be_deleted头节点更新为nodes_to_delete
    while(!to_be_deleted.compare_exchange_weak(last->next,first));
}
void chain_pending_node(node* n){
    chain_pending_nodes(n,n); 
}

线程C添加节点Y到to_be_deleted链表中,即使线程B此时仍将节点Y引用作为old_head,之后会尝试访问节点Y的next指针。如果线程A此时删除节点,将会造成线程B发生未定义行为。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

当高负荷访问栈时,该方案的链表将无限增加,会再次出现泄漏。

使用风险指针检测不可回收的节点

为了应对高负荷栈访问,将回收节点机制更换为**“风险指针”**检测机制。

当有线程去访问(其他线程)删除的对象时,会先对这个对象设置“风险指针”,而后通知其他线程。当线程想要删除一个对象,就必须检查系统中其他线程是否持有“风险指针”。当没有“风险指针”时,就可以安全删除对象。否则,就必须等待“风险指针”消失。

std::shared_ptr<T> pop(){
    std::atomic<void*>& hp=get_hazard_pointer_for_current_thread();
    node* old_head=head.load();
    do{
        node* temp;
        do{ // 循环直到将风险指针设为head指针
            temp=old_head;
            hp.store(old_head); // 避免设置风险指针时被删除
            old_head=head.load(); // 读取最新的head指针
        }while(old_head!=temp);
    }
    while(old_head && // 用compare_exchange_strong避免重复设置“风险指针”
          !head.compare_exchange_strong(old_head,old_head->next));
    hp.store(nullptr); // 当声明完成,清除风险指针
    std::shared_ptr<T> res;
    if(old_head){
        res.swap(old_head->data);
        // 在删除之前对风险指针引用的节点进行检查,是否有其他节点引用
        if(outstanding_hazard_pointers_for(old_head))
            reclaim_later(old_head); // 放回链表,之后再删除
        else 
            delete old_head; // 直接删除
        delete_nodes_with_no_hazards();//如链表无风险指针引用节点,可安全删除节点
    }
    return res;
}

get_hazard_pointer_for_current_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 pointers available");
    }
    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(); // 返回风险指针
}

outstanding_hazard_pointer_for()的实现(查找风险指针):

bool outstanding_hazard_pointers_for(void* p){
 	for(unsigned i=0;i<max_hazard_pointers;++i){
        if(hazard_pointers[i].pointer.load()==p) return true;
 	}
 	return false; 
}

reclaim_later()添加节点和delete_nodes_with_no_hazards()删除节点的实现:

template<typename T>
void do_delete(void* p){ delete static_cast<T*>(p); }

struct data_to_reclaim{
 	void* data;
 	std::function<void(void*)> deleter; // 产生函数指针
 	data_to_reclaim* next;
 	template<typename T>
 	data_to_reclaim(T* p) : data(p), deleter(&do_delete<T>), next(0){}
 	~data_to_reclaim(){ deleter(data); } //调用do_delete<T>(data)
};
std::atomic<data_to_reclaim*> nodes_to_reclaim;

void add_to_reclaim_list(data_to_reclaim* node){ // 循环链表头
 	node->next=nodes_to_reclaim.load();
 	while(!nodes_to_reclaim.compare_exchange_weak(node->next,node));
}

template<typename T> // 函数模板,使用std::atomic<void*>存储风险指针
void reclaim_later(T* data){
 	add_to_reclaim_list(new data_to_reclaim(data)); // 创建实例放入回收链表
}

void delete_nodes_with_no_hazards(){
 	data_to_reclaim* current=nodes_to_reclaim.exchange(nullptr); // 回收节点
 	while(current){
 		data_to_reclaim* const next=current->next;
 		if(!outstanding_hazard_pointers_for(current->data)) // 检测风险指针
            delete current; 
        else
            add_to_reclaim_list(current); // 放回链表后面
        current=next;
    }
}

使用引用计数

之前使用两种方法实现无锁线程安全栈:一种使用引用计数,另一种使用危险指针。但在实际中很难管理。std::shared_ptr<>内部有引用计数,一些操作是原子操作,但不保证是无锁的。大量上下文使用,使开销大。如果平台支持无锁的std::shared_ptr<>,那么所有内存回收问题就都迎刃而解了。

使用std::experimental::atomic_shared_ptr<>std::atomic< std::shared_ptr<T> >两者等价)的实现:

#include <experimental/atomic>
using namespace std;
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=head.load();
        while(!head.compare_exchange_weak(new_node->next,new_node));
    }
    std::shared_ptr<T> pop(){
        std::shared_ptr<node> old_head=head.load();
        while(old_head && !head.atomic_compare_exchange_weak(
                  old_head,old_head->next.load()));
        if (old_head){
            old_head->next=std::shared_ptr<node>();
            return old_head->data;
        }
        return std::shared_ptr<T>();
    }
    ~lock_free_stack(){
        while(pop());
    }
};

如果不使用std::experimental::atomic_shared_ptr<>,就要手动管理引用计数。可使用分离引用计数的方式。读取节点时外部计数+1,读取结束时内部计数-1,内部计数为0时删除:

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:
    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();
        while(!head.compare_exchange_weak(new_node.ptr->next,new_node));
    }
    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);
                //external_count-2:链表删除节点-1;不从当前线程访问节点-1。
                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)//相减之后为0
                delete ptr;
        }
    }
    ~lock_free_stack(){ while(pop()); }
};

目前,使用默认std::memory_order_seq_cst内存序来规定原子操作的执行顺序。可改变内存序提高效率。

无锁栈上的内存模型

根据上一节的无锁线程安全栈,观察各个原子/非原子操作间的依赖关系,再选择最佳内存序。因此,需在不同场景不同线程的角度来观察内存序,比如入栈后出栈,需要三个重要数据片段:

  1. counted_node_ptr转移的数据head
  2. head引用的节点node
  3. node->data

push()的线程,会先构造数据项,并设置head

pop()的线程,会先加载head,再循环“比较/交换”操作且引用计数+1,读取node->next

在此可得非原子对象next,为了保证读取安全,必须确定push()线程和pop()线程之间的先行关系(happen-before)。因为push()函数中的原子操作只有compare_exchange_weak(),所以由此可确定两个线程间的先行关系,调用成功必须是std::memory_order_release或更严格的内存序。调用失败时可以持续循环下去,所以使用std::memory_order_relaxed就够了:

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));
}

因为pop()函数会使用两个compare_exchange_strong(还有load()fetch_add()),后一个会读取ptr->next

为了让保存先行于读取,前一个compare_exchange_strong(old_counter,new_counter)如调用成功必须是std::memory_order_acquire或更严格的内存序操作。如调用失败时可以持续循环,所以使用std::memory_order_relaxed就够了:

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;
}

head.load()的初始化并不妨碍分析,可以使用std::memory_order_relaxed

因为pop()函数会再使用一个head.compare_exchange_strong(old_head,ptr->next)。需确保push()对于ptr->data的保存先行于这里的读取(前面一个原子操作已确保,即使后一个的compare_exchange_strong()操作使用std::memory_order_relaxed)。唯一需注意的是res.swap(ptr->data),且无其他线程可以对同一节点进行操作(“比较/交换”操作的作用)。当compare_exchange_strong()失败时, 使用std::memory_order_relaxed就够了,因为前一个原子操作已经使用了std::memory_order_acquire

由于有acquire和release的先行与同步保证,其余线程无需设置更严格的内存序。

fetch_add()会改变引用计数。但是任何未成功检索数据的线程都知道另一个线程使用swap()提取数据。为避免数据竞争,要确保swap()先行于delete。最简单的方法:操作成功时内存序为std::memory_order_acquire;失败时循环内存序为std::memory_order_release。但是delete只会使用一次,在delete前使用load(std::memory_order_acquire)确保先行关系即可。

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:
    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));
    }
    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);
                
                //external_count-2:链表删除节点-1;不从当前线程访问节点-1。
                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){//相减之后为0
                ptr->internal_count.load(std::memory_order_acquire);
                delete ptr;
            }
        }
    }
    ~lock_free_stack(){ while(pop()); }
};

实现一个无锁的线程安全队列

设计无锁数据结构的指导建议

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

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

相关文章

windwos10搭建我的世界服务器,并通过内网穿透实现联机游戏Minecraft

文章目录 1. Java环境搭建2.安装我的世界Minecraft服务3. 启动我的世界服务4.局域网测试连接我的世界服务器5. 安装cpolar内网穿透6. 创建隧道映射内网端口7. 测试公网远程联机8. 配置固定TCP端口地址8.1 保留一个固定tcp地址8.2 配置固定tcp地址 9. 使用固定公网地址远程联机 …

mysql双主搭建

https://www.bilibili.com/video/BV1BK4y1t7MY/?spm_id_from333.880.my_history.page.click&vd_source297c866c71fa77b161812ad631ea2c25 要到用双主&#xff0c;或多主&#xff0c;主要是考虑到这么一个场景&#xff1a; 如果一个应用&#xff0c;全球用户都要用&#x…

API接口安全设计

简介 HTTP接口是互联网各系统之间对接的重要方式之一&#xff0c;使用HTTP接口开发和调用都很方便&#xff0c;也是被大量采用的方式&#xff0c;它可以让不同系统之间实现数据的交换和共享。 由于HTTP接口开放在互联网上&#xff0c;所以我们就需要有一定的安全措施来保证接口…

LuaHttp库写的一个简单的爬虫

LuaHttp库是一个基于Lua语言的HTTP客户端库&#xff0c;可以用于爬取网站数据。与Python的Scrapy框架类似&#xff0c;LuaHttp库也可以实现网站数据的抓取&#xff0c;并且可以将抓取到的数据保存到数据库中。不过需要注意的是&#xff0c;LuaHttp库并不像Scrapy框架那样具有完…

限制LitstBox控件显示指定行数的最新数据(3/3)

实例需求&#xff1a;由于数据行数累加增加&#xff0c;控件加载的数据越来越多&#xff0c;每次用户都需要使用右侧滚动条拖动才能查看最新数据。 因此希望ListBox只加载最后10行数据&#xff08;不含标题行&#xff09;&#xff0c;这样用户可以非常方便地选择数据&#xff…

基于python+django开发的电影链接搜索网站 - 毕业设计 - 课程设计

文章目录 源码下载地址项目介绍界面预览项目备注毕设定制&#xff0c;咨询 源码下载地址 点击这里下载代码 项目介绍 该项目是基于python的web类库django开发的一套web网站&#xff0c;给同学做的课程作业。 本人的研究方向是一项关于搜索的研究项目。在该项目中&#xff0c…

WoShop跨境电商源码:解放你的双手,批量发货轻松搞定

随着跨境电商的快速发展&#xff0c;越来越多的企业开始涉足这一领域。在这个过程中&#xff0c;如何高效地处理批量发货成为了亟待解决的问题。本文将探讨跨境电商源码支持批量发货的优势、需求分析、实现方案、技术实现、测试与维护以及总结与建议。 一、引言 在跨境电商领域…

Linux进程概念(2)

Linux进程概念(2) &#x1f4df;作者主页&#xff1a;慢热的陕西人 &#x1f334;专栏链接&#xff1a;Linux &#x1f4e3;欢迎各位大佬&#x1f44d;点赞&#x1f525;关注&#x1f693;收藏&#xff0c;&#x1f349;留言 本博客主要内容讲解了进程的概念&#xff0c;PCB&am…

Python用RoboBrowser库写一个通用爬虫模版

以下是一个使下载lianjia内容的Python程序&#xff0c;爬虫IP服务器为duoip的8000端口。 from robobrowser import RoboBrowser# 创建一个RoboBrowser对象 browser RoboBrowser(user_agentMozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) …

人工智能与无人驾驶:未来驾驶体验的革命性变革

人工智能与无人驾驶&#xff1a;未来驾驶体验的革命性变革 人工智能&#xff08;AI&#xff09;和无人驾驶技术的迅速发展正在改变我们的交通方式和出行体验。它们结合了先进的感知技术、智能算法和高性能计算能力&#xff0c;为实现自动驾驶提供了可能性。本文将探讨人工智能和…

悟道云端,探索测试新境

“探寻新技术的前沿&#xff0c;分享测试经验的心得”&#xff0c;这是我参加云栖大会的初衷。回顾第一次参加云栖大会的情景&#xff0c;仿佛还历历在目。那是2017年&#xff0c;我刚刚步入职场&#xff0c;对云计算领域充满了好奇和憧憬。云栖大会给了我一个难得的机会&#…

JS利用时间戳倒计时案例

我们在逛某宝&#xff0c;或者逛某东时&#xff0c;我们时常看到一个倒计时&#xff0c;时间一到就开抢&#xff0c;这个倒计时是如何做的呢&#xff1f;让我为大家介绍一下。 理性分析一下&#xff1a; 1.用将来时间减去现在时间就是剩余的时间 2.核心&#xff1a;使用将来的时…

基于 golang 从零到一实现时间轮算法 (一)

前言 时间轮是用来解决海量百万级定时器&#xff08;或延时&#xff09;任务的最佳方案&#xff0c;linux 的内核定时器就是采用该数据结构实现。 应用场景 自动删除缓存中过期的 Key&#xff1a;缓存中设置了 TTL 的 kv&#xff0c;通过把该 key 对应的 TTL 以及回调方法注册…

云安全-云原生k8s攻击点(8080,6443,10250未授权攻击点)

0x00 k8s简介 k8s&#xff08;Kubernetes&#xff09; 是容器管理平台&#xff0c;用来管理容器化的应用&#xff0c;提供快速的容器调度、弹性伸缩等诸多功能&#xff0c;可以理解为容器云&#xff0c;不涉及到业务层面的开发。只要你的应用可以实现容器化&#xff0c;就可以部…

css——半圆实心

案例 代码 <view class"circleBox"></view>.circleBox {width: 50px;height: 100px;background: red;border-radius: 100px 0 0 100px; }

史上最详细注释,用flask写一个博客系统

文本用flask写个博客系统&#xff0c;源码带有详细注释&#xff0c;通俗易懂&#xff0c;拿去就能用。博客效果如下&#xff0c;博客首页&#xff1a; 这个博客麻雀虽小&#xff0c;但五脏俱全。有如下功能&#xff1a; 博客文章浏览用户注册用户登录/登出发文章/修改文章/删除…

Linux设置ssh免密登录

ssh连接其他服务器 基本语法 ssh 另一台机器的ip地址 连接后输入连接主机用户的密码&#xff0c;即可成功连接。 输入exit 可以登出&#xff1b; 由于我配置了主机映射所以可以不写ip直接写映射的主机名即可&#xff0c;Linux配置主机映射的操作为 vim /etc/hosts # 我自己…

Linux上编译sqlite3库出现undefined reference to `sqlite3_column_table_name‘

作者&#xff1a;朱金灿 来源&#xff1a;clever101的专栏 为什么大多数人学不会人工智能编程&#xff1f;>>> 在Ubuntu 18上编译sqlite3库后在运行程序时出现undefined reference to sqlite3_column_table_name’的错误。网上的说法是说缺少SQLITE_ENABLE_COLUMN_M…

【使用Python编写游戏辅助工具】第四篇:Windows窗口操作

前言 这里是【使用Python编写游戏辅助工具】的第四篇&#xff1a;Windows窗口操作。本文主要介绍使用Python来实现Windows窗口的各种操作。 Windows窗口操作是游戏辅助功能中不可或缺的一部分。 Windows窗口操作指的是与Windows操作系统中的窗口进行交互和控制的操作&#xff…

Docker安装ElasticSearch7.8.0

Docker安装ElasticSearch7.8.0 1&#xff1a;docker可能会拉取不了es&#xff0c;此时可以配置一个很好用的镜像源&#xff08;daocloud&#xff09;&#xff0c;下载非常快&#xff1a; curl -sSL https://get.daocloud.io/daotools/set_mirror.sh | sh -s http://f1361db2.…