3
\$\begingroup\$

Follow-up to this question: after reading the comments, I decided to redesign my linked list to have a cursor that only contains one node and implement the interface similar to the one of forward_list in STL. The interface is fairly clear in my opinion but I also added extra comments where I found appropriate. There are some TODO that I added that I want to fix and haven't been able to find a solution to. Also looking for general design/style comments.

#include <iostream>
#include <thread>
#include <mutex>
#include <memory>
#include <optional>
#include <vector>

// Singly linked list that contains a shared pointer to its head and a mutex
// that has to be acquired to change the head.
class LockBasedLinkedList {
public:
    // A node contains a value and a shared pointer to the next node. The
    // reason for shared_ptr is because a cursor (iterator) contains a weak_ptr
    // to a node. To read or write to a field of a node, a mutex has to be
    // acquired.
    struct Node {
        int val;
        std::shared_ptr<Node> next;
        std::mutex m;

        explicit Node(int val_, const std::shared_ptr<Node> &next_)
            : val(val_), next(next_) {}
    };

    // A cursor (iterator) contains a weak pointer to a node that is
    // materialized every time the node is accessed. Because the interface of
    // the list allows insertion and deletion AFTER the node in the cursor, to
    // insert at the beginning, there must be a special BEFORE_BEGIN cursor.
    struct Cursor {
        enum class NodeType {BEFORE_BEGIN, NORMAL, END};
        NodeType type;
        std::weak_ptr<Node> node_weak;

        explicit Cursor(NodeType type_, const std::shared_ptr<Node> &node)
            : type(type_), node_weak(node) {}
    };

    explicit LockBasedLinkedList();
    ~LockBasedLinkedList();
    Cursor before_begin();  // not const for consistency with begin()
    Cursor begin();         // TODO: think about how to make it const
    Cursor end();           // not const for consistency with begin()
    bool empty();           // TODO: think about how to make it const
    bool try_insert_after(const Cursor &c, int val);
    bool try_erase_after(const Cursor &c);
    std::optional<int> get(const Cursor &c);
    bool cursor_try_next(Cursor &c);

private:
    std::shared_ptr<Node> head;
    std::mutex m;
};

LockBasedLinkedList::LockBasedLinkedList() : head(nullptr) {}

LockBasedLinkedList::~LockBasedLinkedList() {
    // TODO: free nodes to avoid stack overflow
}

LockBasedLinkedList::Cursor LockBasedLinkedList::before_begin() {
    return Cursor(Cursor::NodeType::BEFORE_BEGIN, nullptr);
}

LockBasedLinkedList::Cursor LockBasedLinkedList::begin() {
    // Lock the list because some other thread might be modifying the head.
    std::lock_guard<std::mutex> guard(m);
    return Cursor(Cursor::NodeType::NORMAL, head);
}

LockBasedLinkedList::Cursor LockBasedLinkedList::end() {
    return Cursor(Cursor::NodeType::END, nullptr);
}

bool LockBasedLinkedList::empty() {
    // Lock the list because some other thread might be modifying the head.
    std::lock_guard<std::mutex> guard(m);
    return head == nullptr;
}

// If the cursor points to the end (one past the last node) then return false.
// If the cursor points to the node before the first one, lock the list because
// the head will change. Otherwise, materialize the node in the cursor and
// acquire its lock to modify it.
bool LockBasedLinkedList::try_insert_after(const Cursor &c, int val) {
    if (c.type == Cursor::NodeType::END) {
        return false;
    }
    if (c.type == Cursor::NodeType::BEFORE_BEGIN) {
        std::lock_guard<std::mutex> guard(m);
        auto node = std::make_shared<Node>(val, head);
        head = node;
        return true;
    }
    auto node = c.node_weak.lock();
    if (node == nullptr) {
        return false;   // the node was erased
    }
    std::lock_guard<std::mutex> guard(node->m);
    auto new_node = std::make_shared<Node>(val, node->next);
    node->next = new_node;
    // TODO: rethink how to change the cursor (do I want to do that?)
    return true;
}

// If the cursor points to the end (one past the last node) then return false.
// If the cursor points to the node before the first one, lock the list because
// the head will change, acquire the lock of the first node and set the head to
// the node after. Otherwise, materialize the node in the cursor, acquire its
// lock, acquire the lock of the next node and set the next field to the node
// after the next one.
bool LockBasedLinkedList::try_erase_after(const Cursor &c) {
    if (c.type == Cursor::NodeType::END) {
        return false;
    }
    if (c.type == Cursor::NodeType::BEFORE_BEGIN) {
        std::lock_guard<std::mutex> guard(m);
        if (head == nullptr) {
            return false;   // the list is empty
        }
        std::lock_guard<std::mutex> guard_head(head->m);
        head = head->next;
        return true;
    }
    auto node = c.node_weak.lock();
    if (node == nullptr) {
        return false;   // the node was erased
    }
    std::lock_guard<std::mutex> guard(node->m);
    if (node->next == nullptr) {
        return false;   // this is the last node
    }
    std::lock_guard<std::mutex> guard_next(node->next->m);
    node->next = node->next->next;
    // TODO: rethink how to change the cursor (do I want to do that?)
    return true;
}

std::optional<int> LockBasedLinkedList::get(const Cursor &c) {
    if (c.type == Cursor::NodeType::BEFORE_BEGIN
            || c.type == Cursor::NodeType::END) {
        return {};
    }
    auto node = c.node_weak.lock();
    if (node == nullptr) {
        return {};  // the node was erased
    }
    std::lock_guard<std::mutex> guard(node->m);
    return node->val;
}

bool LockBasedLinkedList::cursor_try_next(Cursor &c) {
    if (c.type == Cursor::NodeType::END) {
        return false;
    }
    if (c.type == Cursor::NodeType::BEFORE_BEGIN) {
        std::lock_guard<std::mutex> guard(m);
        // TODO: maybe get rid of NodeType::END
        if (head == nullptr) {
            c = Cursor(Cursor::NodeType::END, nullptr);
        } else {
            c = Cursor(Cursor::NodeType::NORMAL, head);
        }
        return true;
    }
    auto node = c.node_weak.lock();
    if (node == nullptr) {
        return false;   // the node was erased
    }
    std::lock_guard<std::mutex> guard(node->m);
    if (node->next == nullptr) {
        c = Cursor(Cursor::NodeType::END, nullptr);
    } else {
        c = Cursor(Cursor::NodeType::NORMAL, node->next);
    }
    return true;
}

void testNThread(int n) {
    constexpr int N = 100;
    std::vector<std::thread> threads(n);
    LockBasedLinkedList lst;
    for (int i = 0; i < n; i++) {
        threads[i] = std::thread([i, &lst]() {
            for (int j = 0; j < N; j++) {
                while (!lst.try_insert_after(lst.before_begin(), i)) {
                    std::cout << "urgh insert\n";
                }
            }
        });
    }
    for (auto &&t : threads) {
        t.join();
    }
    for (int i = 0; i < N * n; i++) {
        auto valopt = lst.get(lst.begin());
        while (!valopt) {
            valopt = lst.get(lst.begin());
        }
        int val = valopt.value();
        while (!lst.try_erase_after(lst.before_begin())) {
            std::cout << "urgh erase\n";
        }
        std::cout << val << " ";
    }
    std::cout << "\n";
}

int main(int argc, char **argv) {
    int n = 10;
    if (argc >= 2) {
        n = atoi(argv[1]);
    }
    testNThread(n);
}
\$\endgroup\$

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.