无锁堆栈实现想法 - 目前已损坏

2023-12-23

我想出了一个想法,尝试实现一个无锁堆栈,该堆栈不依赖引用计数来解决 ABA 问题,并且还可以正确处理内存回收。它在概念上与 RCU 类似,并且依赖于两个功能:将列表条目标记为已删除,以及跟踪遍历列表的读者。前者很简单,它只使用指针的LSB。后者是我对实现无限无锁堆栈的方法的“聪明”尝试。

基本上,当任何线程尝试遍历列表时,一个原子计数器 (list.entries) 就会增加。当遍历完成时,第二个计数器(list.exits)会递增。

节点分配由push 处理,释放由pop 处理。

推入和弹出操作与简单的无锁堆栈实现非常相似,但是必须遍历标记为删除的节点才能到达未标记的条目。因此,推送基本上很像链表插入。

pop 操作类似地遍历列表,但它在遍历时使用atomic_fetch_or 将节点标记为已删除,直到到达未标记的节点。

在遍历完 0 个或多个标记节点的列表后,正在弹出的线程将尝试 CAS 栈头。至少有一个线程同时弹出将成功,此后所有进入堆栈的读者将不再看到以前标记的节点。

成功更新列表的线程然后加载原子list.entries,并且基本上旋转加载atomic.exits,直到该计数器最终超过list.entries。这应该意味着“旧”版本列表的所有读者都已完成。然后,线程简单地释放它从列表顶部交换的标记节点列表。

因此,pop 操作的含义应该是(我认为)不会出现 ABA 问题,因为被释放的节点不会返回到可用的指针池,直到所有使用它们的并发读取器完成为止,显然还有内存回收问题出于同样的原因,也进行了处理。

无论如何,这是理论,但我仍然对实现摸不着头脑,因为它目前不起作用(在多线程情况下)。似乎我在其他问题中遇到了一些免费写入后的问题,但我很难发现问题,或者也许我的假设有缺陷,它就是行不通。

任何有关概念和调试代码的方法的见解都将不胜感激。

这是我当前的(损坏的)代码(使用 gcc -D_GNU_SOURCE -std=c11 -Wall -O0 -g -pthread -o list list.c 编译):

#include <pthread.h>
#include <stdatomic.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdlib.h>

#include <sys/resource.h>

#include <stdio.h>
#include <unistd.h>

#define NUM_THREADS 8
#define NUM_OPS (1024 * 1024)

typedef uint64_t list_data_t;

typedef struct list_node_t {
    struct list_node_t * _Atomic next;
    list_data_t data;
} list_node_t;

typedef struct {
    list_node_t * _Atomic head;
    int64_t _Atomic size;
    uint64_t _Atomic entries;
    uint64_t _Atomic exits;
} list_t;

enum {
    NODE_IDLE    = (0x0),
    NODE_REMOVED = (0x1 << 0),
    NODE_FREED   = (0x1 << 1),
    NODE_FLAGS    = (0x3),
};

static __thread struct {
    uint64_t add_count;
    uint64_t remove_count;
    uint64_t added;
    uint64_t removed;
    uint64_t mallocd;
    uint64_t freed;
} stats;

#define NODE_IS_SET(p, f) (((uintptr_t)p & f) == f)
#define NODE_SET_FLAG(p, f) ((void *)((uintptr_t)p | f))
#define NODE_CLR_FLAG(p, f) ((void *)((uintptr_t)p & ~f))
#define NODE_POINTER(p) ((void *)((uintptr_t)p & ~NODE_FLAGS))

list_node_t * list_node_new(list_data_t data)
{
    list_node_t * new = malloc(sizeof(*new));
    new->data = data;
    stats.mallocd++;

    return new;
}

void list_node_free(list_node_t * node)
{
    free(node);
    stats.freed++;
}

static void list_add(list_t * list, list_data_t data)
{
    atomic_fetch_add_explicit(&list->entries, 1, memory_order_seq_cst);

    list_node_t * new = list_node_new(data);
    list_node_t * _Atomic * next = &list->head;
    list_node_t * current = atomic_load_explicit(next,  memory_order_seq_cst);
    do
    {
        stats.add_count++;
        while ((NODE_POINTER(current) != NULL) &&
                NODE_IS_SET(current, NODE_REMOVED))
        {
                stats.add_count++;
                current = NODE_POINTER(current);
                next = &current->next;
                current = atomic_load_explicit(next, memory_order_seq_cst);
        }
        atomic_store_explicit(&new->next, current, memory_order_seq_cst);
    }
    while(!atomic_compare_exchange_weak_explicit(
            next, &current, new,
            memory_order_seq_cst, memory_order_seq_cst));

    atomic_fetch_add_explicit(&list->exits, 1, memory_order_seq_cst);
    atomic_fetch_add_explicit(&list->size, 1, memory_order_seq_cst);
    stats.added++;
}

static bool list_remove(list_t * list, list_data_t * pData)
{
    uint64_t entries = atomic_fetch_add_explicit(
            &list->entries, 1, memory_order_seq_cst);

    list_node_t * start = atomic_fetch_or_explicit(
            &list->head, NODE_REMOVED, memory_order_seq_cst);
    list_node_t * current = start;

    stats.remove_count++;
    while ((NODE_POINTER(current) != NULL) &&
            NODE_IS_SET(current, NODE_REMOVED))
    {
        stats.remove_count++;
        current = NODE_POINTER(current);
        current = atomic_fetch_or_explicit(&current->next,
                NODE_REMOVED, memory_order_seq_cst);
    }

    uint64_t exits = atomic_fetch_add_explicit(
            &list->exits, 1, memory_order_seq_cst) + 1;

    bool result = false;
    current = NODE_POINTER(current);
    if (current != NULL)
    {
        result = true;
        *pData = current->data;

        current = atomic_load_explicit(
                &current->next, memory_order_seq_cst);

        atomic_fetch_add_explicit(&list->size,
                -1, memory_order_seq_cst);

        stats.removed++;
    }

    start = NODE_SET_FLAG(start, NODE_REMOVED);
    if (atomic_compare_exchange_strong_explicit(
            &list->head, &start, current,
            memory_order_seq_cst, memory_order_seq_cst))
    {
        entries = atomic_load_explicit(&list->entries, memory_order_seq_cst);
        while ((int64_t)(entries - exits) > 0)
        {
            pthread_yield();
            exits = atomic_load_explicit(&list->exits, memory_order_seq_cst);
        }

        list_node_t * end = NODE_POINTER(current);
        list_node_t * current = NODE_POINTER(start);
        while (current != end)
        {
            list_node_t * tmp = current;
            current = atomic_load_explicit(&current->next, memory_order_seq_cst);
            list_node_free(tmp);
            current = NODE_POINTER(current);
        }
    }

    return result;
}

static list_t list;

pthread_mutex_t ioLock = PTHREAD_MUTEX_INITIALIZER;

void * thread_entry(void * arg)
{
    sleep(2);
    int id = *(int *)arg;

    for (int i = 0; i < NUM_OPS; i++)
    {
        bool insert = random() % 2;

        if (insert)
        {
            list_add(&list, i);
        }
        else
        {
            list_data_t data;
            list_remove(&list, &data);
        }
    }

    struct rusage u;
    getrusage(RUSAGE_THREAD, &u);

    pthread_mutex_lock(&ioLock);
    printf("Thread %d stats:\n", id);
    printf("\tadded = %lu\n", stats.added);
    printf("\tremoved = %lu\n", stats.removed);
    printf("\ttotal added = %ld\n", (int64_t)(stats.added - stats.removed));
    printf("\tadded count = %lu\n", stats.add_count);
    printf("\tremoved count = %lu\n", stats.remove_count);
    printf("\tadd average = %f\n", (float)stats.add_count / stats.added);
    printf("\tremove average = %f\n", (float)stats.remove_count / stats.removed);
    printf("\tmallocd = %lu\n", stats.mallocd);
    printf("\tfreed = %lu\n", stats.freed);
    printf("\ttotal mallocd = %ld\n", (int64_t)(stats.mallocd - stats.freed));
    printf("\tutime = %f\n", u.ru_utime.tv_sec
            + u.ru_utime.tv_usec / 1000000.0f);
    printf("\tstime = %f\n", u.ru_stime.tv_sec
                    + u.ru_stime.tv_usec / 1000000.0f);
    pthread_mutex_unlock(&ioLock);

    return NULL;
}

int main(int argc, char ** argv)
{
    struct {
            pthread_t thread;
            int id;
    }
    threads[NUM_THREADS];
    for (int i = 0; i < NUM_THREADS; i++)
    {
        threads[i].id = i;
        pthread_create(&threads[i].thread, NULL, thread_entry, &threads[i].id);
    }

    for (int i = 0; i < NUM_THREADS; i++)
    {
        pthread_join(threads[i].thread, NULL);
    }

    printf("Size = %ld\n", atomic_load(&list.size));

    uint32_t count = 0;

    list_data_t data;
    while(list_remove(&list, &data))
    {
        count++;
    }
    printf("Removed %u\n", count);
}

你提到你正在尝试解决 ABA 问题,但描述和代码实际上是试图解决一个更难的问题:内存回收 https://arxiv.org/abs/1712.06134问题。

这个问题通常出现在用没有垃圾回收的语言实现的无锁集合的“删除”功能中。核心问题是,从共享结构中删除节点的线程通常不知道何时可以安全地释放已删除的节点,因为其他读取可能仍然引用它。经常解决这个问题,作为一个副作用,also解决了 ABA 问题:具体来说,即使底层指针(和对象的状态)在此期间已被更改至少两次,最终还是原始指针,CAS 操作仍能成功value却呈现出完全不同的状态。

ABA 问题更容易,因为对于 ABA 问题有几种直接的解决方案,具体来说,这些解决方案不会导致“内存回收”问题的解决方案。从某种意义上说,可以检测位置修改的硬件(例如使用 LL/SC 或事务内存原语)可能根本不会出现问题,这也更容易。

也就是说,您正在寻找内存回收问题的解决方案,并且它还将避免 ABA 问题。

你的问题的核心是这样的说法:

成功更新列表的线程然后加载原子 list.entries,基本上旋转加载atomic.exits直到计数器 终于超过了list.entries。这应该意味着所有读者 列表的“旧”版本已经完成。线程然后简单地 释放它从顶部交换的标记节点列表 列表。

这个逻辑不成立。等待list.exits(你说原子退出但我认为这是一个错字,因为你只谈论list.exits其他地方)大于list.entries只告诉你现在已经有更多退出总数比有entries此时变异线程捕获了条目计数。然而,这些退出可能是由来来往往的新读者产生的:这根本不意味着所有老读者都读完了正如你所说!

这是一个简单的例子。首先是一个写作线程T1和一个阅读线程T2大约在同一时间访问该列表,所以list.entries是 2 并且list.exits为0。写入线程弹出一个节点,并保存当前值(2)list.entries并等待lists.exits大于 2。现在还有三个阅读线程,T3, T4, T5到达并快速阅读清单并离开。现在lists.exits是 3,并且满足您的条件并且T1释放节点。T2但它并没有去任何地方,并且因为它正在读取一个已释放的节点而爆炸!

你的基本想法是可行的,但你的两种反击方法绝对行不通。

这是一个经过充分研究的问题,因此您不必发明自己的算法(请参阅上面的链接),甚至不必编写自己的代码,因为诸如librcu https://liburcu.org/ and 并发工具包 https://github.com/concurrencykit/ck已经存在。

用于教育目的

If you wanted不过,为了使这项工作达到教育目的,一种方法是确保修改后进入的线程已开始使用一组不同的list.entry/exit柜台。一种方法是使用生成计数器,当编写者想要修改列表时,它会增加生成计数器,这会导致新的读者切换到另一组不同的列表。list.entry/exit柜台。

现在编剧只需要等待list.entry[old] == list.exists[old],这意味着所有的old读者已经离开。您也可以只使用每代一个计数器:您实际上并没有两个entry/exit计数器(尽管它可能有助于减少争用)。

当然,您知道管理每代单独计数器的列表有一个新问题......这看起来像构建无锁列表的原始问题!不过,这个问题要容易一些,因为您可能会对“飞行中”的代数设置一些合理的限制,然后将它们全部预先分配,或者您可能会实现一种更容易推理的有限类型的无锁列表因为添加和删除只发生在头部或尾部。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

无锁堆栈实现想法 - 目前已损坏 的相关文章

  • Unity3D StartCoroutine 调用一个函数,该函数什么时候返回?

    我知道Unity3D StartCoroutine调用了一个与StartCoroutine在同一线程上运行的函数 但是被调用的函数什么时候返回到原始调用者 我在互联网上查找了一个很好的 Unity3D Coroutine 示例 但找不到完整
  • Windows 上使用 g++ 的 Makefile,链接库

    我已经厌倦了 MSVC 6 以及每个人总是告诉我它是一个蹩脚的编译器等等 所以现在我决定尝试使用 vim 加 g 和 makefile 这是我的问题 我有以下 makefile This is supposed to be a commen
  • C修改printf()输出到文件

    有没有办法修改printf为了将字符串输出到文件而不是控制台 我尝试在互联网上查找一些内容 发现了类似的电话dup dup2 and fflush这可能与此有关 EDIT 也许我不清楚 问题是这是C考试问题 问题如下 解释一个通常将字符串输
  • 切换图片框可见性 C#

    为什么图片框控件的可见性属性在这里不起作用 我最初将它们设置为 false 以便在屏幕加载时它们不可见 但后来我想切换这个 我已完成以下操作 但似乎不起作用 这是一个 Windows 窗体应用程序 private void Action w
  • 为什么派生类不使用基类的operator=(赋值运算符)?

    以下是实际问题的简化版本 而不是打电话Base operator int 代码似乎生成了一个临时的Derived对象并复制它 既然函数签名似乎完美匹配 为什么不使用基本赋值运算符 这个简化的示例没有显示任何不良影响 但原始代码在析构函数中有
  • 如何将pdf页面设置设置为打印属性对话框?

    大家好 我想知道如何设置 pdf 页面设置到打印属性对话框 例如 如果我的 PDF 页面设置为横向 则布局会自动显示横向而不是纵向 如果我的 PDF 页面设置为纵向 则布局会自动显示纵向 我在这个主题上做了很多研发 但没有找到任何满意的链接
  • 关闭 XDOCUMENT 的实例

    我收到这个错误 该进程无法访问文件 C test Person xml 因为它是 被另一个进程使用 IOException 未处理 保存文件内容后如何关闭 xml 文件的实例 using System using System Collec
  • 如何使用汇编获取BIOS时间?

    我正在从头开始实现一个小型操作系统 用于教育目的 现在 我想使用汇编来获取 BIOS 时间 我对此进行了很多搜索 但找不到任何代码示例来执行此操作 如果有人可以提供任何参考或代码示例或与此相关的任何内容 我将非常感激 See 时钟中断 1a
  • 使用 openssl 检查服务器安全协议

    我有一个框架应用程序 它根据使用方式连接到不同的服务器 对于 https 连接 使用 openssl 我的问题是 我需要知道我连接的服务器是否使用 SSL 还是 TLS 以便我可以创建正确的 SSL 上下文 目前 如果我使用错误的上下文尝试
  • 导出到 CSV 时 Gridview 出现空行

    这个问题是由进一步讨论引发的这个问题 https stackoverflow com questions 6674555 export gridview data into csv file 6674589 noredirect 1 com
  • 如何使用泛型类型的 DataContractSerializer 编写自定义序列化器?

    我想编写一个自定义序列化器 用于将会话状态存储到Azure 缓存 预览版 这意味着这个自定义序列化器必须实现IDataCacheObjectSerializer 如果我错了 请告诉我 我需要编写这个自定义序列化程序的原因是我需要序列化一些包
  • 指示泛型返回动态类型的对象

    这个问题是我原来问题的后续问题here https stackoverflow com questions 2541184 using a type object to create a generic 假设我有以下泛型类 简化 class
  • 不要声明只读可变引用类型 - 为什么不呢?

    我一直在阅读这个问题 https stackoverflow com questions 2274412 immutable readonly reference types fxcop violation do not declare r
  • 如何不在类中实现接口的功能?

    面试时面试官问了我以下问题 但我不知道这个问题的答案是什么 请帮忙 如果我不想 我必须做什么 在我的类中实现一个函数 在接口中声明为 由我班实施 Edited 我正在使用 NET 和 C 如果有人可以提供 C 示例代码示例 那就太好了 Th
  • asp.net c# 防止在从服务器端代码更改索引时触发 selectedindexchanged 事件

    我在同一个 aspx 页面上有两个下拉列表控件
  • 为什么C语言中可以使用多个分号?

    在 C 中我可以执行以下操作 int main printf HELLO WORLD 它有效 这是为什么 我个人的想法 分号是一个 NO OPERATION 来自维基百科 指示符 拥有一大串分号与拥有一个分号并告诉 C 语句已结束具有相同的
  • 如何将 CSV 文件读入 .NET 数据表

    如何将 CSV 文件加载到System Data DataTable 根据CSV文件创建数据表 常规 ADO net 功能是否允许这样做 我一直在使用OleDb提供者 但是 如果您正在读取具有数值的行 但希望将它们视为文本 则会出现问题 但
  • 将一个 long 转换为两个 int 以进行重构

    我需要将一个参数作为两个 int 参数传递给 Telerik Report 因为它不能接受长参数 将 long 拆分为两个 int 并在不丢失数据的情况下重建它的最简单方法是什么 使用掩蔽和移位是最好的选择 根据文档 long 保证为 64
  • 通过 cmake 链接作为外部项目包含的 opencv 库[重复]

    这个问题在这里已经有答案了 我对 cmake 比较陌生 经过几天的努力无法弄清楚以下事情 我有一个依赖于 opencv 的项目 它本身就是一个 cmake 项目 我想静态链接 opencv 库 我正在做的是我的项目中有一份 opencv 源
  • 如何从函数返回矩阵(二维数组)? (C)

    我创建了一个生成宾果板的函数 我想返回宾果板 正如我没想到的那样 它不起作用 这是函数 int generateBoard int board N M i j fillNum Boolean exists True initilize se

随机推荐

  • 生成器不是迭代器吗?

    我有一个生成器 一个产生东西的函数 但是当试图将它传递给gensim Word2Vec我收到以下错误 类型错误 您不能将生成器作为句子参数传递 尝试迭代器 生成器不是迭代器的一种吗 如果没有 我如何从中创建一个迭代器 查看库代码 它似乎只是
  • 范围为“class”的 Pytest 装置不适用于“setup_class”方法

    我目前正在使用pytest addoption运行我的 API 测试 因此测试应该针对用户在命令行上使用的环境运行 在我的测试文件中 我试图实例化UsersSupport只上一次课 就通过了env争论 我的代码 测试 py import p
  • NSMutableData 的 mutableBytes 和 bytes 方法之间的区别

    两者都返回相同的指针 我知道 bytes属于NSData 为什么NSMutableData介绍 mutableBytes 是否只是为了代码清晰 以便更明显地访问可变数据 使用哪一个真的很重要吗 NSMutableData mydata NS
  • 使用ExternalProject_Add将Opus包含在Android中

    这可能很简单 我有一个使用 NDK 的 Android 项目 我想包括opus https opus codec org downloads 本机代码中的源代码 我尝试使用 CMake 的ExternalProject Add 属性 但我的
  • ASP.Net WebForms 路由 多个目的地的单一路由

    我正在考虑为我计划创建的新网站设置数据库路由 我一直在查看以下有关使用数据库中的FriendlyUrls 的教程 http www asp net web forms tutorials aspnet 45 getting started
  • 在 C 扩展中释放全局 VM 锁而不使用其他函数

    我不明白为什么在 Ruby C API 中释放或获取 GVL 时需要另一个间接级别 Both rb thread call without gvl and rb thread call with gvl 需要一个只接受一个参数的函数 但情况
  • DataTable 内的 Markdown 正在其周围添加段落

    当 DashTable 中使用 markdown 时 需要额外增加一个 p 添加标签使单元格和实习生所有行变大 import dash from dash import dash table app dash Dash app layout
  • 在三星上,Compose AlertDialog 始终采用全宽

    在我的配备 One UI 5 0 和 Android 13 的三星 Galaxy S22 上 撰写 AlertDialog 始终占据全宽 在其他设备上它的工作方式与预期一致 Compose版本是1 3 1 您只需下载即可重现此内容 我怀疑这
  • 为什么我的 WPF 应用程序禁用了拖放功能(即使当 AllowDrop 为 true 时)?

    我的 WPF 应用程序禁止从 Windows 资源管理器中删除文件 并显示停止标志光标 我尝试在主窗口和包含的控件上将AllowDrop属性 UIElement祖先的属性 设置为true 但完全没有运气 没有触发拖放事件 有什么想法或建议来
  • 区分具有未知功能的产品 - sympy

    我尝试了各种搜索 但找不到一个好的谷歌字符串来得出正确的结果 我有以下形式的产品 y x f x 其中 f 是 x 的未知函数 我希望 sympy 对 y 相对于 x 进行微分 有谁知道我该怎么做 怎么样 gt gt gt x sympy
  • jQuery .trigger('click') 在间隔函数内?

    这是一个改写的问题here https stackoverflow com questions 5031019 stuck on weird jquery error 经过一些测试后 我隔离了问题 但没有解决它的线索 无需阅读上一个问题 这
  • 为什么线程过程应该是静态的或成员函数

    为什么线程过程应该是静态的或成员函数 有什么正当理由吗 非静态成员变量有一个隐式的this编译器内部传递的参数 You have ClassInQuestion void threadFunc int 并且编译器内部创建了一个函数 void
  • DataTable.Merge 和 DataTable.ImportRow 不会更改 RowState

    我在 ADO NET 2 0 合并 导入数据时遇到问题 我需要将数据从一个通用表更新 插入到另一个表 并且两个表都保持相同的架构 以下代码在本地运行良好 但不会对数据库进行更改 OleDbDataAdapter localDA loadLo
  • mailgun 传入邮件事件获取附件 url

    我有一个节点端点 它接收 json 格式的传入电子邮件 其中包含来自 mailgun 的所有附件 附件位于 json 数组中 xxx com 用于隐私 attachments url https sw api mailgun net v3
  • 导航架构组件 - 将参数数据传递到 startDestination

    我有一个活动 A 启动活动 B 并向其传递一些意图数据 活动 B 托管来自新导航架构组件的导航图 我想将该意图数据作为参数传递给 startDestination 片段 如何做到这一点 好的 感谢 Google 团队的 Ian Lake 我
  • 正确传输和保护 Web 应用程序的用户密码

    我正在为我的硕士项目开发一个网络应用程序 这是一个供教授管理学生项目的系统 它使用Java作为服务器端代码 使用HSQLDB作为数据库 使用JSP作为表示层 并且运行在tomcat上 将存储的数据不包括任何敏感信息 学生 ID 财务信息等
  • FixIO 是做什么的?

    The System IO 文档 https hackage haskell org package base 4 5 0 0 docs System IO html包含一个神秘的 未记录的函数fixIO 它的来源 https hackag
  • 显示 Maven dependency:tree 中省略的版本?

    在 Eclipse 中 当我转到 Maven 依赖关系层次结构页面时 我得到的输出指出了哪些冲突导致版本被忽略 但是 如果我使用依赖 树 http maven apache org plugins maven dependency plug
  • purescript 中列表/数组中的类似记录类型

    有什么办法可以做类似的事情 first x 0 second x 1 y 1 both first second 这样both被推断为 x Int r 或类似的东西 我尝试过一些事情 x 3 Array forall r x Int r n
  • 无锁堆栈实现想法 - 目前已损坏

    我想出了一个想法 尝试实现一个无锁堆栈 该堆栈不依赖引用计数来解决 ABA 问题 并且还可以正确处理内存回收 它在概念上与 RCU 类似 并且依赖于两个功能 将列表条目标记为已删除 以及跟踪遍历列表的读者 前者很简单 它只使用指针的LSB