实现带有条目超时的 C 哈希表的最有效方法是什么?

问题描述

我目前正在开发一个哈希表作为数据库数据结构,它可能包含大量元素,并且必须尽可能高效(特别是在添加新元素和更新现有元素的操作上) . 我还被迫只使用 C(避免使用 C++ 或其他具有在这种情况下确实有帮助的现有类或结构的语言)。

我需要开发的是一个带有链表的哈希表,其中每个条目都有一个超时(比如说几分钟),之后它应该自动删除自己(或者,作为替代,旧条目应该是“垃圾在某个时间点收集”,因为元素可能会以非常快的速度添加,我不想为太旧的条目使用太多内存)。

我正在考虑为哈希表的每个条目添加一个计时器字段:

struct HTnode {
    // Hash table entry ID
    long int id;
    
    // Pointer to the next element of the linked list (when hash is the same for two different IDs)
    struct STnode * next;
    
    // Other fields...

    // Timer for each entry
    timer_t entryTimer;
};

然后,当添加一个新条目时,启动一个计时器(这个项目只会在 Linux 上运行,这就是我考虑使用 timer_t 的原因 - 为了这个示例代码中没有执行错误检查简洁):

struct sigevent entryTimerEvent;
struct itimerspec entryTimerTs;

// Allocate a new entry for a given id (struct HTnode *entry)
// ...

memset(&entryTimerEvent,sizeof(entryTimerEvent));

// entryDeleter() is a function deleting the current entry from the hash table
entryTimerEvent.sigev_notify_function=entryDeleter;
entryTimerEvent.sigev_notify=SIGEV_THREAD;

entryTimerTs.it_value.tv_sec=...; // Set to a certain timeout value
entryTimerTs.it_value.tv_nsec=...; // Set to a certain timeout value
entryTimerTs.it_interval.tv_sec=...; // Set to a certain timeout value
entryTimerTs.it_interval.tv_nsec=...; // Set to a certain timeout value

timer_create(CLOCK_REALTIME,&entryTimerEvent,&(entry->entryTimer));

timer_settime(entry->entryTimer,&entryTimerTs,NULL);

当条目更新时,我只会用 timer_settime 重新启动计时器。

但是,我担心这样的解决方案可能会在我达到超过数千个条目时在性能方面出现问题,所有条目都有自己的运行计时器(某些活动条目甚至可能以亚秒级粒度更新,导致非常频繁地调用 timer_settime),我目前正在努力寻找一个好的替代方案。

在您看来,是否有更好、更有效的解决方案,可能不需要为每个条目使用计时器?

非常感谢您。

解决方法

我对您的要求的理解:

  1. 如果某个元素已超时,则在您尝试获取它时不应出现该元素
  2. 最终会被删除,以免表永远增长

为了实现这一点,您可以为每个元素添加一个时间戳

  1. 改变你的get函数,检查当前时间是否在时间戳之前,否则不返回
  2. 有一个删除过期元素的观察者线程
,

一个粗略但简单的选择:

具有不同的哈希表 - 例如三个表,每个表存储在特定分钟内添加的所有条目;您将新条目添加到 table[active]。要进行查找或擦除,请在活动表中搜索(如果最近添加的元素在统计上最有可能被访问),然后依次尝试旧表。当您添加一个元素并发现分钟已更改时,在向其中添加新元素之前,将 ++active %= 3 切换为 active,然后清除 table[active]


一种更加过度设计的方式,使用单个哈希表...

假设您希望删除超过 X 个时间单位的元素,即使它们最近被访问过,那么您所需要的只是一个双端队列,位于哈希表的一侧,用于跟踪您已访问的元素添加。队列中的每个元素都应该存储到期时间和任何便于快速删除的元素(例如,指向存储桶中链表中前一个元素(或存储桶/根)的指针,如果您使用单独的链接,则不要支持调整哈希表的大小,并且知道元素不会被任何其他机制擦除)。我说上一个是因为如果它是一个单链表,那么你需要重新连接上一个元素的下一个指针。如果是双向链表,存储一个指向元素本身的指针可能更直观,那么可以使用prev->next = next重新连线。

当您添加一个元素时,您将其添加到该队列的后面。每当您有空闲时间进行一些内务处理时,您就从队列的前面开始并擦除 - 但是许多元素已过期。您可以查看是否最好有一个单独的线程来处理到期(但随后您需要锁定哈希表访问/更新),或者从使用哈希表的同一线程执行此操作。

如果您确实希望最近访问的元素停留更长时间,那么您需要使用最近最少使用的缓存机制来跟踪到期时间,这意味着您需要能够通过键在 LRU 中进行搜索,所以你可能想要使用平衡二叉树。这将需要更多的实施工作。

如果您使用的是封闭散列/开放寻址散列表,那么如何擦除元素将取决于您处理冲突的方式:您可以只用不再存在的-使用哨兵值,或者您可能不得不将另一个元素移动到更接近其散列到存储桶的位置。这些因素可能会改变在超时跟踪双端队列或 LRU 中存储的索引数据。

在 C 中从头开始是一项重大任务,因此我建议您利用现有的数据结构库。尽管如此,在 StackOverflow 上寻求对 3rd 方资源的建议是无关紧要的,如果您还不知道您的选择,这可能会令人沮丧。这些天我无法从 C++ 回到 C - 太令人沮丧了 - 而且我不知道当前的产品可以提出什么建议......