Openmp Runtime 库函数汇总(下)——深入剖析锁🔒原理与实现

Openmp Runtime 库函数汇总(下)——深入剖析锁?原理与实现

前言

在本篇文章当中主要给大家介绍一下 OpenMP 当中经常使用到的锁并且仔细分析它其中的内部原理!在 OpenMP 当中主要有两种类型的锁,一个是 omp_lock_t 另外一个是 omp_nest_lock_t,这两个锁的主要区别就是后者是一个可重入锁,所谓可冲入锁就是一旦一个线程已经拿到这个锁了,那么它下一次想要拿这个锁的就是就不会阻塞,但是如果是 omp_lock_t 不管一个线程是否拿到了锁,只要当前锁没有释放,不管哪一个线程都不能够拿到这个锁。在后问当中将有仔细的例子来解释这一点。本篇文章是基于 GNU OpenMP Runtime Library !

深入分析 omp_lock_t

这是 OpenMP 头文件给我们提供的一个结构体,我们来看一下它的定义:

typedef struct
{
  unsigned char _x[4] 
    __attribute__((__aligned__(4)));
} omp_lock_t;

事实上这个结构体并没有什么特别的就是占 4 个字节,我们甚至可以认为他就是一个 4 字节的 int 的类型的变量,只不过使用方式有所差异。与这个结构体相关的主要有以下几个函数:

  • omp_init_lock,这个函数的主要功能是初始化 omp_lock_t 对象的,当我们初始化之后,这个锁就处于一个没有上锁的状态,他的函数原型如下所示:
void omp_init_lock(omp_lock_t *lock);
  • omp_set_lock,在调用这个函数之前一定要先调用函数 omp_init_lock 将 omp_lock_t 进行初始化,直到这个锁被释放之前这个线程会被一直阻塞。如果这个锁被当前线程已经获取过了,那么将会造成一个死锁,这就是上面提到了锁不能够重入的问题,而我们在后面将要分析的锁 omp_nest_lock_t 是能够进行重入的,即使当前线程已经获取到了这个锁,也不会造成死锁而是会重新获得锁。这个函数的函数原型如下所示:
void omp_set_lock(omp_lock_t *lock);
  • omp_test_lock,这个函数的主要作用也是用于获取锁,但是这个函数可能会失败,如果失败就会返回 false 成功就会返回 true,与函数 omp_set_lock 不同的是,这个函数并不会导致线程被阻塞,如果获取锁成功他就会立即返回 true,如果失败就会立即返回 false 。它的函数原型如下所示:
int omp_test_lock(omp_lock_t *lock); 
  • omp_unset_lock,这个函数和上面的函数对应,这个函数的主要作用就是用于解锁,在我们调用这个函数之前,必须要使用 omp_set_lock 或者 omp_test_lock 获取锁,它的函数原型如下:
void omp_unset_lock(omp_lock_t *lock);
  • omp_destroy_lock,这个方法主要是对锁进行回收处理,但是对于这个锁来说是没有用的,我们在后文分析他的具体的实现的时候会发现这是一个空函数。

我们现在使用一个例子来具体的体验一下上面的函数:


#include <stdio.h>
#include <omp.h>

int main()
{
   omp_lock_t lock;
   // 对锁进行初始化操作
   omp_init_lock(&lock);
   int data = 0;
#pragma omp parallel num_threads(16) shared(lock, data) default(none)
   {
      // 进行加锁处理 同一个时刻只能够有一个线程能够获取锁
      omp_set_lock(&lock);
      data++;
      // 解锁处理 线程在出临界区之前需要解锁 好让其他线程能够进入临界区
      omp_unset_lock(&lock);
   }
   omp_destroy_lock(&lock);
   printf("data = %d\n", data);
   return 0;
}

在上面的函数我们定义了一个 omp_lock_t 锁,并且在并行域内启动了 16 个线程去执行 data ++ 的操作,因为是多线程环境,因此我们需要将上面的操作进行加锁处理。

omp_lock_t 源码分析

  • omp_init_lock,对于这个函数来说最终在 OpenMP 动态库内部会调用下面的函数:
typedef int gomp_mutex_t;
static inline void
gomp_mutex_init (gomp_mutex_t *mutex)
{
  *mutex = 0;
}

从上面的函数我们可以知道这个函数的作用就是将我们定义的 4 个字节的锁赋值为0,这就是锁的初始化,其实很简单。

  • omp_set_lock,这个函数最终会调用 OpenMP 内部的一个函数,具体如下所示:
static inline void
gomp_mutex_lock (gomp_mutex_t *mutex)
{
  int oldval = 0;
  if (!__atomic_compare_exchange_n (mutex, &oldval, 1, false,
				    MEMMODEL_ACQUIRE, MEMMODEL_RELAXED))
    gomp_mutex_lock_slow (mutex, oldval);
}

在上面的函数当中线程首先会调用 __atomic_compare_exchange_n 将锁的值由 0 变成 1,还记得我们在前面对锁进行初始化的时候将锁的值变成0了吗?

我们首先需要了解一下 __atomic_compare_exchange_n ,这个是 gcc 内嵌的一个函数,在这里我们只关注前面三个参数,后面三个参数与内存模型有关,这并不是我们本篇文章的重点,他的主要功能是查看 mutex 指向的地址的值等不等于 oldval ,如果等于则将这个值变成 1,这一整个操作能够保证原子性,如成功将 mutex 指向的值变成 1 的话,那么这个函数就返回 true 否则返回 false 对应 C 语言的数据就是 1 和 0 。如果 oldval 的值不等于 mutex 所指向的值,那么这个函数就会将这个值写入 oldval 。

如果这个操作不成功那么就会调用 gomp_mutex_lock_slow 函数这个函数的主要作用就是如果使用不能够使用原子指令获取锁的话,那么就需要进入内核态,将这个线程挂起。在这个函数的内部还会测试是否能够通过源自操作获取锁,因为可能在我们调用 gomp_mutex_lock_slow 这个函数的时候可能有其他线程释放锁了。如果仍然不能够成功的话,那么就会真正的将这个线程挂起不会浪费 CPU 资源,gomp_mutex_lock_slow 函数具体如下:

void
gomp_mutex_lock_slow (gomp_mutex_t *mutex, int oldval)
{
  /* First loop spins a while.  */
  // 先自旋 如果自旋一段时间还没有获取锁 那就将线程刮挂起
  while (oldval == 1)
    {
      if (do_spin (mutex, 1))
	{
	  /* Spin timeout, nothing changed.  Set waiting flag.  */
	  oldval = __atomic_exchange_n (mutex, -1, MEMMODEL_ACQUIRE);
    // 如果获得? 就返回
	  if (oldval == 0)
	    return;
    // 如果没有获得? 那么就将线程刮起
	  futex_wait (mutex, -1);
    // 这里是当挂起的线程被唤醒之后的操作 也有可能是 futex_wait 没有成功
	  break;
	}
      else
	{
	  /* Something changed.  If now unlocked, we're good to go.  */
	  oldval = 0;
	  if (__atomic_compare_exchange_n (mutex, &oldval, 1, false,
					   MEMMODEL_ACQUIRE, MEMMODEL_RELAXED))
	    return;
	}
    }

  /* Second loop waits until mutex is unlocked.  We always exit this
     loop with wait flag set, so next unlock will awaken a thread.  */
  while ((oldval = __atomic_exchange_n (mutex, -1, MEMMODEL_ACQUIRE)))
    do_wait (mutex, -1);
}

在上面的函数当中有三个依赖函数,他们的源代码如下所示:


static inline void
futex_wait (int *addr, int val)
{
  // 在这里进行系统调用,将线程挂起 
  int err = syscall (SYS_futex, addr, gomp_futex_wait, val, NULL);
  if (__builtin_expect (err < 0 && errno == ENOSYS, 0))
    {
      gomp_futex_wait &= ~FUTEX_PRIVATE_FLAG;
      gomp_futex_wake &= ~FUTEX_PRIVATE_FLAG;
    // 在这里进行系统调用,将线程挂起 
      syscall (SYS_futex, addr, gomp_futex_wait, val, NULL);
    }
}

static inline void do_wait (int *addr, int val)
{
  if (do_spin (addr, val))
    futex_wait (addr, val);
}

static inline int do_spin (int *addr, int val)
{
  unsigned long long i, count = gomp_spin_count_var;

  if (__builtin_expect (__atomic_load_n (&gomp_managed_threads,
                                         MEMMODEL_RELAXED)
                        > gomp_available_cpus, 0))
    count = gomp_throttled_spin_count_var;
  for (i = 0; i < count; i++)
    if (__builtin_expect (__atomic_load_n (addr, MEMMODEL_RELAXED) != val, 0))
      return 0;
    else
      cpu_relax ();
  return 1;
}

static inline void
cpu_relax (void)
{
  __asm volatile ("" : : : "memory");
}

如果大家对具体的内部实现非常感兴趣可以仔细研读上面的代码,如果从 0 开始解释上面的代码比较麻烦,这里就不做详细的分析了,简要做一下概括:

  • 在锁的设计当中有一个非常重要的原则:一个线程最好不要进入内核态被挂起,如果能够在用户态最好在用户态使用原子指令获取锁,这是因为进入内核态是一个非常耗时的事情相比起原子指令来说。

  • 锁(就是我们在前面讨论的一个 4 个字节的 int 类型的值)有以下三个值:

    • -1 表示现在有线程被挂起了。
    • 0 表示现在是一个无锁状态,这个状态就表示锁的竞争比较激烈。
    • 1 表示这个线程正在被一个线程用一个原子指令——比较并交换(CAS)获得了,这个状态表示现在锁的竞争比较轻。
  • _atomic_exchange_n (mutex, -1, MEMMODEL_ACQUIRE); ,这个函数也是 gcc 内嵌的一个函数,这个函数的主要作用就是将 mutex 的值变成 -1,然后将 mutex 指向的地址的原来的值返回。

  • __atomic_load_n (addr, MEMMODEL_RELAXED),这个函数的作用主要作用是原子的加载 addr 指向的数据。

  • futex_wait 函数的功能是将线程挂起,将线程挂起的系统调用为 futex ,大家可以使用命令 man futex 去查看 futex 的手册。

  • do_spin 函数的功能是进行一定次数的原子操作(自旋),如果超过这个次数就表示现在这个锁的竞争比较激烈为了更好的使用 CPU 的计算资源可以将这个线程挂起。如果在自旋(spin)的时候发现锁的值等于 val 那么就返回 0 ,如果在进行 count 次操作之后我们还没有发现锁的值变成 val 那么就返回 1 ,这就表示锁的竞争比较激烈。

  • 可能你会疑惑在函数 gomp_mutex_lock_slow 的最后一部分为什么要用 while 循环,这是因为 do_wait 函数不一定会将线程挂起,这个和 futex 系统调用有关,感兴趣的同学可以去看一下 futex 的文档,就了解这么设计的原因了。

  • 在上面的源代码当中有两个 OpenMP 内部全局变量,gomp_throttled_spin_count_var 和 gomp_spin_count_var 用于表示自旋的次数,这个也是 OpenMP 自己进行设计的这个值和环境变量 OMP_WAIT_POLICY 也有关系,具体的数值也是设计团队的经验值,在这里就不介绍这一部分的源代码了。

其实上面的加锁过程是非常复杂的,大家可以自己自行去好好分析一下这其中的设计,其实是非常值得学习的,上面的加锁代码贯彻的宗旨就是:能不进内核态就别进内核态。

  • omp_unset_lock,这个函数的主要功能就是解锁了,我们再来看一下他的源代码设计。这个函数最终调用的 OpenMP 内部的函数为 gomp_mutex_unlock ,其源代码如下所示:
static inline void
gomp_mutex_unlock (gomp_mutex_t *mutex)
{
  int wait = __atomic_exchange_n (mutex, 0, MEMMODEL_RELEASE);
  if (__builtin_expect (wait < 0, 0))
    gomp_mutex_unlock_slow (mutex);
}

在上面的函数当中调用一个函数 gomp_mutex_unlock_slow ,其源代码如下:

void
gomp_mutex_unlock_slow (gomp_mutex_t *mutex)
{
  // 表示唤醒 1 个线程
  futex_wake (mutex, 1);
}

static inline void
futex_wake (int *addr, int count)
{
  int err = syscall (SYS_futex, addr, gomp_futex_wake, count);
  if (__builtin_expect (err < 0 && errno == ENOSYS, 0))
    {
      gomp_futex_wait &= ~FUTEX_PRIVATE_FLAG;
      gomp_futex_wake &= ~FUTEX_PRIVATE_FLAG;
      syscall (SYS_futex, addr, gomp_futex_wake, count);
    }
}

在函数 gomp_mutex_unlock 当中首先调用原子操作 __atomic_exchange_n,将锁的值变成 0 也就是无锁状态,这个其实是方便被唤醒的线程能够不被阻塞(关于这一点大家可以好好去分分析 gomp_mutex_lock_slow 最后的 while 循环,就能够理解其中的深意了),然后如果 mutex 原来的值(这个值会被赋值给 wait )小于 0 ,我们在前面已经谈到过,这个值只能是 -1,这就表示之前有线程进入内核态被挂起了,因此这个线程需要唤醒之前被阻塞的线程,好让他们能够继续执行。唤醒之前线程的函数就是 gomp_mutex_unlock_slow,在这个函数内部会调用 futex_wake 去真正的唤醒一个之前被锁阻塞的线程。

  • omp_test_lock,这个函数主要是使用原子指令看是否能够获取锁,而不尝试进入内核,如果成功获取锁返回 1 ,否则返回 0 。这个函数在 OpenMP 内部会最终调用下面的函数。

int
gomp_test_lock_30 (omp_lock_t *lock)
{
  int oldval = 0;

  return __atomic_compare_exchange_n (lock, &oldval, 1, false,
				      MEMMODEL_ACQUIRE, MEMMODEL_RELAXED);
}

从上面源代码来看这函数就是做了原子的比较并交换操作,如果成功就是获取锁并且返回值为 1 ,反之没有获取锁那么就不成功返回值就是 0 。

总的说来上面的锁的设计主要有一下的两个方向:

  • Fast path : 能够在用户态解决的事儿就别进内核态,只要能够通过原子指令获取锁,那么就使用原子指令,因为进入内核态是一件非常耗时的事情。
  • Slow path : 当经历过一定数目的自旋操作之后发现还是不能够获得锁,那么就能够判断此时锁的竞争比较激烈,如果这个时候还不将线程挂起的话,那么这个线程好就会一直消耗 CPU ,因此这个时候我们应该要进入内核态将线程挂起以节省 CPU 的计算资源。

杂谈:

  • 其实上面的锁的设计是非公平的我们可以看到在 gomp_mutex_unlock 函数当中,他是直接将 mutex 和 0 进行交换,根据前面的分析现在的锁处于一个没有线程获取的状态,如果这个时候有其他线程进来那么就可以直接通过原子操作获取锁了,而这个线程如果将之前被阻塞的线程唤醒,那么这个被唤醒的线程就会处于 gomp_mutex_lock_slow 最后的那个循环当中,如果这个时候 mutex 的值不等于 0 (因为有新来的线程通过原子指令将 mutex 的值由 0 变成 1 了),那么这个线程将继续阻塞,而且会将 mutex 的值设置成 -1。

  • 上面的锁设计加锁和解锁的交互情况是非常复杂的,因为需要确保加锁和解锁的操作不会造成死锁,大家可以使用各种顺序去想象一下代码的执行就能够发现其中的巧妙之处了。

  • 不要将获取锁和线程的唤醒关联起来,线程被唤醒不一定获得锁,而且 futex 系统调用存在虚假唤醒的可能(关于这一点可以查看 futex 的手册)。

深入分析 omp_nest_lock_t

在介绍可重入锁(omp_nest_lock_t)之前,我们首先来介绍一个需求,看看之前的锁能不能够满足这个需求。


#include <stdio.h>
#include <omp.h>

void echo(int n, omp_nest_lock_t* lock, int * s)
{
   if (n > 5)
   {
      omp_set_nest_lock(lock);
      // 在这里进行递归调用 因为在上一行代码已经获取锁了 递归调用还需要获取锁
      // omp_lock_t 是不能满足这个要求的 而 omp_nest_lock_t 能
      echo(n - 1, lock, s);
      *s += 1;
      omp_unset_nest_lock(lock);
   }
   else
   {
      omp_set_nest_lock(lock);
      *s += n;
      omp_unset_nest_lock(lock);
   }
}

int main()
{
   int n = 100;
   int s = 0;
   omp_nest_lock_t lock;
   omp_init_nest_lock(&lock);
   echo(n, &lock, &s);
   printf("s = %d\n", s);
   omp_destroy_nest_lock(&lock);

   printf("%ld\n", sizeof (omp_nest_lock_t));
   return 0;
}

在上面的代码当中会调用函数 echo,而在 echo 函数当中会进行递归调用,但是在递归调用之前线程已经获取锁了,如果进行递归调用的话,因为之前这个锁已经被获取了,因此如果再获取锁的话就会产生死锁,因为线程已经被获取了。

如果要解决上面的问题就需要使用的可重入锁了,所谓可重入锁就是当一个线程获取锁之后,如果这个线程还想获取锁他仍然能够获取到锁,而不会产生死锁的现象。如果将上面的锁改成可重入锁 omp_nest_lock_t 那么程序就会正常执行完成,而不会产生死锁。


#include <stdio.h>
#include <omp.h>

void echo(int n, omp_nest_lock_t* lock, int * s)
{
   if (n > 5)
   {
      omp_set_nest_lock(lock);
      echo(n - 1, lock, s);
      *s += 1;
      omp_unset_nest_lock(lock);
   }
   else
   {
      omp_set_nest_lock(lock);
      *s += n;
      omp_unset_nest_lock(lock);
   }
}

int main()
{
   int n = 100;
   int s = 0;
   omp_nest_lock_t lock;
   omp_init_nest_lock(&lock);
   echo(n, &lock, &s);
   printf("s = %d\n", s);
   omp_destroy_nest_lock(&lock);
   return 0;
}

上面的各个函数的使用方法和之前的 omp_lock_t 的使用方法是一样的:

  • 锁的初始化 —— init 。
  • 加锁 —— set_lock。
  • 解锁 —— unset_lock 。
  • 锁的释放 —— destroy 。

我们现在来分析一下 omp_nest_lock_t 的实现原理,首先需要了解的是 omp_nest_lock_t 这个结构体一共占用 16 个字节,这 16个字节的字段如下所示:

typedef struct { 
  int lock; 
  int count; 
  void *owner; 
} omp_nest_lock_t;

上面的结构体一共占 16 个字节现在我们来仔细分析以上面的三个字段的含义:

  • lock,这个字段和上面谈到的 omp_lock_t 是一样的作用都是占用 4 个字节,主要是用于原子操作。
  • count,在前面我们已经谈到了 omp_nest_lock_t 同一个线程在获取锁之后仍然能够获取锁,因此这个字段的含义就是表示线程获取了多少次锁。
  • owner,这个字段的含义就比较简单了,我们需要记录是哪个线程获取的锁,这个字段的意义就是执行获取到锁的线程。
  • 这里大家只需要稍微了解一下这几个字段的含义,在后面分析源代码的时候大家就能够体会到这其中设计的精妙之处了。

omp_nest_lock_t 源码分析

  • omp_init_nest_lock,这个函数的作用主要是进行初始化操作,将 omp_nest_lock_t 中的数据中所有的比特全部变成 0 。在 OpenMP 内部中最终会调用下面的函数:
void
gomp_init_nest_lock_30 (omp_nest_lock_t *lock)
{
  // 字符 '\0' 对应的数值就是 0 这个就是将 lock 指向的 16 个字节全部清零
  memset (lock, '\0', sizeof (*lock));
}
  • omp_set_nest_lock,这个函数的主要作用就是加锁,在 OpenMP 内部最终调用的函数如下所示:
void
gomp_set_nest_lock_30 (omp_nest_lock_t *lock)
{
  // 首先获取当前线程的指针
  void *me = gomp_icv (true);
	// 如果锁的所有者不是当前线程,那么就调用函数 gomp_mutex_lock 去获取锁
  // 这里的 gomp_mutex_lock 函数和我们之前在 omp_lock_t 当中所分析的函数
  // 是同一个函数
  if (lock->owner != me)
    {
      gomp_mutex_lock (&lock->lock);
    	// 当获取锁成功之后将当前线程的所有者设置成自己
      lock->owner = me;
    }
	// 因为获取锁了所以需要将当前线程获取锁的次数加一
  lock->count++;
}

在上面的程序当中主要的流程如下:

  • 如果当前锁的所有者是自己,也就是说如果当前线程之前已经获取到锁了,那么久直接将 count 进行加一操作。

  • 如果当线程还还没有获取到锁,那么就使用 gomp_mutex_lock 去获取锁,如果当前已经有线程获取到锁了,那么线程就会被挂起。

  • omp_unset_nest_lock

void
gomp_unset_nest_lock_30 (omp_nest_lock_t *lock)
{
  if (--lock->count == 0)
    {
      lock->owner = NULL;
      gomp_mutex_unlock (&lock->lock);
    }
}

在由了 omp_lock_t 的分析基础之后上面的代码也是比较容易分析的,首先会将 count 的值减去一,如果 count 的值变成 0,那么就可以进行解锁操作,将锁的所有者变成 NULL ,然后使用 gomp_mutex_unlock 函数解锁,唤醒之前被阻塞的线程。

  • omp_test_nest_lock
int
gomp_test_nest_lock_30 (omp_nest_lock_t *lock)
{
  void *me = gomp_icv (true);
  int oldval;

  if (lock->owner == me)
    return ++lock->count;

  oldval = 0;
  if (__atomic_compare_exchange_n (&lock->lock, &oldval, 1, false,
				   MEMMODEL_ACQUIRE, MEMMODEL_RELAXED))
    {
      lock->owner = me;
      lock->count = 1;
      return 1;
    }

  return 0;
}

这个不进入内核态获取锁的代码也比较容易,首先分析当前锁的拥有者是不是当前线程,如果是那么就将 count 的值加一,否则就使用原子指令看看能不能获取锁,如果能够获取锁就返回 1 ,否则就返回 0 。

源代码函数名称不同的原因揭秘

在上面的源代码分析当中我们可以看到我们真正分析的代码并不是在 omp.h 的头文件当中定义的,这是因为在 OpenMP 内部做了很多的重命名处理:

# define gomp_init_lock_30 omp_init_lock
# define gomp_destroy_lock_30 omp_destroy_lock
# define gomp_set_lock_30 omp_set_lock
# define gomp_unset_lock_30 omp_unset_lock
# define gomp_test_lock_30 omp_test_lock
# define gomp_init_nest_lock_30 omp_init_nest_lock
# define gomp_destroy_nest_lock_30 omp_destroy_nest_lock
# define gomp_set_nest_lock_30 omp_set_nest_lock
# define gomp_unset_nest_lock_30 omp_unset_nest_lock
# define gomp_test_nest_lock_30 omp_test_nest_lock

在 OponMP 当中一个跟锁非常重要的文件就是 lock.c,现在查看一下他的源代码,你的疑惑就能够揭开了:

#include <string.h>
#include "libgomp.h"

/* The internal gomp_mutex_t and the external non-recursive omp_lock_t
   have the same form.  Re-use it.  */

void
gomp_init_lock_30 (omp_lock_t *lock)
{
  gomp_mutex_init (lock);
}

void
gomp_destroy_lock_30 (omp_lock_t *lock)
{
  gomp_mutex_destroy (lock);
}

void
gomp_set_lock_30 (omp_lock_t *lock)
{
  gomp_mutex_lock (lock);
}

void
gomp_unset_lock_30 (omp_lock_t *lock)
{
  gomp_mutex_unlock (lock);
}

int
gomp_test_lock_30 (omp_lock_t *lock)
{
  int oldval = 0;

  return __atomic_compare_exchange_n (lock, &oldval, 1, false,
				      MEMMODEL_ACQUIRE, MEMMODEL_RELAXED);
}

void
gomp_init_nest_lock_30 (omp_nest_lock_t *lock)
{
  memset (lock, '\0', sizeof (*lock));
}

void
gomp_destroy_nest_lock_30 (omp_nest_lock_t *lock)
{
}

void
gomp_set_nest_lock_30 (omp_nest_lock_t *lock)
{
  void *me = gomp_icv (true);

  if (lock->owner != me)
    {
      gomp_mutex_lock (&lock->lock);
      lock->owner = me;
    }

  lock->count++;
}

void
gomp_unset_nest_lock_30 (omp_nest_lock_t *lock)
{
  if (--lock->count == 0)
    {
      lock->owner = NULL;
      gomp_mutex_unlock (&lock->lock);
    }
}

int
gomp_test_nest_lock_30 (omp_nest_lock_t *lock)
{
  void *me = gomp_icv (true);
  int oldval;

  if (lock->owner == me)
    return ++lock->count;

  oldval = 0;
  if (__atomic_compare_exchange_n (&lock->lock, &oldval, 1, false,
				   MEMMODEL_ACQUIRE, MEMMODEL_RELAXED))
    {
      lock->owner = me;
      lock->count = 1;
      return 1;
    }

  return 0;
}

总结

在本篇文章当中主要给大家分析了 OpenMP 当中两种主要的锁的实现,分别是 omp_lock_t 和 omp_nest_lock_t,一种是简单的锁实现,另外一种是可重入锁的实现。其实 critical 子句在 OpenMP 内部的也是利用上面的锁实现的。整个锁的实现还是非常复杂的,里面有很多耐人寻味的细节,这些代码真的很值得一读,看看能操刀 OpenMP Runtime Library 这些编程大师的作品,真的很有收获。

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

关注公众号:一无是处的研究僧,了解更多计算机(Java、Python、计算机系统基础、算法与数据结构)知识。

本文转载于网络 如有侵权请联系删除

相关文章

  • think in java一_Think in Java(一):Java基础「建议收藏」

    大家好,又见面了,我是你们的朋友全栈君。一.OOP的特点(1)万物皆为对象;(2)程序是对象的集合,他们通过发送信息来告诉彼此所要做的;(3)每个对象都有自己的由其他对象所构成的存储;(4)每个对象都拥有它的类型;(5)某一特定类型的对象都可以接收同样的消息;二.Java比C++简单?(1)Java有垃圾回收器,不用手动销毁对象;(2)Java使用单根继承;(3)Java只能以一种方式创建对象(在堆上创建);三.数据存储(1)寄存器:速度最快,数量有限,它位于CPU内部,但我们不能直接控制它。在C,C++中允许你向编译器建议寄存器的分配方式。(2)堆栈:存放基本类型的数据和对象的引用,但对象本身不存放在栈中,而是存放在堆中。(3)堆:存放用new产生的对象,速度比分配栈要慢一些。(4)静态存储:固定的一块存储区域,静态存储里面的数据在整个程序运行期间都能访问到。(5)非RAM:对象被转化成某种能保存在其他介质上的东西,要用的时候又能在内存里重建。四.“==”和equals(1)==用于判断引用对象的内存地址是否相同。(2)equals比较的也是地址,但是如果你重写了equals方法,那

  • 从零到一,构建你的持续交付流程(一):一个持续交付流程的构思

    在编码过程中,一个很容易发现的现象就是:经常依赖手工操作的过程,一定容易出错,而且是反复出错这就是持续交付出现的原因。持续交付的目标就是从代码编译到可部署的二进包,甚至是部署这个很多都是依赖手工操作的过程自动化,流水线化。微言码道推出国庆特别系列:从零到一,构建你的持续交付流程这是第一篇。一)稍微观察下,你所参与的项目,从编码到部署服务,这个过程是如何推动的?估计方式有挺多,但总体来说,可以分为两种方式,一种是依赖人工手动操作,还有一种是自动。手工的方式当然可能多种多样,有些可能还会有非常规范的流程约束,但最终仍然避免不了由某个特定的开发或测试或运营人员,以远程登录到服务器的方式,执行某些命令,来达到部署新版本的目标。这个过程是不是非常熟悉?当然,因为这就是大多数项目的日常而已。而持续交付则主张:把从源码编译开始,直至部署,甚至部署后的必要验证等全部由计算机来执行,将这个过程流水线化,自动化二)所以,我们需要开始思索一个问题,究竟哪些过程应该或者是可以放到持续交付中。构思一:将二进制包的构建过程自动化这种是最初级的,也是大多数持续交付应该最起码要达到的目标。触发构建-->编译源码

  • eeglab教程系列(12)-学习和删除ICA组件

    研究组件属性的操作:Tools>RejectdatausingICA>Rejectcomponentsbymap.操作过程如下:出现如下界面后,点击"OK"即可。 生成的3D图形与先前的2D头皮贴图之间的区别在于,这里可以通过单击每个组件头皮图上方的矩形按钮来绘制每个组件的属性。点击某个组件下的矩形,会出现如下界面。在如下界面中可以查看一些眼部伪影。也可以在组件erpimage.m(右上面板)中看到个别的眼球运动。眼睛伪影(几乎)总是出现在脑电图数据集中。它们通常处于成分阵列的领先位置(因为它们往往很大)。SubtractingICAcomponentsfromdata通常我们(在sccn)不会从数据集中减去整个独立的组件过程,因为通常研究单个组件(而不是总头皮通道)的活动。但是,如果要删除组件的话,操作如下:Tools>Removecomponents点击后会出现如下界面:在顶部文本框中输入要移除的通道,比如1、2和3. 点击OK后,出现如下界面,点击Accept. 点击Accept后,出现如下界面,点击OK即可: 点击OK后,重新绘制2-DCo

  • AMP自定义样式【ytkah英译AMP-3】

    AMP页面是网页;页面及其元素的任何样式都是使用常见的CSS属性完成的。在<head>中嵌入的样式表中使用类或元素选择器的样式元素,<styleamp-custom></style>,如下代码演示,注意:amp禁止引入除字体外的css文件<linkrel=”stylesheet”><styleamp-custom> /*anycustomstylegoeshere*/ body{ background-color:white; } amp-img{ background-color:gray; border:1pxsolidblack; } </style>复制  每个AMP页面只能有一个单一的嵌入样式表和内联样式,但有些选择器你不允许使用:  不允许使用和引用!important。这是使AMP能够执行其元素大小调整规则的必要要求。除了自定义字体不允许使用<linkrel=”stylesheet”>验证器不允许使用含i-amphtml-标记的名称。这些是AMP框架内部保留使用的。因此,用户的样式表不能引

  • 腾讯云云HDFS批量删除权限规则api接口

    1.接口描述接口请求域名:chdfs.tencentcloudapi.com。 批量删除权限规则。 默认接口请求频率限制:20次/秒。 APIExplorer提供了在线调用、签名验证、SDK代码生成和快速检索接口等能力。您可查看每次调用的请求内容和返回结果以及自动生成SDK调用示例。 2.输入参数以下请求参数列表仅列出了接口请求参数和部分公共参数,完整公共参数列表见公共请求参数。 参数名称 必选 类型 描述 Action 是 String 公共参数,本接口取值:DeleteAccessRules。 Version 是 String 公共参数,本接口取值:2020-11-12。 Region 是 String 公共参数,详见产品支持的地域列表。 AccessRuleIds.N 是 ArrayofInteger 多个权限规则ID,上限为10 3.输出参数 参数名称 类型 描述 RequestId String 唯一请求ID,每次请求都会返回。定位问题时需要提供该次请求的RequestId。 4.示例示例1批量删除权限规则批量删除

  • 五二不休息,今天也学习,从JS执行栈角度图解递归以及二叉树的前、中、后遍历的底层差异

    壹❀引 想必凡是接触过二叉树算法的同学,在刚上手那会,一定都经历过题目无从下手,甚至连题解都看不懂的痛苦。由于leetcode不方便调试,题目做错了也不知道错在哪里,最后无奈的cv答案后心里还不断安慰自己。不甘心想着要不直接背模板吧,可当天一知半解的记住了,不到半个月回头面对一道曾做过的简单二叉树题,脑袋里跟看一道新题一样。 那么二叉树对于我这个不是计算机专业的人来说难在哪呢?第一,我始终无法在脑中构建递归的过程,就像我的思维空间不足以支撑递归在我的脑中运行,大致脑补了两步就直接乱套了,我想象不出这个过程。 第二,对于二叉树的前中后层序遍历始终是一知半解,因为不理解递归到底怎么运行的,所以心里其实不知道它们三者到底差异在哪,为啥arr.push(root.val)写的地方不同,就实现了三种不同遍历方式?小小的脑袋里留下了大大的疑惑。 consttraverse=(root)=>{ if(root===null){ return; }; //前序遍历 traverse(root.left); //中序遍历 traverse(root.right); //后序遍历 } 复制 那么本

  • Java实现第十一届蓝桥杯 C组 省赛真题

      试题A:指数计算 试题B:解密 试题C:跑步训练 试题D:合并检测 试题E:REPEAT程序 试题F:分类计数 试题G:整除序列 试题H:走方格 试题I:字符串编码 试题J:整数小拼接   试题A:指数计算 本题总分:5分 【问题描述】 7月1日是建党日,从1921年到2020年,已经带领中国人民 走过了99年。 请计算:7^2020mod1921,其中AmodB表示A除以B的余数。 【答案提交】 这是一道结果填空题,你只需要算出结果后提交即可。本题的结果为一个 整数,在提交答案时只填写这个整数,填写多余的内容将无法得分。 package第十一届蓝桥杯; importjava.math.BigInteger; publicclassA指数计算{ publicstaticvoidmain(String[]args){ BigIntegernum1=newBigInteger("7"); BigIntegernum2=newBigInteger("1921"); BigIntegernum3=num1.pow(2020).remainder(num2);

  • ML科普系列(三)监督学习和无监督学习

    概述 在机器学习领域,主要有三类不同的学习方法: 监督学习(Supervisedlearning) 非监督学习(Unsupervisedlearning) 半监督学习(Semi-supervisedlearning) 定义 监督学习:通过已有的一部分输入数据与输出数据之间的对应关系,生成一个函数,将输入映射到合适的输出,例如分类。 非监督学习:直接对输入数据集进行建模,例如聚类。 半监督学习:综合利用有类标的数据和没有类标的数据,来生成合适的分类函数。 区别 是否有监督(supervised),就看输入数据是否有标签(label)。输入数据有标签,则为有监督学习,没标签则为无监督学习。 标签,简而言之,就是样本的分类标签,是不重合的,比如男/女,价值/非价值。 举个例子,判断一支股票是价值型还是非价值型。我们有三只股票: 现在要做一个分类系统,很显然,那就是如果PE-Ratio(市盈率)大于3,就是价值型股票。 分类系统做好了,现在新来一支股票,PE-Ratio是4,系统判断4>3,那就是价值股票。 监督学习 监督学习最常见的是分类问题,因为目标往往是让计算机去学习已经创建

  • iOS 使用 socket 即时通信(非第三方库)

    其实写这个socket一开始我是拒绝的。 因为大家学C语言和linux基础时肯定都有接触,客户端和服务端的通信也都了解过,加上现在很多开放的第三方库都不需要我们来操作底层的通信。 但是来了!!! 但是!还是想写。底层的东西最好了解下。 好了正经了!!!! 效果 由于5M的上传限制GIF可能看不清我再截两张图吧 模型图 做了个逗比模型图️ 分析 由上图可以了解到服务器和客户端需要做哪些工作 服务器 抽象一点分为: 1.创建线程等待接收客户端的连接 2.接收并解析客户端发来的消息 3.给客户端发送消息 具体一点: 1.创建socket.绑定端口.开始监听. 2.创建线程.等待接收客户端连接. 3.接收客户端发来的消息 4.解析消息内容 a.设置用户名 b.发送消息给指定客户端 客户端 抽象一点分为: 1.连接服务器 2.给服务器发送消息 3.接收服务器消息 4.解析消息内容 具体一点: 1.创建socket.绑定端口.连接服务器 2.发送消息 a.设置用户名 b.给指定用户发消息:按服务器格式拼接字符串 3.接收消息 a.普通消息 b.用

  • (笔记)(5)AMCL包源码分析 | 粒子滤波器模型与pf文件夹(一)

      粒子滤波器这部分内容涉及到的理论和数据结构比较多,我们分好几讲来介绍。这一讲的内容是对pf文件夹的简要分析,蒙特卡罗定位在pf中的代码具体实现,KLD采样算法的理论介绍以及它在pf中的代码具体实现。 1.pf文件夹头文件简要分析 说到AMCL包下的pf文件夹,它其实就是由这几部分组成:一个3✖3对称矩阵的特征值和特征向量的分解,定义一个kdtree以及维护方法来管理所有的粒子(粒子在这里表征位姿和权重),给定Gaussian模型以及概率密度模型采样生成粒子,定义三维列向量,三维矩阵以及实现pose的向量的加减乘除,局部到全局坐标的转换以及全局坐标到局部坐标的转换。 图1.AMCL包pf文件夹展开 接下来,是各个头文件的简要分析。 图2.pf包头文件简要分析 2.粒子滤波器与蒙特卡罗定位 2.1蒙特卡罗定位算法 什么是粒子滤波器呢?AMCL定位的理论基础就是基于滤波方法,是属于粒子滤波的一种。关于粒子滤波的原理以及代码效果演示,可以转到这里查阅一下。 2.MonteCarloLocalization|基础原理篇+配备代码讲解12赞同·3评论文章 2.2改进的蒙特卡洛算法:A

  • Bayesian RL and PGMRL

    简介: PGMRL:PGMRL就是把RL问题建模成一个概率图模型,如下图所示:   然后通过variationalinference的方法进行学习: PGMRL给RL问题的表示给了一个范例,对解决很多RL新问题提供了一种思路和工具。   BayesianRL:主要是对RL的rewardfunction,transationfunction引入uncertainty,引入prior和更新posterior来建模,从而更好地进行探索。   思考:为什么PGMRL推导过程中没有BeyesianRL的exploration-exploitationtrade-off的问题。 简单的PGMRL建模的reward和transation是确定的,没有超参数的。在某种程度下,比如问题是凸的情况下,是不需要进行exploration的。而BeyesianRL的问题设定是假设这些东西是一种概率分布,而不是确定性的。而BeyesionRL对这种不确定性的处理恰巧克服了RL问题不是凸的情况localoptimal的减弱。   thinking:whatthingsdo

  • 冲刺博客Day4

    DAY4 1.会议照片 2.工作详情 成员 昨天已完成的工作 今天计划完成的工作 工作中遇到的困难 周讯超 接上后台登录接口进行测试 完成随心贴模块 无 林佳浩 图片模块后端开发 随心贴模块代码的完成 暂时没有 黄欣茵 了解前后端交互 实现背景图片接口 用户界面不美观,没有吸引力 江男辉 模块基本设计完成 学习新功能所需要的编码 需要对前端有一点的了解 夏依达 小组计划交互以及计划发布功能优化 细节优化、测试完成 在开发过程中,算法是一个难题。因为文章推荐算法没有达到预期的效果。 阿卜杜乃比 小组计划交互以及计划发布功能优化 细节优化、测试完成 因为文章推荐算法没有达到预期的效果。 3.燃尽图 4.签入记录 5.主要代码截图 点击查看代码 //1.插入随心贴 @PostMapping("/add") publicReturnResultinsertMlog(@RequestBodyMlogmlog){ IntegermlogId=mlogService.insertMlog(mlog); if(mlogId!=null&&

  • 垃圾回收机制算法分析

                    垃圾回收机制算法分析 什么是垃圾回收机制           不定时去堆内存中清理不可达对象。不可达的对象并不会马上就会直接回收,垃圾收集器在一个Java程序中的执行是自动的,不能强制执行,即使程序员能明确地判断出有一块内存已经无用了,是应该回收的,程序员也不能强制垃圾收集器回收该内存块。程序员唯一能做的就是通过调用System.gc方法来"建议"执行垃圾收集器,但其是否可以执行,什么时候执行却都是不可知的。这也是垃圾收集器的最主要的缺点。当然相对于它给程序员带来的巨大方便性而言,这个缺点是瑕不掩瑜的   如何判断对象是否存活   引用计数法 (淘汰)     引用计数法就是如果一个对象没有被任何引用指向,则可视之为垃圾。这种方法的缺点就是不能检测到环的存在。     首先需要声明,至少主流的Java虚拟机里面都没有选用引用计数算法来管理内存。      什么是引用计数算法:给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值加1;当引用失效时,计数器值减1.任何时刻计数器值为0的对象就是不可能再被使用的。     那为什么主

  • 自动化测试框架相关资料下载

    自动化测试框架相关资料下载http://automationqa.com/forum.php?mod=viewthread&tid=2565&fromuid=29 自动化测试资讯平台http://www.AutomationQA.com

  • Redis安装手册

    wgethttp://download.redis.io/releases/redis-2.8.3.tar.gz tarxzfredis-2.8.3.tar.gz cdredis-2.8.3 make 2、编译完成后,在./Src目录下,有三个可执行文件redis-server、redis-benchmark、redis-cli和./redis.conf然后拷贝到一个目录下。   mkdir/usr/redis cpredis-server /usr/redis cpredis-benchmark/usr/redis cpredis-cli /usr/redis cpredis.conf /usr/redis cd/usr/redis 3、启动Redis服务。   redis-server  redis.conf 4、然后用客户端测试一下是否启动成功。   redis-cli redis>setfoobar OK redis>getfoo "bar"   ---------------

  • 设置linux查看历史命令显示执行时间

    1、以ROOT用户编辑/etc/profile文件,在里面加入下面内容   注意:在末尾的“引号”与“S”之间,加入一位空格,将日期时间和历史命令用空格相隔开来。   2.执行 source/etc/profile 使环境变量立即生效,或者reboot重启系统也能达到相同效果,然后查看效果    

  • 继承和接口课后作业

    动手动脑及验证: 一、 TestInherits.java实例运行结果及结论: 代码:class Grandparent  {        public Grandparent()  {              System.out.println("GrandParent Created.");       }        public Grandparent(String string)   {                 System.out.println("GrandParent&nb

  • 1004 成绩排名

    读入n(>0)名学生的姓名、学号、成绩,分别输出成绩最高和成绩最低学生的姓名和学号。 输入格式: 每个测试输入包含1个测试用例,格式为 第1行:正整数n 第2行:第1个学生的姓名学号成绩 第3行:第2个学生的姓名学号成绩 ......... 第n+1行:第n个学生的姓名学号成绩 复制 其中姓名和学号均为不超过10个字符的字符串,成绩为0到100之间的一个整数,这里保证在一组测试用例中没有两个学生的成绩是相同的。 输出格式: 对每个测试用例输出2行,第1行是成绩最高学生的姓名和学号,第2行是成绩最低学生的姓名和学号,字符串间有1空格。 输入样例: 3 JoeMath99011289 MikeCS991301100 MaryEE99083095 复制 输出样例: MikeCS991301 JoeMath990112 复制 代码 //b1004-成绩排名.cpp:此文件包含"main"函数。程序执行将在此处开始并结束。 // #include<iostream> usingnamespacestd; intmain() { intn; cin>&g

  • 【工作心得】新晋TL的思考

    从17年3月份开始吧,开始慢慢承担一些团队管理的事情,到现在带着10人的技术团队,承担整个APP系统组的开发工作。 从最开始的手忙脚乱,到现在慢慢地开始游刃有余起来。这期间经历了很多,也学习了很多,感悟了很多。 在开发团队管理上,有以下几条粗浅的感悟: (1)管理自己是一件很有深度的事情,能够有效地管理一个小的团队更加是一件有深度的事情; (2)健壮地实现一个复杂的特性是一件有挑战性的事情,能够将一个抽象的需求,分解为一个个具体的特性,并且能够为每个特性提供较为合适的解决方案,是一件更有挑战性的事情; (3)一个团队里面需要披坚执锐的人,也需要愿意承担脏活累活的人。能够精确地识别每个兄弟的属性,分配合适的任务,并能够给出恰当的指导,是一件很有学问的事情; (4)一个合适的TeamLeader,比想象中要更加有技术含量: 要有较高的智商和情商; 要有宽阔的技术视野; 要在技术的深度和广度上具有权威性; 要能够精确把握每个需求的优先级; 能够准确的预估需求的工作量; 能够给出需求的具体的需求分析,需求分解,以及具体的设计方案和开发方案; 能够高质量的实现核心的代码; 能够安排合适的人来进

  • springframwork历史版本下载地址

    http://sourceforge.net/projects/springframework/files/springframework-2/

  • 【maven系列】:maven构建模块化项目之SpringMVC整合Mybatis (01)

    一、整合所需jar包 <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>${spring-version}</version> </dependency> <!--整合mybatis所需jar包--> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>${mybatis.spring-version}</version> </dependency> <!--mybatis--> <dependency> <groupId>org.mybatis</groupI

相关推荐

推荐阅读