Concurrency 通过更改基址快速同步访问共享阵列(在C11中)

Concurrency 通过更改基址快速同步访问共享阵列(在C11中),concurrency,synchronization,scheduling,c11,stdatomic,Concurrency,Synchronization,Scheduling,C11,Stdatomic,我目前正在为Linux下的定制协处理器设计C11中的用户空间调度器(用户空间,因为协处理器不运行自己的操作系统,而是由主机CPU上运行的软件控制)。它使用数组跟踪所有任务的状态。在这种情况下,任务状态是正则整数。阵列是动态分配的,每次提交状态不再适合阵列的新任务时,阵列将重新分配到其当前大小的两倍。调度程序使用多个线程,因此需要同步其数据结构 现在的问题是,我经常需要读取该数组中的条目,因为我需要知道任务的状态,以便进行调度决策和资源管理。如果保证在每次重新分配后基址始终相同,我只需使用C11原

我目前正在为Linux下的定制协处理器设计C11中的用户空间调度器(用户空间,因为协处理器不运行自己的操作系统,而是由主机CPU上运行的软件控制)。它使用数组跟踪所有任务的状态。在这种情况下,任务状态是正则整数。阵列是动态分配的,每次提交状态不再适合阵列的新任务时,阵列将重新分配到其当前大小的两倍。调度程序使用多个线程,因此需要同步其数据结构

现在的问题是,我经常需要读取该数组中的条目,因为我需要知道任务的状态,以便进行调度决策和资源管理。如果保证在每次重新分配后基址始终相同,我只需使用C11原子访问它。不幸的是,realloc显然不能提供这样的保证。因此,我目前的方法是以pthread互斥体的形式,用一个大锁包装每个访问(读写)。显然,这是非常缓慢的,因为每次读取都有锁定开销,而且读取非常小,因为它只包含一个整数

为了澄清问题,我在这里给出一些代码,显示相关段落:

写作:

// pthread_mutex_t mut;
// size_t len_arr;
// int *array, idx, x;
pthread_mutex_lock(&mut);
if (idx >= len_arr) {
    len_arr *= 2;
    array = realloc(array, len_arr*sizeof(int));
    if (array == NULL)
        abort();
}
array[idx] = x;
pthread_mutex_unlock(&mut);
if (idx == len) {
  wait = true;
  while (cnt > 0);  // busy wait to minimize latency of reallocation
  array = realloc(array, 2*len*sizeof(int));
  if (!array) abort();  // shit happens
  len *= 2;  // must not be updated before reallocation completed
  wait = false;
}

// this is why len must be updated after realloc,
// it serves for synchronization with other writers
// exceeding the current length limit
while (idx > len) {yield();}

while(true) {
  cnt++;
  if (wait) {
    cnt--;
    yield();
  } else {
    break;
  }
}
array[idx] = x;
cnt--;
阅读:

// pthread_mutex_t mut;
// int *array, idx;
pthread_mutex_lock(&mut);
int x = array[idx];
pthread_mutex_unlock(&mut);
while(true) {
  cnt++;
  if (wait) {
    cnt--;
    yield();
  } else {
    break;
  }
}
int x = array[idx];
cnt--;
我已经在实现中的其他地方使用了C11原子来实现高效的同步,我也希望使用它们来解决这个问题,但是我找不到一种有效的方法来实现。在一个完美的世界中,将有一个用于阵列的原子存取器,它在单个原子操作中执行地址计算和内存读/写。不幸的是,我找不到这样的手术。但是,在这种情况下,也许有一种类似的快速甚至更快的方法来实现同步

编辑:
我忘记指定任务终止时不能重用阵列中的插槽。由于我保证可以访问自调度程序启动以来提交的每个任务的状态,因此我需要存储每个任务的最终状态,直到应用程序终止。因此,静态分配并不是一个真正的选择。

您是否需要对虚拟地址空间如此经济?您不能设置一个非常大的上限,并为它分配足够的地址空间(甚至可以是一个静态数组,如果您希望在启动时从命令行选项设置上限,则可以是动态数组)

Linux执行延迟内存分配,因此您从未接触过的虚拟页面实际上不使用任何物理内存。请参见通过示例演示首次读取或写入匿名页面会导致页面错误。如果是读访问,它会让内核将其映射到一个共享的物理零页。只有初始写入或对CoW页的写入才会触发物理页的实际分配

让虚拟页面完全保持不变,甚至可以避免将它们连接到硬件页面表的开销

如果您的目标是像x86-64这样的64位ISA,那么您有大量的虚拟地址空间。使用更多的虚拟地址空间(只要不浪费物理页面)基本上是可以的


分配超过您所能使用的地址虚拟空间的实际示例: 如果您分配的内存超过了实际使用的内存量(触摸所有内存肯定会导致segfault或调用内核的OOM killer),那么它将与通过
realloc
增长的内存一样大或更大

要分配这么多,您可能需要将
/proc/sys/vm/overmit_memory
全局设置为1(无需检查),而不是默认的
0
(启发式操作会导致极大的分配失败)。或者用于分配它,使其映射为页面错误上的最大努力增长

文档中说,您可能会在触摸内存上获得一个
SIGSEGV
,该内存分配有
MAP\u NORESERVE
,这与调用OOM killer不同。但我认为,一旦你成功地触动了记忆,它就是你的,不会被丢弃。我认为它也不会错误地失败,除非你真的耗尽了RAM+交换空间。IDK您计划如何在当前的设计中检测到这一点(如果您无法取消分配,这听起来相当粗略)

测试程序:

#include <stdlib.h>
#include <stdio.h>
#include <sys/mman.h>

int main(void) {
        size_t sz = 1ULL << 46;   // 2**46 = 64 TiB = max power of 2 for x86-64 with 48-bit virtual addresses
                                  // in practice  1ULL << 40  (1TiB) should be more than enough.
        // the smaller you pick, the less impact if multiple things use this trick in the same program

        //int *p = aligned_alloc(64, sz); // doesn't use NORESERVE so it will be limited by overcommit settings

        int *p = mmap(NULL, sz, PROT_WRITE|PROT_READ, MAP_PRIVATE|MAP_ANONYMOUS|MAP_NORESERVE, -1, 0);
        madvise(p, sz, MADV_HUGEPAGE);       // for good measure to reduce page-faults and TLB misses, since you're using large contiguous chunks of this array

        p[1000000000] = 1234;    // or sz/sizeof(int) - 1 will also work; this is only touching 1 page somewhere in the array.
        printf("%p\n", p);
}
我的桌面有16GiB的RAM(Chromium使用了很多RAM,在/tmp中有一些大文件)+2Gb的交换空间。然而,这个程序分配了64个TiB的虚拟地址空间,并且几乎立即触及了其中的1
int
。如果它只分配了1MiB,则速度不会明显减慢。(实际使用该内存的未来性能也不会受到影响。)


在当前的x86-64硬件上,最大的2次幂是
1all,因此,我提出了一个解决方案:

阅读:

// pthread_mutex_t mut;
// int *array, idx;
pthread_mutex_lock(&mut);
int x = array[idx];
pthread_mutex_unlock(&mut);
while(true) {
  cnt++;
  if (wait) {
    cnt--;
    yield();
  } else {
    break;
  }
}
int x = array[idx];
cnt--;
写作:

// pthread_mutex_t mut;
// size_t len_arr;
// int *array, idx, x;
pthread_mutex_lock(&mut);
if (idx >= len_arr) {
    len_arr *= 2;
    array = realloc(array, len_arr*sizeof(int));
    if (array == NULL)
        abort();
}
array[idx] = x;
pthread_mutex_unlock(&mut);
if (idx == len) {
  wait = true;
  while (cnt > 0);  // busy wait to minimize latency of reallocation
  array = realloc(array, 2*len*sizeof(int));
  if (!array) abort();  // shit happens
  len *= 2;  // must not be updated before reallocation completed
  wait = false;
}

// this is why len must be updated after realloc,
// it serves for synchronization with other writers
// exceeding the current length limit
while (idx > len) {yield();}

while(true) {
  cnt++;
  if (wait) {
    cnt--;
    yield();
  } else {
    break;
  }
}
array[idx] = x;
cnt--;
wait
是初始化为false的原子布尔,
cnt
是初始化为零的原子int。 这只会起作用,因为我知道任务ID是在没有间隙的情况下递增选择的,并且在写入操作初始化任务之前,不会读取任何任务状态。因此,我可以始终依赖于一个线程,该线程提取的ID仅超过当前数组长度1。并发创建的新任务将阻塞其线程,直到负责的线程执行重新分配。因此需要繁忙的等待,因为重新分配应该很快发生,所以其他线程不必等待太长时间

这样,我消除了瓶颈大锁。阵列访问可以同时进行,代价是添加两个原子。由于重新分配很少发生(由于指数增长),阵列访问实际上是无块的

编辑: 再看一眼之后,我注意到,在整个长度范围内重新排序商店时,必须小心