C++ 并发编程实战 第八章 设计并发代码 二

news2024/11/24 7:16:38

目录

8.3 设计数据结构以提升多线程程序的性能

8.3.1 针对复杂操作的数据划分

8.3.2 其他数据结构的访问模式

8.4 设计并发代码时要额外考虑的因素

8.4.1 并行算法代码中的异常安全

8.4.2 可扩展性和Amdahl定律

8.4.3 利用多线程隐藏等待行为

8.4.4 借并发特性改进响应能力

8.5 并发代码的设计实践


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

8.3 设计数据结构以提升多线程程序的性能

8.1节了解了各种划分方法,8.2节中了解了影响性能的各种因素。如何在设计数据结构时,使用这些信息提高多线程代码的性能?与第6、7章中的问题不同,之前是关于如何设计安全、并发访问的数据结构。

当为多线程性能设计数据结构时,需要考虑竞争(contention),伪共享(false sharing)和邻近数据(data proximity),这三个对于性能都有着重大的影响的因素,改善数据布局,或者将数据进行修改。

8.3.1 针对复杂操作的数据划分

线程间划分工作的方式有很多,假设矩阵的行或列数量大于处理器的数量,可以让每个线程计算出结果矩阵列上的元素,或是行上的元素,亦或计算一个子矩阵。

回顾一下8.2.3和8.2.4节,对于数组来说访问连续元素是最好的方式,因为这会减少缓存的刷新,降低伪共享的概率。如果要让每个线程处理几行,线程需要读取第一个矩阵中的每一个元素,并且读取第二个矩阵上的相关数据,不过这里只需要对列进行写入。给定的两个矩阵是以行连续的方式存储,这就意味着当访问第一个矩阵的第一行的前N个元素,而后是第二行的前N个元素,以此类推(N是列的数量)。其他线程会访问每行的的其他元素,访问相邻的列,所以从行上读取的N个元素也是连续的,这将最大程度降低伪共享的几率。当然,如果N个元素已占有相应的空间,且N个元素也就是每个缓存行上具体的存储元素数量,就会让伪共享的情况消失,因为线程将会对独立缓存行上的数据进行操作。

另一方面,当每个线程处理一组行数据,就需要读取第二个矩阵上的数据,还要读取第一个矩阵中的相关行上的值,不过只需要对行上的值进行写入即可。因为矩阵是以行连续的方式存储,那么可以以N行的方式访问所有的元素。如果再次选择相邻行,这就意味着线程现在只能写入N行,就有不能被其他线程所访问的连续内存块。让线程对每组列进行处理就是一种改进,因为伪共享只可能有在一个内存块的最后几个元素和下一个元素的开始几个上发生,不过具体的时间还要根据目标架构来决定。

第三个选择——将矩阵分成小矩阵块?这可以看作先对列进行划分,再对行进行划分。因此,划分列的时候,同样有伪共享的问题存在。如果可以选择内存块所拥有行的数量,就可以有效的避免伪共享。将大矩阵划分为小块,对于读取来说是有好处的:就不再需要读取整个源矩阵了。只需要读取目标矩形里面相关行列的值就可以了。具体的来看,考虑1,000行和1,000列的两个矩阵相乘,就会有1百万个元素。如果有100个处理器,这样就可以每次处理10行的数据,也就是10,000个元素。不过,为了计算着10,000个元素,就需要对第二个矩阵中的全部内容进行访问(1百万个元素),再加上10,000个相关行(第一个矩阵)上的元素,大概就要访问1,010,000个元素。另外,硬件能处理100x100的数据块(总共10,000个元素),这就需要对第一个矩阵中的100行进行访问(100x1,000=100,000个元素),还有第二个矩阵中的100列(另外100,000个)。这才只有200,000个元素,就需要5轮读取才能完成。如果读取的元素少一些,缓存缺失的情况就会少一些。

因此,将矩阵分成小块或正方形的块,要比使用单线程来处理少量的列好的多。当然,可以根据源矩阵的大小和处理器的数量,在运行时对块的大小进行调整。性能是很重要的指标时,就需要对目标架构上的各项指标进行测量,并且查阅相关领域的文献——如果只是做矩阵乘法,我认为这并不是最好的选择。

同样的原理可以应用于任何情况,这种情况就是有很大的数据块需要在线程间进行划分。仔细观察所有数据访问的各个方面,以及确定性能问题产生的原因。各种领域中,出现问题的情况都很相似:改变划分方式就能够提高性能,不需要对基本算法进行任何修改。

OK,我们已经了解了访问数组是如何对性能产生影响的

8.3.2 其他数据结构的访问模式

同样的考虑适用于数据结构的数据访问模式,如同优化对数组的访问:

  • 尝试调整数据在线程间的分布,让同一线程中的数据紧密联系在一起。

  • 尝试减少线程上所需的数据量。

  • 尝试让不同线程访问不同的存储位置,以避免伪共享。

和用互斥量来保护数据类似。假设有一个类,包含一些数据项和一个用于保护数据的互斥量(多线程环境下)。如果互斥量和数据项在内存中很接近,对于一个需要获取互斥量的线程来说是很理想的情况。因为在之前为了对互斥量进行修改,已经加载了需要的数据,所以需要的数据可能早已存入处理器的缓存中。不过,还有一个缺点:当其他线程尝试锁住互斥量时(第一个线程还没有是释放),线程就能对数据项进行访问。互斥锁是作为“读-改-写”原子操作实现的,对于相同位置的操作都需要先获取互斥量,如果互斥量已锁,就会调用系统内核。这种“读-改-写”操作可能会让数据存储在缓存中,让线程获取的互斥量变得毫无作用。从目前互斥量的发展来看,这并不是个问题,因为线程不会直到互斥量解锁才接触互斥量。不过,当互斥量共享同一缓存行时,其中存储的是线程已使用的数据,这时拥有互斥量的线程将会遭受到性能打击,因为其他线程也在尝试锁住互斥量。

一种测试伪共享问题的方法:填充大量的数据块,让不同线程并发访问。

struct protected_data
{
  std::mutex m;
  char padding[65536];  // 如果你的编译器不支持std::hardware_destructive_interference_size,可以使用类似65536字节,这个数字肯定超过一个缓存行
  my_data data_to_protect;
};

 用来测试互斥量竞争或

struct my_data
{
  data_item1 d1;
  data_item2 d2;
  char padding[65536];
};
my_data some_array[256];

用来测试数组数据中的伪共享。

如果这样能够提高性能,就能知道伪共享在这里的确存在。

当然,设计并发的时候有更多的数据访问模式需要考虑。

8.4 设计并发代码时要额外考虑的因素

虽然,有很多设计并发代码的内容,但还需要考虑的更多,比如异常安全和可扩展性。随着核数的增加,性能越来越高(无论是在减少执行时间,还是增加吞吐率),这样的代码称为“可扩展”代码。理想状态下,性能随着核数的增加线性增长,也就是当系统有100个处理器时,其性能是系统只有1核时的100倍。

虽然,非扩展性代码依旧可以正常工作——单线程应用就无法扩展,例如:异常安全是一个正确性问题,如果代码不是异常安全的,最终会破坏不变量,或是造成条件竞争,亦或是操作抛出异常意外终止应用。我们就先来看一下异常安全的问题。

8.4.1 并行算法代码中的异常安全

代码8.2 std::accumulate的原始并行版本(源于代码2.8)

template<typename Iterator,typename T>
struct accumulate_block
{
  void operator()(Iterator first,Iterator last,T& result)
  {
    result=std::accumulate(first,last,result);  // 1
  }
};

template<typename Iterator,typename T>
T parallel_accumulate(Iterator first,Iterator last,T init)
{
  unsigned long const length=std::distance(first,last);  // 2

  if(!length)
    return init;

  unsigned long const min_per_thread=25;
  unsigned long const max_threads=
    (length+min_per_thread-1)/min_per_thread;

  unsigned long const hardware_threads=
    std::thread::hardware_concurrency();
  
  unsigned long const num_threads=
    std::min(hardware_threads!=0?hardware_threads:2,max_threads);
  
  unsigned long const block_size=length/num_threads;
  
  std::vector<T> results(num_threads);  // 3
  std::vector<std::thread> threads(num_threads-1);  // 4

  Iterator block_start=first;  // 5
  for(unsigned long i=0;i<(num_threads-1);++i)
  {
    Iterator block_end=block_start;  // 6
    std::advance(block_end,block_size);
    threads[i]=std::thread(  // 7
      accumulate_block<Iterator,T>(),
      block_start,block_end,std::ref(results[i]));
    block_start=block_end;  // 8
  }
  accumulate_block()(block_start,last,results[num_threads-1]);  // 9

  std::for_each(threads.begin(),threads.end(),
    std::mem_fn(&std::thread::join));

  return std::accumulate(results.begin(),results.end(),init);  // 10
}

代码8.3 使用std::packaged_task的并行std::accumulate

template<typename Iterator,typename T>
struct accumulate_block
{
  T operator()(Iterator first,Iterator last)  // 1
  {
    return std::accumulate(first,last,T());  // 2
  }
};

template<typename Iterator,typename T>
T parallel_accumulate(Iterator first,Iterator last,T init)
{
  unsigned long const length=std::distance(first,last);

  if(!length)
    return init;

  unsigned long const min_per_thread=25;
  unsigned long const max_threads=
    (length+min_per_thread-1)/min_per_thread;

  unsigned long const hardware_threads=
    std::thread::hardware_concurrency();

  unsigned long const num_threads=
    std::min(hardware_threads!=0?hardware_threads:2,max_threads);

  unsigned long const block_size=length/num_threads;

  std::vector<std::future<T> > futures(num_threads-1);  // 3
  std::vector<std::thread> threads(num_threads-1);

  Iterator block_start=first;
  for(unsigned long i=0;i<(num_threads-1);++i)
  {
    Iterator block_end=block_start;
    std::advance(block_end,block_size);
    std::packaged_task<T(Iterator,Iterator)> task(  // 4
      accumulate_block<Iterator,T>());
    futures[i]=task.get_future();  // 5
    threads[i]=std::thread(std::move(task),block_start,block_end);  // 6
    block_start=block_end;
  }
  T last_result=accumulate_block()(block_start,last);  // 7
  
  std::for_each(threads.begin(),threads.end(),
    std::mem_fn(&std::thread::join));

  T result=init;  // 8
  for(unsigned long i=0;i<(num_threads-1);++i)
  {
    result+=futures[i].get();  // 9
  }
  result += last_result;  // 10
  return result;
}

这样问题就解决了:工作线程上抛出的异常,可以在主线程上抛出。如果不止一个工作线程抛出异常,那么只有一个异常能在主线程中抛出。如果这个问题很重要,可以使用类似std::nested_exception对所有抛出的异常进行捕捉。

剩下的问题:当第一个新线程和当所有线程都汇入主线程时抛出异常时,就会让线程产生泄露。最简单的方法就是捕获所有抛出的线程,汇入的线程依旧是joinable()的,并且会再次抛出异常:

代码8.4 异常安全版std::accumulate

template<typename Iterator,typename T>
T parallel_accumulate(Iterator first,Iterator last,T init)
{
  unsigned long const length=std::distance(first,last);

  if(!length)
    return init;

  unsigned long const min_per_thread=25;
  unsigned long const max_threads=
    (length+min_per_thread-1)/min_per_thread;

  unsigned long const hardware_threads=
    std::thread::hardware_concurrency();

  unsigned long const num_threads=
    std::min(hardware_threads!=0?hardware_threads:2,max_threads);

  unsigned long const block_size=length/num_threads;

  std::vector<std::future<T> > futures(num_threads-1);
  std::vector<std::thread> threads(num_threads-1);
  join_threads joiner(threads);  // 1

  Iterator block_start=first;
  for(unsigned long i=0;i<(num_threads-1);++i)
  {
    Iterator block_end=block_start;
    std::advance(block_end,block_size);
    std::packaged_task<T(Iterator,Iterator)> task(
      accumulate_block<Iterator,T>());
    futures[i]=task.get_future();
    threads[i]=std::thread(std::move(task),block_start,block_end);
    block_start=block_end;
  }
  T last_result=accumulate_block()(block_start,last);
  T result=init;
  for(unsigned long i=0;i<(num_threads-1);++i)
  {
    result+=futures[i].get();  // 2
  }
  result += last_result;
  return result;
}

std::async()的异常安全

当需要管理线程时,需要代码是异常安全的。那现在来看一下使用std::async()是怎样完成异常安全的。本例中标准库对线程进行了较好的管理,并且当future处以就绪状态时,就能生成新的线程。对于异常安全,还需要注意一件事,如果没有等待的情况下对future实例进行销毁,析构函数会等待对应线程执行完毕后才执行。这就能体现线程泄露的问题,因为线程还在执行,且持有数据引用。下面将展示使用std::async()完成异常安全的实现。

代码8.5 异常安全并行版std::accumulate——使用std::async()

template<typename Iterator,typename T>
T parallel_accumulate(Iterator first,Iterator last,T init)
{
  unsigned long const length=std::distance(first,last);  // 1
  unsigned long const max_chunk_size=25;
  if(length<=max_chunk_size)
  {
    return std::accumulate(first,last,init);  // 2
  }
  else
  {
    Iterator mid_point=first;
    std::advance(mid_point,length/2);  // 3
    std::future<T> first_half_result=
      std::async(parallel_accumulate<Iterator,T>,  // 4
        first,mid_point,init);
    T second_half_result=parallel_accumulate(mid_point,last,T());  // 5
    return first_half_result.get()+second_half_result;  // 6
  }
}

这个版本是对数据进行递归划分,而非在预计算后对数据进行分块。因此,这个版本要比之前简单很多,并且这个版本也是异常安全的。和之前一样,要确定序列的长度①,如果其长度小于数据块包含数据的最大值,可以直接调用std::accumulate②。如果元素的数量超出了数据块包含数据的最大值,就需要找到数量中点③,将这个数据块分成两部分,然后再生成一个异步任务对另一半数据进行处理④。第二半的数据是通过直接的递归调用来处理的⑤,之后将两个块的结果加和到一起⑥。标准库能保证std::async的调用能够充分的利用硬件线程,并且不会产生线程的超额申请,一些“异步”调用在get()⑥后同步执行。

优雅的地方不仅在于利用硬件并发的优势,还能保证异常安全。如果有异常在递归⑤中抛出,通过std::async④所产生的future,将异常在传播时销毁。这就需要依次等待异步任务的完成,因此也能避免悬空线程的出现。另外,当异步任务抛出异常,且被future所捕获后,在对get()⑥调用的时候,future中存储的异常会再次抛出。

8.4.2 可扩展性和Amdahl定律

扩展性代表了应用利用系统中处理器执行任务的能力。一种极端的方式就是将应用写死为单线程运行,这种应用就是完全不可扩展的。即使添加了100个处理器到你的系统中,应用的性能都不会有任何改变。另一种就是像SETI@Home[3]项目一样,让应用使用系统中成千上万的处理器(以个人电脑的形式加入网络的用户)成为可能。

对于任意的多线程程序,运行时的工作线程数量会有所不同。应用初始阶段只有一个线程,之后会在这个线程上衍生出新的线程。理想状态:每个线程都做着有用的工作,不过这种情况几乎是不可能发生的。线程通常会花时间进行互相等待,或等待I/O操作的完成。

一种简化的方式就是就是将程序划分成“串行”和“并行”部分。串行部分:只能由单线程执行一些工作的地方。并行部分:可以让所有可用的处理器一起工作的部分。当在多处理系统上运行应用时,“并行”部分理论上会完成的相当快,因为其工作被划分为多份,放在不同的处理器上执行。“串行”部分则不同,只能一个处理器执行所有工作。这样的(简化)假设下,就可以随着处理数量的增加,估计一下性能的增益:当程序“串行”部分的时间用fs来表示,那么性能增益(P)就可以通过处理器数量(N)进行估计:

这就是Amdahl定律,讨论并发程序性能的时候都会引用到的公式。如果每行代码都能并行化,串行部分就为0,性能增益就为N。或者,当串行部分为1/3时,当处理器数量无限增长,都无法获得超过3的性能增益。

Amdahl定律明确了,对代码最大化并发可以保证所有处理器都能用来做有用的工作。如果将“串行”部分的减小,或者减少线程的等待,就可以在多处理器的系统中获取更多的性能收益。或者,当能提供更多的数据让系统进行处理,并且让并行部分做最重要的工作,就可以减少“串行”部分,以获取更高的性能增益。

扩展性:当有更多的处理器加入时,减少单个动作的执行时间,或在给定时间内做更多工作。有时这两个指标是等价的(如果处理器的速度相当快,就可以处理更多的数据)。选择线程间的工作划分的技术前,需要辨别哪些方面是能够扩展的。

8.4.3 利用多线程隐藏等待行为

8.4.4 借并发特性改进响应能力

代码8.6 将GUI线程和任务线程进行分离

std::thread task_thread;
std::atomic<bool> task_cancelled(false);

void gui_thread()
{
  while(true)
  {
    event_data event=get_event();
    if(event.type==quit)
      break;
    process(event);
  }
}

void task()
{
  while(!task_complete() && !task_cancelled)
  {
    do_next_operation();
  }
  if(task_cancelled)
  {
    perform_cleanup();
  }
  else
  {
    post_gui_event(task_complete);
  }
}

void process(event_data const& event)
{
  switch(event.type)
  {
  case start_task:
    task_cancelled=false;
    task_thread=std::thread(task);
    break;
  case stop_task:
    task_cancelled=true;
    task_thread.join();
    break;
  case task_complete:
    task_thread.join();
    display_results();
    break;
  default:
    //...
  }
}

8.5 并发代码的设计实践

略 

8.6 小结

本章我们讨论了很多东西。从划分线程间的工作开始(比如,数据提前划分或让线程形成流水线),以低层视角来看多线程下的性能问题,顺带了解了伪共享和数据通讯,了解访问数据的模式对性能的影响。再后,了解了异常安全和可扩展性是如何影响并发代码设计的。最后,用一些并行算法实现结束本章。

设计这些并行算法实现时碰到的问题,在设计其他并行代码的时候也会遇到。

本章关于线程池的部分移除了,线程池——一个预先设定的线程组,会将任务指定给池中的线程。很多不错的想法可以用来设计一个不错的线程池,所以我们将在下一章中来了解一些有关线程池的问题,以及线程的高级管理方式。

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

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

相关文章

ELK整合springboot(第二课)

一、创建一个springboot的项目 pom文件如下&#xff1a; <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://maven.apache.org/POM/4.0.0" xmlns:xsi"http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLo…

Uniapp小程序 时间段选择限制(开始时间 结束时间相互限制)

实现效果&#xff1a; 这里我使用的是uview2.0的DatetimePicker 选择器&#xff0c;实现开始时间与结束时间相互限制的效果&#xff0c;接下来简单看下实现的代码吧&#xff0c;首先看下选择器组件以及相关参数值的初始化 <!-- 时间选择-开始 --> <u-datetime-picker …

【前段基础入门之】=>你不知道的 CSS 选择器的进阶使用!

导语&#xff1a; 在上一章节中&#xff0c;我们了解了 CSS 的一些基本语法概念&#xff0c;那么在这一章节中我们就带来 CSS 选择器知识的分享&#xff0c;选择器这一章的知识点有一点多&#xff0c;不过我们只要认真去理解&#xff0c;学习它也是没什么问题的&#xff0c;还有…

PowerShell 命令窗口执行 pnpm 命令报错 无法加载文件 pnpm.ps1,因为在此系统上禁止运行脚本

目录 问题解决验证附&#xff1a;开源项目微服务商城项目前后端分离项目 问题 在 PowerShell 命令行窗口使用 pnpm run dev 启动 vue3-element-admin 报错&#xff1a; pnpm : 无法加载文件 C:\Users\youlai\AppData\Roaming\npm\pnpm.ps1&#xff0c;因为在此系统上禁止运行脚…

【kubernetes】kubernetes中的Ingress使用

文章目录 1 Service和Endpoint1.1 服务的转发1.2 headless service及其使用场景1.3 服务的类型 2 Ingress3 ingress controller4 IngressClass5 nginx-ingress-controller 1 Service和Endpoint k8s中运行的最小单位是Pod&#xff0c;如果单独运行Pod&#xff0c;没有用控制器进…

假期get新技能?低代码模型应用工具HuggingFists

HuggingFists是什么&#xff1f; HuggingFists是一款研究和使用HuggingFace模型和数据集的AI应用工具。 众所周知&#xff0c;Hugging Face是一家人工智能&#xff08;AI&#xff09;技术公司&#xff0c;致力于开发和推广自然语言处理&#xff08;NLP&#xff09;技术&#xf…

工信部:杭州亚运会开幕式首创 5G 超密组网方案,场馆网络无缝覆盖

“工信 V 报”今日发布消息称&#xff0c;工信部经过精心统筹、周密部署&#xff0c;举全系统之力圆满完成了杭州亚运会开幕式各项保障任务。 据介绍&#xff0c;亚运会的指挥调度、安全保卫、通信网络、计时记分、电视转播等系统顺畅运行&#xff0c;对无线电安全、信息通信服…

《Jetpack Compose从入门到实战》第三章 定制 UI 视图

目录 配置颜色、字体与形状Welcome PageLogin PageHome Page 主题CompositionLocal 配置颜色、字体与形状 -ui.theme.Color.kt val pink100 Color(0xFFFFF1F1) val pink900 Color(0xFF3F2C2C) val white Color(0xFFFFFFFF) val white850 Color(0xD9FFFFFF) val gray Col…

朝着“强国建设民族复兴”之路奋勇前行

今&#xff08;2023年10月1日&#xff09;天&#xff0c;本“人民体验官”隆重推广人民日报官方微博文化产品《人民日报国庆社论#&#xff1a;坚定不移朝着强国建设、民族复兴的宏伟目标奋勇前进》。 图&#xff1a;来源“人民体验官”推广平台 以下推广今天的《人民日报》社论…

深度学习与python theano

文章目录 前言1.安装2.基本用法3.function用法4.shared 变量5.activation function6.Layer层7.regression 回归例子8.classification分类学习9.过拟合10.正则化11.save model12 总结 前言 本章主要介绍深度学习与python theano。 1.安装 2.基本用法 3.function用法 4.sha…

Linux系统编程系列之进程间通信-信号量组

一、什么是信号量组 信号量组是信号量的一种&#xff0c; 是system-V三种IPC对象之一&#xff0c;是进程间通信的一种方式。 二、信号量组的特性 信号量组不是用来传输数据的&#xff0c;而是作为“旗语”&#xff0c;用来协调各进程或者线程工作的。信号量组可以一次性在其内…

【Linux】 OpenSSH_9.3p1 升级到 OpenSSH_9.3p2(亲测无问题,建议收藏)

&#x1f468;‍&#x1f393;博主简介 &#x1f3c5;云计算领域优质创作者   &#x1f3c5;华为云开发者社区专家博主   &#x1f3c5;阿里云开发者社区专家博主 &#x1f48a;交流社区&#xff1a;运维交流社区 欢迎大家的加入&#xff01; &#x1f40b; 希望大家多多支…

基于Python3搭建qt开发环境

Python可视化编程相信大部分刚接触都是tkinter&#xff0c;tkinter是Python自带的库&#xff0c;不需要安装第三方库即可使用&#xff0c;在我的Python专栏中也有很多基于tkinter来设计的可视化界面。本篇文章将尝试另外一个Python的可视化编程库(pyqt)&#xff0c;与tkinter编…

深度剖析Linux磁盘分区 | LVM逻辑卷 | VDO卷 | AutoFS存储自动挂载

深度剖析Linux磁盘分区 | LVM逻辑卷 | VDO卷 | AutoFS存储自动挂载 前言说明1. 安装操作系统分区配置2. 大磁盘分区管理3. LVM逻辑卷管理3.1. 创建LVM逻辑卷3.1.1. 创建物理卷PV3.1.2. 创建卷组VG3.1.3. 创建逻辑卷LV3.1.4. 创建并挂载文件系统3.1.5. 配置开机自动挂载 3.2. 逻…

bypass disable_function 学习

LD_PRELOAD 我是在做了 buu的 REC ME 来做这个系列 所以 LD_PRELOAD 已经有了解了 我们来做这个题目 CTFHub Bypass disable_function —— LD_PRELOAD本环境来源于AntSword-Labs <!DOCTYPE html> <html> <head><title>CTFHub Bypass disable_func…

零基础Linux_9(进程)环境变量+进程地址空间+进程创建fork

目录 1. 环境变量 1.1 环境变量基本概念 1.2 环境变量PATH 1.3 环境变量HOME和SHELL 1.4 获取环境变量&#xff08;main函数参数&#xff09; 1.4.1 main函数第三个参数 1.4.2 设置普通变量和环境变量 1.4.3 main函数前两个参数 2. 进程地址空间 2.1 验证进程地址空…

技术Leader对下管理的法宝-SMART

SMART方法论 源于国外管理大师的《管理的实践》是管理者能够更加明确员工高效工作的利器&#xff0c;科学、规范的对员工绩效制定考核目标和考核标准5个单词缩写 Specific:目标要具体Measurable:目标成果要可衡量(量化) Attainable:目标要可实现&#xff0c;避免过高/过低Rel…

为什么字节大量用GO而不是Java?

见字如面&#xff0c;我是军哥。 我看很多程序员对字节编程语言选型很好奇&#xff0c;为此我还特地问了在字节的两位4-1的技术大佬朋友&#xff0c;然后加上自己的思考&#xff0c;总结了一下就以下 2 个原因&#xff1a; 1、 选型上没有历史包袱 字节的早期的程序员大多来自于…

OpenHarmony Trace的使用

背景&#xff1a; 近期很多开发者反馈OpenHarmony三方库Imageknife有性能问题&#xff1a;连续拖动很多张图片时&#xff0c;界面有明显的卡顿现象。 因为对这个三方库的源码并不了解&#xff0c;因此需要了解目前Imageknife渲染花费了多少时间&#xff0c;最初想的是只有通过…

不愧是疑问解决神器!你强任你强

不愧是疑问解决神器&#xff01;你强任你强&#x1f44d;&#x1f44d;&#x1f44d; 在过去&#xff0c;我习惯用这种方式来阅读书籍或文章&#xff1a;先快速浏览一遍&#xff0c;然后再进行复读&#xff0c;并最终总结所学的知识点。然而&#xff0c;长期以来&#xff0c;我…