OpenMP task construct 实现原理及源码示例解析

目录
  • 前言
  • 从编译器角度看 task construct
  • Task Construct 源码分析
  • 总结

前言

在本篇文章当中主要给大家介绍在 OpenMP 当中 task 的实现原理,以及他调用的相关的库函数的具体实现。

在本篇文章当中最重要的就是理解整个 OpenMP 的运行机制。

从编译器角度看 task construct

在本小节当中主要给大家分析一下编译器将 openmp 的 task construct 编译成什么样子,下面是一个 OpenMP 的 task 程序例子:

#include <stdio.h>
#include <omp.h>
int main()
{
#pragma omp parallel num_threads(4) default(none)
  {
#pragma omp task default(none)
    {
       printf("Hello World from tid = %d\n", omp_get_thread_num());
    }
  }
  return 0;
}

首先先捋一下整个程序被编译之后的执行流程,经过前面的文章的学习,我们已经知道了并行域当中的代码会被编译器编译成一个函数,关于这一点我们已经在前面的很多文章当中已经讨论过了,就不再进行复述。事实上 task construct 和 parallel construct 一样,task construct 也会被编译成一个函数,同样的这个函数也会被作为一个参数传递给 OpenMP 内部,被传递的这个函数可能被立即执行,也可能在函数 GOMP_parallel_end 被调用后,在到达同步点之前执行被执行(线程在到达并行域的同步点之前需要保证所有的任务都被执行完成)。

整个过程大致如下图所示:

上面的 OpenMP task 程序对应的反汇编程序如下所示:

00000000004008ad <main>:
  4008ad:       55                      push   %rbp
  4008ae:       48 89 e5                mov    %rsp,%rbp
  4008b1:       ba 04 00 00 00          mov    $0x4,%edx
  4008b6:       be 00 00 00 00          mov    $0x0,%esi
  4008bb:       bf db 08 40 00          mov    $0x4008db,%edi
  4008c0:       e8 8b fe ff ff          callq  400750 <GOMP_parallel_start@plt>
  4008c5:       bf 00 00 00 00          mov    $0x0,%edi
  4008ca:       e8 0c 00 00 00          callq  4008db <main._omp_fn.0>
  4008cf:       e8 8c fe ff ff          callq  400760 <GOMP_parallel_end@plt>
  4008d4:       b8 00 00 00 00          mov    $0x0,%eax
  4008d9:       5d                      pop    %rbp
  4008da:       c3                      retq
00000000004008db <main._omp_fn.0>:
  4008db:       55                      push   %rbp
  4008dc:       48 89 e5                mov    %rsp,%rbp
  4008df:       48 83 ec 10             sub    $0x10,%rsp
  4008e3:       48 89 7d f8             mov    %rdi,-0x8(%rbp)
  4008e7:       c7 04 24 00 00 00 00    movl   $0x0,(%rsp)		# 参数 flags
  4008ee:       41 b9 01 00 00 00       mov    $0x1,%r9d			# 参数 if_clause
  4008f4:       41 b8 01 00 00 00       mov    $0x1,%r8d			# 参数 arg_align
  4008fa:       b9 00 00 00 00          mov    $0x0,%ecx			# 参数 arg_size
  4008ff:       ba 00 00 00 00          mov    $0x0,%edx			# 参数 cpyfn
  400904:       be 00 00 00 00          mov    $0x0,%esi			# 参数 data
  400909:       bf 15 09 40 00          mov    $0x400915,%edi # 这里就是调用函数 main._omp_fn.1
  40090e:       e8 9d fe ff ff          callq  4007b0 <GOMP_task@plt>
  400913:       c9                      leaveq
  400914:       c3                      retq
0000000000400915 <main._omp_fn.1>:
  400915:       55                      push   %rbp
  400916:       48 89 e5                mov    %rsp,%rbp
  400919:       48 83 ec 10             sub    $0x10,%rsp
  40091d:       48 89 7d f8             mov    %rdi,-0x8(%rbp)
  400921:       e8 4a fe ff ff          callq  400770 <omp_get_thread_num@plt>
  400926:       89 c6                   mov    %eax,%esi
  400928:       bf d0 09 40 00          mov    $0x4009d0,%edi
  40092d:       b8 00 00 00 00          mov    $0x0,%eax
  400932:       e8 49 fe ff ff          callq  400780 <printf@plt>
  400937:       c9                      leaveq
  400938:       c3                      retq
  400939:       0f 1f 80 00 00 00 00    nopl   0x0(%rax)

从上面程序反汇编的结果我们可以知道,在主函数当中仍然和之前一样在并行域前后分别调用了 GOMP_parallel_start 和 GOMP_parallel_end,然后在两个函数之间调用并行域的代码 main._omp_fn.0 ,并行域当中的代码被编译成函数 main._omp_fn.0 ,从上面的汇编代码我们可以看到在函数 main._omp_fn.0 调用了函数 GOMP_task ,这个函数的函数声明如下所示:

void
GOMP_task (void (*fn) (void *), void *data, void (*cpyfn) (void *, void *),
	   long arg_size, long arg_align, bool if_clause, unsigned flags);

在这里我们重要解释一下部分参数,首先我们需要了解的是在 x86 当中的函数调用规约,这一点我们在前面的文章当中已经讨论过了,这里只是说明一下:

寄存器 含义
rdi 第一个参数
rsi 第二个参数
rdx 第三个参数
rcx 第四个参数
r8 第五个参数
r9 第六个参数

根据上面的寄存器和参数的对应关系,在上面的汇编代码当中已经标注了对应的参数。在这些参数当中最重要的一个参数就是第一个函数指针,对应的汇编语句为 mov $0x400915,%edi,可以看到的是传入的函数的地址为 0x400915,根据上面的汇编程序可以知道这个地址对应的函数就是 main._omp_fn.1,这其实就是 task 区域之间被编译之后的对应的函数,从上面的 main._omp_fn.1 汇编程序当中也可以看出来调用了函数 omp_get_thread_num,这和前面的 task 区域当中代码是相对应的。

现在我们来解释一下其他的几个参数:

  • fn,task 区域被编译之后的函数地址。
  • data,函数 fn 的参数。
  • cpyfn,参数拷贝函数,一般是 NULL,有时候需要 task 当中的数据不能是共享的,需要时私有的,这个时候可能就需要数据拷贝函数,如果有数据需要及进行拷贝而且这个参数还为 NULL 的话,那么在 OpenMP 内部就会使用 memcpy 进行内存拷贝。
  • arg_size,参数的大小。
  • arg_align,参数多少字节对齐。
  • if_clause,if 子句当中的比较结果,如果没有 if 字句的话就是 true 。
  • flags,用于表示 task construct 的特征或者属性,比如是否是最终任务。

我们现在使用另外一个例子,来看看参数传递的变化。

#include <stdio.h>
#include <omp.h>
int main()
{
#pragma omp parallel num_threads(4) default(none)
  {
     int data = omp_get_thread_num();
#pragma omp task default(none) firstprivate(data) if(data > 100)
    {
       data = omp_get_thread_num();
       printf("data = %d Hello World from tid = %d\n", data, omp_get_thread_num());
    }
  }
  return 0;
}

上面的程序被编译之后对应的汇编程序如下所示:

00000000004008ad <main>:
  4008ad:       55                      push   %rbp
  4008ae:       48 89 e5                mov    %rsp,%rbp
  4008b1:       48 83 ec 10             sub    $0x10,%rsp
  4008b5:       ba 04 00 00 00          mov    $0x4,%edx
  4008ba:       be 00 00 00 00          mov    $0x0,%esi
  4008bf:       bf df 08 40 00          mov    $0x4008df,%edi
  4008c4:       e8 87 fe ff ff          callq  400750 <GOMP_parallel_start@plt>
  4008c9:       bf 00 00 00 00          mov    $0x0,%edi
  4008ce:       e8 0c 00 00 00          callq  4008df <main._omp_fn.0>
  4008d3:       e8 88 fe ff ff          callq  400760 <GOMP_parallel_end@plt>
  4008d8:       b8 00 00 00 00          mov    $0x0,%eax
  4008dd:       c9                      leaveq
  4008de:       c3                      retq
00000000004008df <main._omp_fn.0>:
  4008df:       55                      push   %rbp
  4008e0:       48 89 e5                mov    %rsp,%rbp
  4008e3:       48 83 ec 20             sub    $0x20,%rsp
  4008e7:       48 89 7d e8             mov    %rdi,-0x18(%rbp)
  4008eb:       e8 80 fe ff ff          callq  400770 <omp_get_thread_num@plt>
  4008f0:       89 45 fc                mov    %eax,-0x4(%rbp)
  4008f3:       83 7d fc 64             cmpl   $0x64,-0x4(%rbp)
  4008f7:       0f 9f c2                setg   %dl
  4008fa:       8b 45 fc                mov    -0x4(%rbp),%eax
  4008fd:       89 45 f0                mov    %eax,-0x10(%rbp)
  400900:       48 8d 45 f0             lea    -0x10(%rbp),%rax
  400904:       c7 04 24 00 00 00 00    movl   $0x0,(%rsp)	# 参数 flags
  40090b:       41 89 d1                mov    %edx,%r9d	# 参数 if_clause
  40090e:       41 b8 04 00 00 00       mov    $0x4,%r8d	# 参数 arg_align
  400914:       b9 04 00 00 00          mov    $0x4,%ecx	# 参数 arg_size
  400919:       ba 00 00 00 00          mov    $0x0,%edx	# 参数 cpyfn
  40091e:       48 89 c6                mov    %rax,%rsi	# 参数 data
  400921:       bf 2d 09 40 00          mov    $0x40092d,%edi	# 这里就是调用函数 main._omp_fn.1
  400926:       e8 85 fe ff ff          callq  4007b0 <GOMP_task@plt>
  40092b:       c9                      leaveq
  40092c:       c3                      retq
000000000040092d <main._omp_fn.1>:
  40092d:       55                      push   %rbp
  40092e:       48 89 e5                mov    %rsp,%rbp
  400931:       48 83 ec 20             sub    $0x20,%rsp
  400935:       48 89 7d e8             mov    %rdi,-0x18(%rbp)
  400939:       48 8b 45 e8             mov    -0x18(%rbp),%rax
  40093d:       8b 00                   mov    (%rax),%eax
  40093f:       89 45 fc                mov    %eax,-0x4(%rbp)
  400942:       e8 29 fe ff ff          callq  400770 <omp_get_thread_num@plt>
  400947:       89 c2                   mov    %eax,%edx
  400949:       8b 45 fc                mov    -0x4(%rbp),%eax
  40094c:       89 c6                   mov    %eax,%esi
  40094e:       bf f0 09 40 00          mov    $0x4009f0,%edi
  400953:       b8 00 00 00 00          mov    $0x0,%eax
  400958:       e8 23 fe ff ff          callq  400780 <printf@plt>
  40095d:       c9                      leaveq
  40095e:       c3                      retq
  40095f:       90                      nop

在上面的函数当中我们将 data 一个 4 字节的数据作为线程私有数据,可以看到给函数 GOMP_task 传递的参数参数的大小以及参数的内存对齐大小都发生来变化,从原来的 0 变成了 4,这因为 int 类型数据占 4 个字节。

Task Construct 源码分析

在本小节当中主要谈论在 OpenMP 内部是如何实现 task 的,关于这一部分内容设计的内容还是比较庞杂,首先需要了解的是在 OpenMP 当中使用 task construct 的被称作显示任务(explicit task),这种任务在 OpenMP 当中会有两个任务队列(双向循环队列),将所有的任务都保存在这样一张列表当中,整体结构如下图所示:

在上图当中由同一个线程创建的任务为 child_task,他们之间使用 next_child 和 prev_child 两个指针进行连接,不同线程创建的任务之间可以使用 next_queue 和 prev_queue 两个指针进行连接。

任务的结构体描述如下所示:

struct gomp_task
{
  struct gomp_task *parent;	// 任务的父亲任务
  struct gomp_task *children;	// 子任务
  struct gomp_task *next_child;	// 下一个子任务
  struct gomp_task *prev_child;	// 上一个子任务
  struct gomp_task *next_queue;	// 下一个任务 (不一定是同一个线程创建的子任务)
  struct gomp_task *prev_queue;	// 上一个任务 (不一定是同一个线程创建的子任务)
  struct gomp_task_icv icv; // openmp 当中内部全局设置使用变量的值(internal control variable)
  void (*fn) (void *);	// task construct 被编译之后的函数
  void *fn_data;	// 函数参数
  enum gomp_task_kind kind; // 任务类型 具体类型如下面的枚举类型
  bool in_taskwait;	// 是否处于 taskwait 状态
  bool in_tied_task; // 是不是在绑定任务当中
  bool final_task; // 是不是最终任务
  gomp_sem_t taskwait_sem; // 对象锁 用于保证线程操作这个数据的时候的线程安全
};
// openmp 当中的任务的状态
enum gomp_task_kind
{
  GOMP_TASK_IMPLICIT,
  GOMP_TASK_IFFALSE,
  GOMP_TASK_WAITING,
  GOMP_TASK_TIED
};

在了解完上面的数据结构之后我们来看一下前面的给 OpenMP 内部提交任务的函数 GOMP_task,其源代码如下所示:

/* Called when encountering an explicit task directive.  If IF_CLAUSE is
   false, then we must not delay in executing the task.  If UNTIED is true,
   then the task may be executed by any member of the team.  */
void
GOMP_task (void (*fn) (void *), void *data, void (*cpyfn) (void *, void *),
	   long arg_size, long arg_align, bool if_clause, unsigned flags)
{
  struct gomp_thread *thr = gomp_thread ();
  // team 是 OpenMP 一个线程组当中共享的数据
  struct gomp_team *team = thr->ts.team;
#ifdef HAVE_BROKEN_POSIX_SEMAPHORES
  /* If pthread_mutex_* is used for omp_*lock*, then each task must be
     tied to one thread all the time.  This means UNTIED tasks must be
     tied and if CPYFN is non-NULL IF(0) must be forced, as CPYFN
     might be running on different thread than FN.  */
  if (cpyfn)
    if_clause = false;
  if (flags & 1)
    flags &= ~1;
#endif
  // 这里表示如果是 if 子句的条件为真的时候或者是孤立任务(team == NULL )或者是最终任务的时候或者任务队列当中的任务已经很多的时候
  // 提交的任务需要立即执行而不能够放入任务队列当中然后在 GOMP_parallel_end 函数当中进行任务的取出
  // 再执行
  if (!if_clause || team == NULL
      || (thr->task && thr->task->final_task)
      || team->task_count > 64 * team->nthreads)
    {
      struct gomp_task task;
      gomp_init_task (&task, thr->task, gomp_icv (false));
      task.kind = GOMP_TASK_IFFALSE;
      task.final_task = (thr->task && thr->task->final_task) || (flags & 2);
      if (thr->task)
	task.in_tied_task = thr->task->in_tied_task;
      thr->task = &task;
      if (__builtin_expect (cpyfn != NULL, 0))
	{
        // 这里是进行数据的拷贝
	  char buf[arg_size + arg_align - 1];
	  char *arg = (char *) (((uintptr_t) buf + arg_align - 1)
				& ~(uintptr_t) (arg_align - 1));
	  cpyfn (arg, data);
	  fn (arg);
	}
      else
        // 如果不需要进行数据拷贝则直接执行这个函数
	fn (data);
      /* Access to "children" is normally done inside a task_lock
	 mutex region, but the only way this particular task.children
	 can be set is if this thread's task work function (fn)
	 creates children.  So since the setter is *this* thread, we
	 need no barriers here when testing for non-NULL.  We can have
	 task.children set by the current thread then changed by a
	 child thread, but seeing a stale non-NULL value is not a
	 problem.  Once past the task_lock acquisition, this thread
	 will see the real value of task.children.  */
      if (task.children != NULL)
	{
	  gomp_mutex_lock (&team->task_lock);
	  gomp_clear_parent (task.children);
	  gomp_mutex_unlock (&team->task_lock);
	}
      gomp_end_task ();
    }
  else
    {
    // 下面就是将任务先提交到任务队列当中然后再取出执行
      struct gomp_task *task;
      struct gomp_task *parent = thr->task;
      char *arg;
      bool do_wake;
      task = gomp_malloc (sizeof (*task) + arg_size + arg_align - 1);
      arg = (char *) (((uintptr_t) (task + 1) + arg_align - 1)
		      & ~(uintptr_t) (arg_align - 1));
      gomp_init_task (task, parent, gomp_icv (false));
      task->kind = GOMP_TASK_IFFALSE;
      task->in_tied_task = parent->in_tied_task;
      thr->task = task;
    // 这里就是参数拷贝逻辑 如果存在拷贝函数就通过拷贝函数进行参数赋值 否则使用 memcpy 进行
    // 参数的拷贝
      if (cpyfn)
	cpyfn (arg, data);
      else
	memcpy (arg, data, arg_size);
      thr->task = parent;
      task->kind = GOMP_TASK_WAITING;
      task->fn = fn;
      task->fn_data = arg;
      task->in_tied_task = true;
      task->final_task = (flags & 2) >> 1;
    // 在这里获取全局队列锁 保证下面的代码在多线程条件下的线程安全
    // 因为在下面的代码当中会对全局的队列进行修改操作 下面的操作就是队列的一些基本操作啦
      gomp_mutex_lock (&team->task_lock);
      if (parent->children)
	{
	  task->next_child = parent->children;
	  task->prev_child = parent->children->prev_child;
	  task->next_child->prev_child = task;
	  task->prev_child->next_child = task;
	}
      else
	{
	  task->next_child = task;
	  task->prev_child = task;
	}
      parent->children = task;
      if (team->task_queue)
	{
	  task->next_queue = team->task_queue;
	  task->prev_queue = team->task_queue->prev_queue;
	  task->next_queue->prev_queue = task;
	  task->prev_queue->next_queue = task;
	}
      else
	{
	  task->next_queue = task;
	  task->prev_queue = task;
	  team->task_queue = task;
	}
      ++team->task_count;
      gomp_team_barrier_set_task_pending (&team->barrier);
      do_wake = team->task_running_count + !parent->in_tied_task
		< team->nthreads;
      gomp_mutex_unlock (&team->task_lock);
      if (do_wake)
	gomp_team_barrier_wake (&team->barrier, 1);
    }
}

对于上述所讨论的内容大家只需要了解相关的整体流程即可,细节除非你是 openmp 的开发人员,否则事实上没有多大用,大家只需要了解大致过程即可,帮助你进一步深入理解 OpenMP 内部的运行机制。

但是需要了解的是上面的整个过程还只是将任务提交到 OpenMP 内部的任务队列当中,还没有执行,我们在前面谈到过在线程执行完并行域的代码会执行函数 GOMP_parallel_end 在这个函数内部还会调用其他函数,最终会调用函数 gomp_barrier_handle_tasks 将内部的所有的任务执行完成。

void
gomp_barrier_handle_tasks (gomp_barrier_state_t state)
{
  struct gomp_thread *thr = gomp_thread ();
  struct gomp_team *team = thr->ts.team;
  struct gomp_task *task = thr->task;
  struct gomp_task *child_task = NULL;
  struct gomp_task *to_free = NULL;
  // 首先对全局的队列结构进行加锁操作
  gomp_mutex_lock (&team->task_lock);
  if (gomp_barrier_last_thread (state))
    {
      if (team->task_count == 0)
	{
	  gomp_team_barrier_done (&team->barrier, state);
	  gomp_mutex_unlock (&team->task_lock);
	  gomp_team_barrier_wake (&team->barrier, 0);
	  return;
	}
      gomp_team_barrier_set_waiting_for_tasks (&team->barrier);
    }
  while (1)
    {
      if (team->task_queue != NULL)
	{
	  struct gomp_task *parent;
	// 从任务队列当中拿出一个任务
	  child_task = team->task_queue;
	  parent = child_task->parent;
	  if (parent && parent->children == child_task)
	    parent->children = child_task->next_child;
	  child_task->prev_queue->next_queue = child_task->next_queue;
	  child_task->next_queue->prev_queue = child_task->prev_queue;
	  if (child_task->next_queue != child_task)
	    team->task_queue = child_task->next_queue;
	  else
	    team->task_queue = NULL;
	  child_task->kind = GOMP_TASK_TIED;
	  team->task_running_count++;
	  if (team->task_count == team->task_running_count)
	    gomp_team_barrier_clear_task_pending (&team->barrier);
	}
      gomp_mutex_unlock (&team->task_lock);
      if (to_free) // 释放任务的内存空间 to_free 在后面会被赋值成 child_task
	{
	  gomp_finish_task (to_free);
	  free (to_free);
	  to_free = NULL;
	}
      if (child_task) // 调用任务对应的函数
	{
	  thr->task = child_task;
	  child_task->fn (child_task->fn_data);
	  thr->task = task;
	}
      else
	return; // 退出 while 循环
      gomp_mutex_lock (&team->task_lock);
      if (child_task)
	{
	  struct gomp_task *parent = child_task->parent;
	  if (parent)
	    {
	      child_task->prev_child->next_child = child_task->next_child;
	      child_task->next_child->prev_child = child_task->prev_child;
	      if (parent->children == child_task)
		{
		  if (child_task->next_child != child_task)
		    parent->children = child_task->next_child;
		  else
		    {
		      /* We access task->children in GOMP_taskwait
			 outside of the task lock mutex region, so
			 need a release barrier here to ensure memory
			 written by child_task->fn above is flushed
			 before the NULL is written.  */
		      __atomic_store_n (&parent->children, NULL,
					MEMMODEL_RELEASE);
		      if (parent->in_taskwait)
			gomp_sem_post (&parent->taskwait_sem);
		    }
		}
	    }
	  gomp_clear_parent (child_task->children);
	  to_free = child_task;
	  child_task = NULL;
	  team->task_running_count--;
	  if (--team->task_count == 0
	      && gomp_team_barrier_waiting_for_tasks (&team->barrier))
	    {
	      gomp_team_barrier_done (&team->barrier, state);
	      gomp_mutex_unlock (&team->task_lock);
	      gomp_team_barrier_wake (&team->barrier, 0);
	      gomp_mutex_lock (&team->task_lock);
	    }
	}
    }
}

总结

在本篇文章当中主要给大家介绍了,OpenMP 内部对于任务的处理流程,这其中的细节非常复杂,大家只需要了解它的整个工作流程即可,这已经能够帮助大家理清楚整个 OpenMP 内部是如何对任务进行处理的,如果大家感兴趣可以自行研读源程序。

更多精彩内容合集可访问项目:github.com/Chang-LeHun…

以上就是OpenMP task construct 实现原理及源码示例解析的详细内容,更多关于OpenMP task construct原理的资料请关注我们其它相关文章!

(0)

相关推荐

  • 深入剖析OpenMP锁的原理与实现

    目录 前言 深入分析 omp_lock_t omp_lock_t 源码分析 深入分析 omp_nest_lock_t omp_nest_lock_t 源码分析 源代码函数名称不同的原因揭秘 总结 前言 在本篇文章当中主要给大家介绍一下 OpenMP 当中经常使用到的锁并且仔细分析它其中的内部原理!在 OpenMP 当中主要有两种类型的锁,一个是 omp_lock_t 另外一个是 omp_nest_lock_t,这两个锁的主要区别就是后者是一个可重入锁,所谓可冲入锁就是一旦一个线程已经拿到这个锁了

  • OpenMP中For Construct对dynamic的调度方式详解

    目录 前言 前置知识 dynamic 调度方式分析 实例分析 总结 前言 在本篇文章当中主要给大家介绍 OpenMp for construct 的实现原理,以及与他相关的动态库函数分析,与 for construct 非常相关的是循环的调度方式,在 OpenMP 当中一共有四种调调方式,auto, dynamic, guided, runtime, 在本篇文章当中主要是对 dynamic 的调度方式进行分析. 前置知识 在介绍 for construct 的实现原理之前,我们首先需要了解一下编

  • OPENMP SECTIONS CONSTRUCT原理示例解析

    目录 前言 编译器角度分析 动态库函数分析 总结 前言 在本篇文章当中主要给大家介绍 OpenMP 当中主要给大家介绍 OpenMP 当中 sections construct 的实现原理以及他调用的动态库函数分析.如果已经了解过了前面的关于 for 的调度方式的分析,本篇文章就非常简单了. 编译器角度分析 在这一小节当中我们将从编译器角度去分析编译器会怎么处理 sections construct ,我们以下面的 sections construct 为例子,看看编译器是如何处理 sectio

  • OpenMP Parallel Construct的实现原理详解

    目录 Parallel 分析——编译器角度 深入剖析 Parallel 动态库函数参数传递 动态库函数分析 参数传递分析 汇编程序分析 GOMP_parallel_start 详细参数分析 动态库函数源码分析 GOMP_parallel_start 源码分析 GOMP_parallel_end 分析 总结 Parallel 分析——编译器角度 在本小节当中我们将从编译器的角度去分析该如何处理 parallel construct .首先从词法分析和语法分析的角度来说这对编译器并不难,只需要加上一

  • OpenMP task construct 实现原理及源码示例解析

    目录 前言 从编译器角度看 task construct Task Construct 源码分析 总结 前言 在本篇文章当中主要给大家介绍在 OpenMP 当中 task 的实现原理,以及他调用的相关的库函数的具体实现. 在本篇文章当中最重要的就是理解整个 OpenMP 的运行机制. 从编译器角度看 task construct 在本小节当中主要给大家分析一下编译器将 openmp 的 task construct 编译成什么样子,下面是一个 OpenMP 的 task 程序例子: #inclu

  • MyBatis SqlSource源码示例解析

    目录 正文 SqlNode SqlNode接口定义 BoundSql SqlSource SqlSource解析时机 SqlSource调用时机 总结 正文 MyBatis版本:3.5.12. 本篇讲从mybatis的角度分析SqlSource.在xml中sql可能是带?的预处理语句,也可能是带$或者动态标签的动态语句,也可能是这两者的混合语句. SqlSource设计的目标就是封装xml的crud节点,使得mybatis运行过程中可以直接通过SqlSource获取xml节点中解析后的SQL.

  • Flink 侧流输出源码示例解析

    目录 Flink 侧流输出源码解析 源码解析 TimestampedCollector#collect CountingOutput#collect BroadcastingOutputCollector#collect RecordWriterOutput#collect ProcessOperator#ContextImpl#output CountingOutput#collect BroadcastingOutputCollector#collect RecordWriterOutput

  • JS前端操作 Cookie源码示例解析

    目录 引言 源码分析 使用 源码 分析 set get remove withAttributes & withConverter 总结 引言 前端操作Cookie的场景其实并不多见,Cookie也因为各种问题被逐渐淘汰,但是我们不用Cookie也可以学习一下它的思想,或者通过这次的源码来学习其他的一些知识. 今天带来的是:js-cookie 源码分析 使用 根据README,我们可以看到js-cookie的使用方式: // 设置 Cookies.set('name', 'value'); //

  • Flutter加载图片流程之ImageProvider源码示例解析

    目录 加载网络图片 ImageProvider resolve obtainKey resolveStreamForKey loadBuffer load(被废弃) evict 总结 困惑解答 加载网络图片 Image.network()是Flutter提供的一种从网络上加载图片的方法,它可以从指定的URL加载图片,并在加载完成后将其显示在应用程序中.本节内容,我们从源码出发,探讨下图片的加载流程. ImageProvider ImageProvider是Flutter中一个抽象类,它定义了一种

  • Python 装饰器常用的创建方式及源码示例解析

    目录 装饰器简介 基础通用装饰器 源码示例 执行结果 带参数装饰器 源码示例 源码结果 源码解析 多装饰器执行顺序 源码示例 执行结果 解析 类装饰器 源码示例 执行结果 解析 装饰器简介 装饰器(decorator)是一种高级Python语法.可以对一个函数.方法或者类进行加工.在Python中,我们有多种方法对函数和类进行加工,相对于其它方式,装饰器语法简单,代码可读性高.因此,装饰器在Python项目中有广泛的应用.修饰器经常被用于有切面需求的场景,较为经典的有插入日志.性能测试.事务处理

  • Flutter加载图片流程之ImageCache源码示例解析

    目录 ImageCache _pendingImages._cache._liveImages maximumSize.currentSize clear evict _touch _checkCacheSize _trackLiveImage putIfAbsent clearLiveImages 答疑解惑 ImageCache const int _kDefaultSize = 1000; const int _kDefaultSizeBytes = 100 << 20; // 100 M

  • React Refs 的使用forwardRef 源码示例解析

    目录 三种使用方式 1. String Refs 2. 回调 Refs 3. createRef 两种使用目的 Refs 转发 createRef 源码 forwardRef 源码 三种使用方式 React 提供了 Refs,帮助我们访问 DOM 节点或在 render 方法中创建的 React 元素. React 提供了三种使用 Ref 的方式: 1. String Refs class App extends React.Component { constructor(props) { su

  • Nacos配置中心集群原理及源码分析

    目录 Nacos集群工作原理 配置变更同步入口 AsyncNotifyService AsyncTask 目标节点接收请求 NacosDelayTaskExecuteEngine ProcessRunnable processTasks DumpProcessor.process Nacos作为配置中心,必然需要保证服务节点的高可用性,那么Nacos是如何实现集群的呢? 下面这个图,表示Nacos集群的部署图. Nacos集群工作原理 Nacos作为配置中心的集群结构中,是一种无中心化节点的设计

  • java并发容器CopyOnWriteArrayList实现原理及源码分析

    CopyOnWriteArrayList是Java并发包中提供的一个并发容器,它是个线程安全且读操作无锁的ArrayList,写操作则通过创建底层数组的新副本来实现,是一种读写分离的并发策略,我们也可以称这种容器为"写时复制器",Java并发包中类似的容器还有CopyOnWriteSet.本文会对CopyOnWriteArrayList的实现原理及源码进行分析. 实现原理 我们都知道,集合框架中的ArrayList是非线程安全的,Vector虽是线程安全的,但由于简单粗暴的锁同步机制,

随机推荐