C++ 并发编程实战 第七章 设计无锁数据结构

news2024/11/23 22:18:51

目录

7.1 定义和推论

7.1.1 非阻塞型数据结构

7.1.2 无锁数据结构

7.1.3 无需等待的数据结构

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

7.2 无锁数据结构范例

7.2.1 实现线程安全的无锁栈

7.2.2 制止麻烦的内存泄漏:在无锁数据结构中管理内存

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

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

7.2.5 为无锁容器施加内存模型

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

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

7.3.1 原则1:在原型设计中使用std::memory_order_seq_cst次序

7.3.2 原则2:使用无锁的内存回收方案

7.3.3 原则3: 防范ABA问题

7.3.4 原则4:找出忙等循环,协助其他线程


参考:https://github.com/xiaoweiChen/CPP-Concurrency-In-Action-2ed-2019/blob/master/content/chapter7/7.2-chinese.md

7.1 定义和推论

使用互斥量、条件变量,以及future可以用来同步算法和数据结构。调用库函数将会挂起执行线程,直到其他线程完成某个特定动作。库函数将调用阻塞操作来对线程进行阻塞,在阻塞解除前线程无法继续自己的任务。通常,操作系统会完全挂起一个阻塞线程(并将其时间片交给其他线程),直到解阻塞。“解阻塞”的方式很多,比如互斥锁解锁、通知条件变量达成,或让“future状态”就绪。

7.1.1 非阻塞型数据结构

 无锁结构的具体定义,这将有助于你判断哪些类型的数据结构是无锁的。这些类型有:

  • 无阻碍——如果其他线程都暂停了,任何给定的线程都将在一定时间内完成操作。
  • 无锁——如果多个线程对一个数据结构进行操作,经过一定时间后,其中一个线程将完成其操作。
  • 无等待——即使有其他线程也在对该数据结构进行操作,每个线程都将在一定的时间内完成操作。

大多数情况下无阻塞算法用的很少——其他线程都暂停的情况太少见了,因此这种方式用于描述一个失败的无锁实现更为合适。

7.1.2 无锁数据结构

无锁结构意味着线程可以并发的访问数据结构,线程不能做相同的操作。一个无锁队列可能允许一个线程压入数据,另一个线程弹出数据,当有两个线程同时添加元素时,将破坏这个数据结构。不仅如此,当调度器中途挂起其中一个访问线程时,其他线程必须能够继续完成自己的工作,而无需等待挂起线程。

具有“比较/交换”操作的数据结构,通常有一个循环。使用“比较/交换”操作的原因:当有其他线程同时对指定的数据进行修改时,代码将尝试恢复数据。当其他线程挂起时,“比较/交换”操作执行成功,这样的代码就是无锁的。当执行失败时,就需要一个自旋锁,且这个结构就是“无阻塞-有锁”的结构。

7.1.3 无需等待的数据结构

无等待数据结构:首先是无锁数据结构,并且每个线程都能在有限的时间内完成操作,暂且不管其他线程是如何工作的。由于可能会和其他线程的行为冲突,从而算法会进行了若干次尝试,因此无法做到无等待。本章的大多数例子都有一种特性——对compare_exchange_weak或compare_exchange_strong操作进行循环,并且循环次数没有上限。操作系统对线程进行进行管理,有些线程的循环次数非常多,有些线程的循环次数就非常少。因此,这些操作不是无等待的。

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

7.2 无锁数据结构范例

无锁结构依赖于原子操作和内存序,以确保多线程以正确的顺序访问数据结构,原子操作默认使用memory_order_seq_cst内存序。不过,后面的例子中会降低内存序的要求。虽然例子中没有直接使用锁,但需要注意std::atomic_flag。一些平台上无锁结构的实现(实际上在C++标准库中实现)使用了内部锁。另一些平台上,基于锁的简单数据结构可能会更加合适,还有很多平台的实现细节不明确。选择一种实现前,需要明确需求,并且配置各种选项以满足需求。

7.2.1 实现线程安全的无锁栈

代码7.2 不用锁实现push()

template<typename T>
class lock_free_stack
{
private:
  struct node
  {
    T data;
    node* next;

    node(T const& data_):  // 1
     data(data_)
    {}
  };

  std::atomic<node*> head;
public:
  void push(T const& data)
  {
    node* const new_node=new node(data); // 2
    new_node->next=head.load();  // 3
    while(!head.compare_exchange_weak(new_node->next,new_node));  // 4
  }
};

上面代码能匹配之前的三个步骤:创建一个新节点②,设置新节点的next指针指向当前head③,并设置head指针指向新节点④。node结构用其自身的构造函数来进行数据填充①,必须保证节点在构造完成后能随时弹出。之后需要使用compare_exchange_weak()来保证在被存储到new_node->next的head指针和之前的一样③。代码的亮点是使用“比较/交换”操作:返回false时,因为比较失败(例如,head被其他线程锁修改),会使用head中的内容更新new_node->next(第一个参数)的内容。因为编译器会重新加载head指针,所以循环中不需要每次都重新加载head指针。同样,因为循环可能直接就失败了,所以使用compare_exchange_weak要好于使用compare_exchange_strong(详见第5章)。

7.2.2 制止麻烦的内存泄漏:在无锁数据结构中管理内存

代码7.5 使用引用计数的回收机制

template<typename T>
class lock_free_stack
{
private:
  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)  // 1
    {
      node* nodes_to_delete=to_be_deleted.exchange(nullptr);  // 2 声明“可删除”列表
      if(!--threads_in_pop)  // 3 是否只有一个线程调用pop()?
      {
        delete_nodes(nodes_to_delete);  // 4
      }
      else if(nodes_to_delete)  // 5
      {
         chain_pending_nodes(nodes_to_delete);  // 6
      }
      delete old_head;  // 7
    }
    else
    {
      chain_pending_node(old_head);  // 8
      --threads_in_pop;
    }
  }
  void chain_pending_nodes(node* nodes)
  {
    node* last=nodes;
    while(node* const next=last->next)  // 9 让next指针指向链表的末尾
    {
      last=next;
    }
    chain_pending_nodes(nodes,last);
  }

  void chain_pending_nodes(node* first,node* last)
  {
    last->next=to_be_deleted;  // 10
    while(!to_be_deleted.compare_exchange_weak(  // 11 用循环来保证last->next的正确性
      last->next,first));
    }
    void chain_pending_node(node* n)
    {
      chain_pending_nodes(n,n);  // 12
    }
};

回收节点时①,threads_in_pop是1,当前线程对pop()进行访问时,就可以安全的将节点删除⑦(将等待节点删除也是安全的)。当数值不是1时,删除任何节点都不安全,所以需要向等待列表中继续添加节点⑧。

假设某一时刻,threads_in_pop值为1。就可以尝试回收等待列表,如果不回收,节点就会继续等待,直到整个栈被销毁。要做到回收,首先要通过原子exchange操作声明②删除列表,并将计数器减1③。如果之后计数的值为0,意味着没有其他线程访问等待节点链表。不必为出现新的等待节点而烦恼,因为它们会安全的回收。而后,可以使用delete_nodes对链表进行迭代,并将其删除④。

计数值在减后不为0时,回收节点就不安全。如果存在⑤,就需要将其挂在等待删除链表后⑥,这种情况会发生在多个线程同时访问数据结构的时候。一些线程在第一次测试threads_in_pop①和对“回收”链表的声明②操作间调用pop(),这可能会将已经访问的节点新填入到链表中。图7.1中,线程C添加节点Y到to_be_deleted链表中,即使线程B仍将其引用作为old_head,之后会尝试访问其next指针。线程A删除节点时,会造成线程B发生未定义行为。

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

 “风险指针”这个术语引用于Maged Michael的研究[1],之所以这样叫是因为删除一个节点可能会让其他引用线程处于危险状态。其他线程持有已删除节点的指针对其进行解引用操作时,会出现未定义行为。基本观点就是,当有线程去访问(其他线程)删除的对象时,会先对这个对象设置风险指针,而后通知其他线程——使用这个指针是危险的行为。当这个对象不再需要,就可以清除风险指针。

std::shared_ptr<T> pop()
{
  std::atomic<void*>& hp=get_hazard_pointer_for_current_thread();
  node* old_head=head.load();  // 1
  node* temp;
  do
  {
    temp=old_head;
    hp.store(old_head);  // 2
    old_head=head.load();
  } while(old_head!=temp); // 3
  // ...
}

while循环能保证node不会在读取旧head指针①时,以及设置风险指针②时被删除。这种模式下,其他线程不知道有线程对这个节点进行了访问。幸运的是,旧head节点要删除时,head本身会发生变化,所以需要对head进行检查并持续循环,直到head指针中的值与风险指针中的值相同③。使用默认的new和delete操作对风险指针进行操作时,会出现未定义行为,所以需要确定实现是否支持这样的操作,或使用自定义内存分配器来保证用法的正确性

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

代码7.12 使用分离引用计数从无锁栈中弹出一个节点

template<typename T>
class lock_free_stack
{
private:
  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));  // 1

    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;  // 2
      if(!ptr)
      {
        return std::shared_ptr<T>();
      }
      if(head.compare_exchange_strong(old_head,ptr->next))  // 3
      {
        std::shared_ptr<T> res;
        res.swap(ptr->data);  // 4

        int const count_increase=old_head.external_count-2;  // 5

        if(ptr->internal_count.fetch_add(count_increase)==  // 6
           -count_increase)
        {
          delete ptr;
        }

        return res;  // 7
      }
      else if(ptr->internal_count.fetch_sub(1)==1)
      {
        delete ptr;  // 8
      }
    }
  }
};

7.2.5 为无锁容器施加内存模型

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

代码7.16 使用带有引用计数tail,实现的无锁队列中的push()

template<typename T>
class lock_free_queue
{
private:
  struct node;
  struct counted_node_ptr
  {
    int external_count;
    node* ptr;
  };

  std::atomic<counted_node_ptr> head;
  std::atomic<counted_node_ptr> tail;  // 1

  struct node_counter
  {
    unsigned internal_count:30;
    unsigned external_counters:2;  // 2
  };

  struct node
  {
    std::atomic<T*> data;
    std::atomic<node_counter> count;  // 3
    counted_node_ptr next;

    node()
    {
      node_counter new_count;
      new_count.internal_count=0;
      new_count.external_counters=2;  // 4
      count.store(new_count);

      next.ptr=nullptr;
      next.external_count=0;
     }
  };
public:
  void push(T new_value)
  {
    std::unique_ptr<T> new_data(new T(new_value));
    counted_node_ptr new_next;
    new_next.ptr=new node;
    new_next.external_count=1;
    counted_node_ptr old_tail=tail.load();

    for(;;)
    {
      increase_external_count(tail,old_tail);  // 5

      T* old_data=nullptr;
      if(old_tail.ptr->data.compare_exchange_strong(  // 6
           old_data,new_data.get()))
      {
        old_tail.ptr->next=new_next;
        old_tail=tail.exchange(new_next);
        free_external_counter(old_tail);  // 7
        new_data.release();
        break;
      }
      old_tail.ptr->release_ref();
    }
  }
};

代码7.17 使用尾部引用计数,将节点从无锁队列中弹出

template<typename T>
class lock_free_queue
{
private:
  struct node
  {
    void release_ref();
  };
public:
  std::unique_ptr<T> pop()
  {
    counted_node_ptr old_head=head.load(std::memory_order_relaxed);  // 1
    for(;;)
    {
      increase_external_count(head,old_head);  // 2
      node* const ptr=old_head.ptr;
      if(ptr==tail.load().ptr)
      {
        ptr->release_ref();  // 3
        return std::unique_ptr<T>();
      }
      if(head.compare_exchange_strong(old_head,ptr->next))  // 4
      {
        T* const res=ptr->data.exchange(nullptr);
        free_external_counter(old_head);  // 5
        return std::unique_ptr<T>(res);
      }
      ptr->release_ref();  // 6
    }
  }
};

代码7.21 修改pop()帮助push()完成工作

template<typename T>
class lock_free_queue
{
private:
  struct node
  {
    std::atomic<T*> data;
    std::atomic<node_counter> count;
    std::atomic<counted_node_ptr> next;  // 1
  };
public:
  std::unique_ptr<T> pop()
  {
    counted_node_ptr old_head=head.load(std::memory_order_relaxed);
    for(;;)
    {
      increase_external_count(head,old_head);
      node* const ptr=old_head.ptr;
      if(ptr==tail.load().ptr)
      {
        return std::unique_ptr<T>();
      }
      counted_node_ptr next=ptr->next.load();  // 2
      if(head.compare_exchange_strong(old_head,next))
      {
        T* const res=ptr->data.exchange(nullptr);
        free_external_counter(old_head);
        return std::unique_ptr<T>(res);
      } 
      ptr->release_ref();
    }
  }
};

代码7.22 无锁队列中简单的帮助性push()的实现

template<typename T>
class lock_free_queue
{
private:
  void set_new_tail(counted_node_ptr &old_tail,  // 1
                    counted_node_ptr const &new_tail)
  {
    node* const current_tail_ptr=old_tail.ptr;
    while(!tail.compare_exchange_weak(old_tail,new_tail) &&  // 2
          old_tail.ptr==current_tail_ptr);
    if(old_tail.ptr==current_tail_ptr)  // 3
      free_external_counter(old_tail);  // 4
    else
      current_tail_ptr->release_ref();  // 5
  }
public:
  void push(T new_value)
  {
    std::unique_ptr<T> new_data(new T(new_value));
    counted_node_ptr new_next;
    new_next.ptr=new node;
    new_next.external_count=1;
    counted_node_ptr old_tail=tail.load();

    for(;;)
    {
      increase_external_count(tail,old_tail);

      T* old_data=nullptr;
      if(old_tail.ptr->data.compare_exchange_strong(  // 6
         old_data,new_data.get()))
      {
        counted_node_ptr old_next={0};
        if(!old_tail.ptr->next.compare_exchange_strong(  // 7
           old_next,new_next))
        {
          delete new_next.ptr;  // 8
          new_next=old_next;  // 9
        }
        set_new_tail(old_tail, new_next);
        new_data.release();
        break;
      }
      else  // 10
      {
        counted_node_ptr old_next={0};
        if(old_tail.ptr->next.compare_exchange_strong(  // 11
           old_next,new_next))
        {
          old_next=new_next;  // 12
          new_next.ptr=new node;  // 13
        }
        set_new_tail(old_tail, old_next);  // 14
      }
    }
  }
};

个人理解:通过else解决多线程调用push()阻塞的问题。 

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

7.3.1 原则1:在原型设计中使用std::memory_order_seq_cst次序

std::memory_order_seq_cst比起其他内存序要简单的多,因为所有操作都将其作为总序。本章的所有例子,都是从std::memory_order_seq_cst开始,只有当基本操作正常工作的时候,才放宽内存序的选择。这种情况下,使用其他内存序就是优化(早期可以不用这样做)。通常,了解整套代码对数据结构的操作后,才能决定是否要放宽内存序的选择。所以,尝试放宽选择,可能会轻松一些。测试通过后,工作代码可能会很复杂(不过,不能完全保证内存序正确)。除非你有一个算法检查器,可以系统的测试,线程能看到的所有可能性组合,这样就能保证指定内存序的正确性(这样的测试的确存在)。

7.3.2 原则2:使用无锁的内存回收方案

与无锁代码最大的区别就是内存管理。当线程对节点进行访问的时候,线程无法删除节点。为避免内存的过多使用,还是希望这个节点能在删除的时候尽快删除。本章中介绍了三种技术来保证内存可以安全回收:

  • 等待无线程对数据结构进行访问时,删除所有等待删除的对象。

  • 使用风险指针来标识正在访问的对象。

  • 对对象进行引用计数,当没有线程对对象进行引用时将其删除。

所有例子的想法都是使用一种方式去跟踪指定对象上的线程访问数量。无锁数据结构中,还有很多方式可以用来回收内存,例如:理想情况下使用一个垃圾收集器,比起算法来说更容易实现一些。只需要让回收器知道,当节点没引用的时就回收节点。

其他替代方案就是循环使用节点,只在数据结构销毁时才将节点完全删除。因为节点能复用,这样就不会有非法的内存,所以就能避免未定义行为的发生。这种方式的缺点,就是会产生“ABA问题”。

7.3.3 原则3: 防范ABA问题

基于“比较/交换”的算法中要格外小心“ABA问题”。其流程是:

  1. 线程1读取原子变量x,并且发现其值是A。
  2. 线程1对这个值进行一些操作,比如,解引用(当其是一个指针的时候),或做查询,或其他操作。
  3. 操作系统将线程1挂起。
  4. 其他线程对x执行一些操作,并且将其值改为B。
  5. 另一个线程对A相关的数据进行修改(线程1持有),让其不再合法。可能会在释放指针指向的内存时,代码产生剧烈的反应(大问题),或者只是修改了相关值而已(小问题)。
  6. 再来一个线程将x的值改回为A。如果A是一个指针,那么其可能指向一个新的对象,只是与旧对象共享同一个地址而已。
  7. 线程1继续运行,并且对x执行“比较/交换”操作,将A进行对比。这里,“比较/交换”成功(因为其值还是A),不过这是一个错误的A(the wrong A value)。从第2步中读取的数据不再合法,但是线程1无法言明这个问题,并且之后的操作将会损坏数据结构。

7.3.4 原则4:找出忙等循环,协助其他线程

最终队列的例子中,已经见识到线程在执行push操作时,必须等待另一个push操作流程的完成。这样等待线程就会陷入到忙等待循环中,当线程尝试失败时会继续循环,这会浪费CPU的计算周期。忙等待循环结束时,就像解阻塞操作和使用互斥锁的行为一样。通过对算法的修改,当之前的线程还没有完成操作前,让等待线程执行未完成的步骤,就能让忙等待的线程不再阻塞。队列示例中需要将一个数据成员转换为原子变量,而不是使用非原子变量和使用“比较/交换”操作来做这件事。要在更加复杂的数据结构中进行使用,需要更多的变化来满足需求。

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

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

相关文章

排序:败者树和置换选择排序(解决外部排序中的优化问题)

1.算法目的&#xff08;败者树&#xff09; 解决多路平衡归并带来的问题。 在外部排序中&#xff0c;使用k路平衡归并策略, 选出一个最小元素需要对比关键字(k-1)次&#xff0c; 导致内部归并所需时间增加。&#xff08;可用“败者树”进行优化&#xff09; 2.败者树的定义 …

Spring源码分析(四) Aop全流程

一、Spring AOP基础概念 1、基础概念 连接点(Join point)&#xff1a;能够被拦截的地方&#xff0c;Spring AOP 是基于动态代理的&#xff0c;所以是方法拦截的&#xff0c;每个成员方法都可以称之为连接点&#xff1b;切点(Poincut)&#xff1a;每个方法都可以称之为连接点&…

从1开始的Matlab(快速入门)

MATLAB软件版本&#xff1a;MATLAB R2016b 本文是博主从零开始学Matlab的记录&#xff0c;适合第一次接触Matlab的同学阅读。 一、基础介绍 1.1界面认识 1.2变量命名 注&#xff1a;Matlab中的注释 %% 独占一行的注释&#xff08;有上下横线分割&#xff09; % 普通注释 …

Node.js 是如何处理请求的

前言&#xff1a;在服务器软件中&#xff0c;如何处理请求是非常核心的问题。不管是底层架构的设计、IO 模型的选择&#xff0c;还是上层的处理都会影响一个服务器的性能&#xff0c;本文介绍 Node.js 在这方面的内容。 TCP 协议的核心概念 要了解服务器的工作原理首先需要了…

【小白专属01】SpringBoot框架搭建

目录 前言 一、搭建环境 二、开始SpringBoot框架搭建 1. 打开IDEA 2. 新建项目 3. 选择版本和依赖 4. 目录结构 5. 启动项目 前言 上节回顾 上一节我们对智慧仓库管理系统的项目背景和项目效果进行的梳理&#xff0c;主要就是功能模块和实现步骤进行展示。想要从零开…

关于Adobe Acrobat Reader升级后界面布局变化-新旧布局的选择切换

Adobe Acrobat Reader 不知何时自动升级了&#xff0c;升级后界面布局发生了较大的变化&#xff1a;工具栏放到了左侧、书签栏放到了右侧。 如果对新界面布局不习惯&#xff0c;可在新界面下通过路径**【菜单】->【禁用新的 Acrobat Reader】、并重启 Acrobat Reader 后切…

【微信小程序】WXML模板语法

模板与绑定 1.数据绑定 1&#xff09;在data中定义数据 在页面对应的.js文件中把数据定义到data对象中即可 2&#xff09;在WXML中使用数据 把data中的数据绑定到页面中渲染&#xff0c;使用Mustache&#xff08;双大括号{{}}&#xff09;将变量名包起来即可&#xff0c;格式…

Codeforces Round 892 (Div. 2) - E. Maximum Monogonosity 思维dp 详细解析

题目链接 好久没有写题了复健一下qwq 题目大意 解题思路 这题目还挺妙的 首先考虑比较正常的dp&#xff0c; d p [ i ] [ j ] dp[i][j] dp[i][j] 为前 i i i的长度选 j j j个长度的最大价值&#xff0c;那么转移方程是&#xff1a; 图片来自&#xff1a;图片来源 但是这个是 …

(自学)黑客————网络安全技术

如果你想自学网络安全&#xff0c;首先你必须了解什么是网络安全&#xff01;&#xff0c;什么是黑客&#xff01;&#xff01; 1.无论网络、Web、移动、桌面、云等哪个领域&#xff0c;都有攻与防两面性&#xff0c;例如 Web 安全技术&#xff0c;既有 Web 渗透2.也有 Web 防…

2023 年 Bitget Wallet 测评

对Bitget Wallet钱包的看法 Bitget Wallet在安全性、产品实力和使用体验方面可与Metamask媲美&#xff0c;甚至有所超越&#xff0c;唯一稍显不足的是知名度稍逊一筹。在众多钱包中&#xff0c;Bitget Wallet是拥有最全面的钱包之一&#xff0c;尤其适合那些希望一步到位&…

Golang 协程池 Ants 实现原理,附详细的图文说明和代码

Golang 协程池 Ants 实现原理&#xff0c;附详细的图文说明和代码。 1 前置知识点 1.1 sync.Locker sync.Locker 是 go 标准库 sync 下定义的锁接口&#xff1a; // A Locker represents an object that can be locked and unlocked. type Locker interface {Lock()Unlock() …

【C++11】多线程

多线程创建线程thread提供的成员函数获取线程id的方式线程函数参数的问题线程join场景和detach 互斥量库&#xff08;mutex&#xff09;mutexrecursive_mutexlock_guard 和 unique_lock 原子性操作库&#xff08;atomic&#xff09;条件变量库&#xff08;condition_varuable&a…

Linux 回收内存到底怎么计算anon/file回收比例,只是swappiness这么简单?

概述 Linux内核为了区分冷热内存,将page以链表的形式保存,主要分为5个链表,除去evictable,我们主要关注另外四个链表:active file/inactive file,active anon和inactive anon链表,可以看到这主要分为两类,file和anon page,内存紧张的时候,内核开始从inactive tail定…

上网Tips: Linux截取动态效果图工具_byzanz

链接1 链接2 安装&#xff1a; sudo apt-get install byzanz 查看指令 说明 byzanz-record --help日常操作 xwininfo点击 待录制窗口 左上角 byzanz-record -x 72 -y 64 -w 1848 -h 893 -d 10 --delay5 -c /home/xixi/myGIF/test.gif小工具 获取鼠标坐标 xdotool getm…

windows下python开发环境的搭建 python入门系列 【环境搭建篇】

在正式学习Python之前要先搭建Python开发环境。由于Python是跨平台的&#xff0c;所以可以在多个操作系统上进行编程 一、python的下载安装与配置 1、Python解释器 1. 要进行Python开发&#xff0c;首先需要Python解释器&#xff0c;这里说的安装Python就是安装Python解释器…

测试必备 | 测试工程师必知的Linux命令有哪些?

在日常的测试工作中&#xff0c;涉及到测试环境搭建及通过查看日志来定位相关问题时经常会用到Linux&#xff0c;在测试工程师的面试中也经常会有笔试或面试的题目来考查测试人员对Linux的熟悉程度&#xff0c;这里分享下测试工程师需知的 Linux 命令有哪些。 Linux 作为一种常…

一文带你全面解析postman工具的使用

写在前面&#xff1a;本文转自今日头条作者雨滴测试&#xff0c;感兴趣可点击下方链接查看原文 基础篇效率篇高级篇 一文带你全面解析postman工具的使用 文章目录 一文带你全面解析postman工具的使用基础篇一、postman安装说明1.下载与安装2.界面导航说明3.发送第一个请求 二、…

动态内存操作(2)

接上一篇文章http://t.csdn.cn/1ONDq&#xff0c;这次我们继续讲解关于动态内存的相关知识。 一、常见的动态内存错误 1.对NULL指针进行解引用操作 #include<stdio.h> #include<stdlib.h> #include<limits.h> int main() {int* p (int*)malloc(INT_MAX/4);…

Linux编译器-gcc/g++使用和动静态库的对比

目录 1. 背景知识 2.安装g/gcc 2.1安装指令 2.2安装成功查看指令 3gcc如何完成 3.1预处理(进行宏替换) 3.2编译&#xff08;生成汇编&#xff09; 3.3汇编&#xff08;生成机器可识别代码&#xff09; 3.4连接&#xff08;生成可执行文件或库文件&#xff09; 3.5gcc选…

使用Python爬虫抓取网站资源的方法

Python爬虫是一种自动化程序&#xff0c;用于从互联网上获取数据。使用Python爬虫可以轻松地抓取网站上的各种资源&#xff0c;例如文本、图片、视频等。在本文中&#xff0c;我们将介绍如何使用Python爬虫抓取网站资源。 安装Python 在使用Python爬虫之前&#xff0c;需要先安…