簡體   English   中英

無鎖堆棧實現想法-目前已中斷

[英]Lock Free stack implementation idea - currently broken

我想出了一個想法,我試圖實現一個無鎖堆棧,該堆棧不依賴引用計數來解決ABA問題,並且還可以正確處理內存回收。 它在概念上與RCU類似,並且依賴於兩個功能:將列表條目標記為已刪除,以及跟蹤遍歷列表的讀者。 前者很簡單,它只使用指針的LSB。 后者是我“聰明”地嘗試實現一種無界無鎖堆棧的方法。

基本上,當任何線程嘗試遍歷列表時,一個原子計數器(list.entries)都會增加。 遍歷完成后,第二個計數器(list.exits)增加。

節點分配由推處理,而釋放則由pop處理。

推和彈出操作與天真無鎖堆棧實現非常相似,但是標記為刪除的節點必須經過遍歷才能到達未標記的條目。 因此,基本上,推入很像鏈表的插入。

彈出操作類似地遍歷列表,但是它使用atomic_fetch_or將遍歷時標記為已刪除的節點,直到到達未標記的節點。

遍歷0個或更多標記節點的列表后,彈出的線程將嘗試將CAS的頭部設為CAS。 至少有一個並發彈出的線程將成功,並且此后所有進入堆棧的讀取器將不再看到以前標記的節點。

成功更新列表的線程然后加載atomic 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問題,但是描述和代碼實際上是為了解決一個更棘手的問題: 內存回收問題。

在無垃圾回收的語言中實現的無鎖集合的“刪除”功能中通常會出現此問題。 核心問題是,從共享結構中刪除節點的線程通常不知道何時可以安全地釋放已刪除的節點,因為其他讀取可能仍然引用該節點。 通常,作為一個副作用解決該問題可以解決ABA問題:這特別是與CAS操作成功有關,即使在此期間基礎指針(和對象的狀態)已被更改了至少兩次,最后還是原始值,但呈現出完全不同的狀態。

從某種意義上說,ABA問題比較容易,因為有很多直接解決ABA問題的方法,特別是不會導致解決“內存回收”問題的方法。 從某種意義上講,可以檢測到位置修改(例如使用LL / SC或事務性存儲原語)的硬件可能根本不會出現此問題,這也更容易。

如此說來,您正在尋找一種解決內存回收問題的方法,它還將避免ABA問題。

您問題的核心是以下聲明:

成功更新列表的線程然后加載atomic list.entries,並基本上旋轉加載atomic.exits,直到該計數器最終超過list.entries。 這應該意味着該列表的“舊”版本的所有讀者都已完成。 然后,該線程簡單地釋放它從列表頂部交換過來的標記節點列表。

這種邏輯不成立。 等待list.exits (您說atomic.exits,但我認為這是一個錯字,因為您只在其他地方談論list.exits )大於list.entries告訴您現在的總退出次數已經比列表中的條目多指向變異線程捕獲的入口計數。 但是,這些退出可能是由新讀者來來往往所產生的:這並不意味着所有舊讀者都已經按照您的要求完成了

這是一個簡單的例子。 首先,寫線程T1和讀線程T2大約同時訪問列表,因此list.entries為2, list.exits為0。寫線程彈出一個節點,並保存list.entries的當前值(2)。並等待lists.exits大於2。現在又有三個讀取線程T3T4T5到達並快速讀取list並離開。 現在lists.exits為3,您的條件得到滿足, T1釋放了該節點。 T2並沒有走到任何地方,因為它正在讀取已釋放的節點而炸毀了!

您擁有的基本想法是可行的,但是您的兩種對立方式絕對是行不通的。

這是一個經過充分研究的問題,因此您無需發明自己的算法(請參見上面的鏈接),甚至不必編寫自己的代碼,因為已經存在諸如librcuconcurrencykit之類的東西。

用於教育目的

但是,如果您將此內容用於教育目的,一種方法是使用確保修改開始后進入的線程使用另一組list.entry/exit計數器。 一種實現方式是生成計數器,當編寫者想要修改列表時,它會增加生成計數器,這會使新的讀者切換到另一組list.entry/exit計數器。

現在,編寫者只需要等待list.entry[old] == list.exists[old] ,這意味着所有讀者都已離開。 您也可以每代只使用一個計數器:您實際上並不需要兩個entry/exit計數器(盡管這可能有助於減少爭用)。

當然,您知道還有一個新問題,即要管理每個世代的獨立計數器列表……這看起來像是構建無鎖列表的原始問題! 但是,此問題要容易一些,因為您可以對“運行中”的世代數進行合理的限制,然后將它們全部預先分配,或者您可以實現有限類型的無鎖列表,這更易於推理因為添加和刪除僅發生在頭部或尾部。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM