Linked list node-level locking











up vote
3
down vote

favorite
2












I am new to C++ threading library. I would love to get some comments on how to improve this code (if there are possible bugs, I would appreciate that feedback too). In particular, I want to refactor the code for the methods of the linked list:




  1. How to return unique_lock in a function so that it didn't unlock the mutex under the hood?

  2. Are there possible deadlocks with the lock acquisition that takes place in my code?


  3. Does it look correct?



    #include <iostream>
    #include <thread>
    #include <mutex>
    #include <atomic>
    #include <vector>

    using namespace std;

    /*
    * This is an implementation of a singly linked list with node-level locks to
    * ensure mutual exclusion. The structure looks like this:
    * head_ -> node_0 -> node_1 -> ... -> node_n -> tail_
    * Note that head_ and tail_ are dummy nodes.
    */
    class LockBasedLinkedList {
    public:
    // Initialize head_ to point to tail_ (empty list).
    LockBasedLinkedList() {
    tail_ = new Node(0);
    head_ = new Node(0, tail_);
    }

    /*
    * Insert a node with value val at position pos.
    *
    * To ensure mutual exclusion, the locks of the current node and the
    * previous nodes must be acquired for insertion to work. As soon as the
    * locks are acquired the code does roughly the following:
    *
    * | prev -> node | prev -> node | prev node |
    * | | ^ | v ^ |
    * | new_node | new_node | new_node |
    */
    void insert(int val, int pos) {
    Node *new_node = new Node(val);
    Node *prev = head_;
    unique_lock<mutex> prev_lk(prev->m);
    Node *node = prev->next;
    unique_lock<mutex> node_lk(node->m);
    for (int i = 0; i < pos && node != tail_; i++) {
    prev = node;
    node = node->next;
    prev_lk.swap(node_lk);
    node_lk = unique_lock<mutex>(node->m);
    }
    new_node->next = node;
    prev->next = new_node;
    }

    /*
    * Erase the node at position pos.
    *
    * To ensure mutual exclusion, follow the steps from insert(). As soon as
    * the locks are acquired the code does roughly the following:
    *
    * (*) (*) (*) (*) (*)
    * | prev -> node -> next | prev node -> next | prev ---------> next |
    * | | v--------------^ | |
    *
    * (*) highlights the nodes whose locks are acquired.
    */
    void erase(int pos) {
    Node *prev = head_;
    unique_lock<mutex> prev_lk(prev->m);
    Node *node = prev->next;
    unique_lock<mutex> node_lk(node->m);
    for (int i = 0; i < pos && node != tail_; i++) {
    prev = node;
    node = node->next;
    prev_lk.swap(node_lk);
    node_lk = unique_lock<mutex>(node->m);
    }
    if (node == tail_) {
    return;
    }
    prev->next = node->next;
    node_lk.unlock();
    delete node;
    }

    int get(int pos) {
    Node *prev = head_;
    unique_lock<mutex> prev_lk(prev->m);
    Node *node = prev->next;
    unique_lock<mutex> node_lk(node->m);
    for (int i = 0; i < pos && node != tail_; i++) {
    prev = node;
    node = node->next;
    prev_lk.swap(node_lk);
    node_lk = unique_lock<mutex>(node->m);
    }
    if (node == tail_) {
    return 0;
    }
    return node->val;
    }

    private:
    struct Node {
    int val;
    Node *next;
    mutex m;

    Node(int val_, Node *next_ = nullptr) : val(val_), next(next_) {}
    };

    Node *head_, *tail_;
    };

    void testNThread(int n) {
    const int N = 10;
    vector<thread> threads(n);
    LockBasedLinkedList lst;
    for (int i = 0; i < n; i++) {
    threads[i] = thread([i, &lst, N]() {
    for (int j = 0; j < N; j++) {
    lst.insert(i, 0);
    }
    });
    }
    for (int i = 0; i < n; i++) {
    threads[i].join();
    }
    for (int i = 0; i < N * n; i++) {
    int val = lst.get(0);
    lst.erase(0);
    cout << val << " ";
    }
    cout << "n";
    }

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











share|improve this question







New contributor




nhtrnm is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.
























    up vote
    3
    down vote

    favorite
    2












    I am new to C++ threading library. I would love to get some comments on how to improve this code (if there are possible bugs, I would appreciate that feedback too). In particular, I want to refactor the code for the methods of the linked list:




    1. How to return unique_lock in a function so that it didn't unlock the mutex under the hood?

    2. Are there possible deadlocks with the lock acquisition that takes place in my code?


    3. Does it look correct?



      #include <iostream>
      #include <thread>
      #include <mutex>
      #include <atomic>
      #include <vector>

      using namespace std;

      /*
      * This is an implementation of a singly linked list with node-level locks to
      * ensure mutual exclusion. The structure looks like this:
      * head_ -> node_0 -> node_1 -> ... -> node_n -> tail_
      * Note that head_ and tail_ are dummy nodes.
      */
      class LockBasedLinkedList {
      public:
      // Initialize head_ to point to tail_ (empty list).
      LockBasedLinkedList() {
      tail_ = new Node(0);
      head_ = new Node(0, tail_);
      }

      /*
      * Insert a node with value val at position pos.
      *
      * To ensure mutual exclusion, the locks of the current node and the
      * previous nodes must be acquired for insertion to work. As soon as the
      * locks are acquired the code does roughly the following:
      *
      * | prev -> node | prev -> node | prev node |
      * | | ^ | v ^ |
      * | new_node | new_node | new_node |
      */
      void insert(int val, int pos) {
      Node *new_node = new Node(val);
      Node *prev = head_;
      unique_lock<mutex> prev_lk(prev->m);
      Node *node = prev->next;
      unique_lock<mutex> node_lk(node->m);
      for (int i = 0; i < pos && node != tail_; i++) {
      prev = node;
      node = node->next;
      prev_lk.swap(node_lk);
      node_lk = unique_lock<mutex>(node->m);
      }
      new_node->next = node;
      prev->next = new_node;
      }

      /*
      * Erase the node at position pos.
      *
      * To ensure mutual exclusion, follow the steps from insert(). As soon as
      * the locks are acquired the code does roughly the following:
      *
      * (*) (*) (*) (*) (*)
      * | prev -> node -> next | prev node -> next | prev ---------> next |
      * | | v--------------^ | |
      *
      * (*) highlights the nodes whose locks are acquired.
      */
      void erase(int pos) {
      Node *prev = head_;
      unique_lock<mutex> prev_lk(prev->m);
      Node *node = prev->next;
      unique_lock<mutex> node_lk(node->m);
      for (int i = 0; i < pos && node != tail_; i++) {
      prev = node;
      node = node->next;
      prev_lk.swap(node_lk);
      node_lk = unique_lock<mutex>(node->m);
      }
      if (node == tail_) {
      return;
      }
      prev->next = node->next;
      node_lk.unlock();
      delete node;
      }

      int get(int pos) {
      Node *prev = head_;
      unique_lock<mutex> prev_lk(prev->m);
      Node *node = prev->next;
      unique_lock<mutex> node_lk(node->m);
      for (int i = 0; i < pos && node != tail_; i++) {
      prev = node;
      node = node->next;
      prev_lk.swap(node_lk);
      node_lk = unique_lock<mutex>(node->m);
      }
      if (node == tail_) {
      return 0;
      }
      return node->val;
      }

      private:
      struct Node {
      int val;
      Node *next;
      mutex m;

      Node(int val_, Node *next_ = nullptr) : val(val_), next(next_) {}
      };

      Node *head_, *tail_;
      };

      void testNThread(int n) {
      const int N = 10;
      vector<thread> threads(n);
      LockBasedLinkedList lst;
      for (int i = 0; i < n; i++) {
      threads[i] = thread([i, &lst, N]() {
      for (int j = 0; j < N; j++) {
      lst.insert(i, 0);
      }
      });
      }
      for (int i = 0; i < n; i++) {
      threads[i].join();
      }
      for (int i = 0; i < N * n; i++) {
      int val = lst.get(0);
      lst.erase(0);
      cout << val << " ";
      }
      cout << "n";
      }

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











    share|improve this question







    New contributor




    nhtrnm is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
    Check out our Code of Conduct.






















      up vote
      3
      down vote

      favorite
      2









      up vote
      3
      down vote

      favorite
      2






      2





      I am new to C++ threading library. I would love to get some comments on how to improve this code (if there are possible bugs, I would appreciate that feedback too). In particular, I want to refactor the code for the methods of the linked list:




      1. How to return unique_lock in a function so that it didn't unlock the mutex under the hood?

      2. Are there possible deadlocks with the lock acquisition that takes place in my code?


      3. Does it look correct?



        #include <iostream>
        #include <thread>
        #include <mutex>
        #include <atomic>
        #include <vector>

        using namespace std;

        /*
        * This is an implementation of a singly linked list with node-level locks to
        * ensure mutual exclusion. The structure looks like this:
        * head_ -> node_0 -> node_1 -> ... -> node_n -> tail_
        * Note that head_ and tail_ are dummy nodes.
        */
        class LockBasedLinkedList {
        public:
        // Initialize head_ to point to tail_ (empty list).
        LockBasedLinkedList() {
        tail_ = new Node(0);
        head_ = new Node(0, tail_);
        }

        /*
        * Insert a node with value val at position pos.
        *
        * To ensure mutual exclusion, the locks of the current node and the
        * previous nodes must be acquired for insertion to work. As soon as the
        * locks are acquired the code does roughly the following:
        *
        * | prev -> node | prev -> node | prev node |
        * | | ^ | v ^ |
        * | new_node | new_node | new_node |
        */
        void insert(int val, int pos) {
        Node *new_node = new Node(val);
        Node *prev = head_;
        unique_lock<mutex> prev_lk(prev->m);
        Node *node = prev->next;
        unique_lock<mutex> node_lk(node->m);
        for (int i = 0; i < pos && node != tail_; i++) {
        prev = node;
        node = node->next;
        prev_lk.swap(node_lk);
        node_lk = unique_lock<mutex>(node->m);
        }
        new_node->next = node;
        prev->next = new_node;
        }

        /*
        * Erase the node at position pos.
        *
        * To ensure mutual exclusion, follow the steps from insert(). As soon as
        * the locks are acquired the code does roughly the following:
        *
        * (*) (*) (*) (*) (*)
        * | prev -> node -> next | prev node -> next | prev ---------> next |
        * | | v--------------^ | |
        *
        * (*) highlights the nodes whose locks are acquired.
        */
        void erase(int pos) {
        Node *prev = head_;
        unique_lock<mutex> prev_lk(prev->m);
        Node *node = prev->next;
        unique_lock<mutex> node_lk(node->m);
        for (int i = 0; i < pos && node != tail_; i++) {
        prev = node;
        node = node->next;
        prev_lk.swap(node_lk);
        node_lk = unique_lock<mutex>(node->m);
        }
        if (node == tail_) {
        return;
        }
        prev->next = node->next;
        node_lk.unlock();
        delete node;
        }

        int get(int pos) {
        Node *prev = head_;
        unique_lock<mutex> prev_lk(prev->m);
        Node *node = prev->next;
        unique_lock<mutex> node_lk(node->m);
        for (int i = 0; i < pos && node != tail_; i++) {
        prev = node;
        node = node->next;
        prev_lk.swap(node_lk);
        node_lk = unique_lock<mutex>(node->m);
        }
        if (node == tail_) {
        return 0;
        }
        return node->val;
        }

        private:
        struct Node {
        int val;
        Node *next;
        mutex m;

        Node(int val_, Node *next_ = nullptr) : val(val_), next(next_) {}
        };

        Node *head_, *tail_;
        };

        void testNThread(int n) {
        const int N = 10;
        vector<thread> threads(n);
        LockBasedLinkedList lst;
        for (int i = 0; i < n; i++) {
        threads[i] = thread([i, &lst, N]() {
        for (int j = 0; j < N; j++) {
        lst.insert(i, 0);
        }
        });
        }
        for (int i = 0; i < n; i++) {
        threads[i].join();
        }
        for (int i = 0; i < N * n; i++) {
        int val = lst.get(0);
        lst.erase(0);
        cout << val << " ";
        }
        cout << "n";
        }

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











      share|improve this question







      New contributor




      nhtrnm is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
      Check out our Code of Conduct.











      I am new to C++ threading library. I would love to get some comments on how to improve this code (if there are possible bugs, I would appreciate that feedback too). In particular, I want to refactor the code for the methods of the linked list:




      1. How to return unique_lock in a function so that it didn't unlock the mutex under the hood?

      2. Are there possible deadlocks with the lock acquisition that takes place in my code?


      3. Does it look correct?



        #include <iostream>
        #include <thread>
        #include <mutex>
        #include <atomic>
        #include <vector>

        using namespace std;

        /*
        * This is an implementation of a singly linked list with node-level locks to
        * ensure mutual exclusion. The structure looks like this:
        * head_ -> node_0 -> node_1 -> ... -> node_n -> tail_
        * Note that head_ and tail_ are dummy nodes.
        */
        class LockBasedLinkedList {
        public:
        // Initialize head_ to point to tail_ (empty list).
        LockBasedLinkedList() {
        tail_ = new Node(0);
        head_ = new Node(0, tail_);
        }

        /*
        * Insert a node with value val at position pos.
        *
        * To ensure mutual exclusion, the locks of the current node and the
        * previous nodes must be acquired for insertion to work. As soon as the
        * locks are acquired the code does roughly the following:
        *
        * | prev -> node | prev -> node | prev node |
        * | | ^ | v ^ |
        * | new_node | new_node | new_node |
        */
        void insert(int val, int pos) {
        Node *new_node = new Node(val);
        Node *prev = head_;
        unique_lock<mutex> prev_lk(prev->m);
        Node *node = prev->next;
        unique_lock<mutex> node_lk(node->m);
        for (int i = 0; i < pos && node != tail_; i++) {
        prev = node;
        node = node->next;
        prev_lk.swap(node_lk);
        node_lk = unique_lock<mutex>(node->m);
        }
        new_node->next = node;
        prev->next = new_node;
        }

        /*
        * Erase the node at position pos.
        *
        * To ensure mutual exclusion, follow the steps from insert(). As soon as
        * the locks are acquired the code does roughly the following:
        *
        * (*) (*) (*) (*) (*)
        * | prev -> node -> next | prev node -> next | prev ---------> next |
        * | | v--------------^ | |
        *
        * (*) highlights the nodes whose locks are acquired.
        */
        void erase(int pos) {
        Node *prev = head_;
        unique_lock<mutex> prev_lk(prev->m);
        Node *node = prev->next;
        unique_lock<mutex> node_lk(node->m);
        for (int i = 0; i < pos && node != tail_; i++) {
        prev = node;
        node = node->next;
        prev_lk.swap(node_lk);
        node_lk = unique_lock<mutex>(node->m);
        }
        if (node == tail_) {
        return;
        }
        prev->next = node->next;
        node_lk.unlock();
        delete node;
        }

        int get(int pos) {
        Node *prev = head_;
        unique_lock<mutex> prev_lk(prev->m);
        Node *node = prev->next;
        unique_lock<mutex> node_lk(node->m);
        for (int i = 0; i < pos && node != tail_; i++) {
        prev = node;
        node = node->next;
        prev_lk.swap(node_lk);
        node_lk = unique_lock<mutex>(node->m);
        }
        if (node == tail_) {
        return 0;
        }
        return node->val;
        }

        private:
        struct Node {
        int val;
        Node *next;
        mutex m;

        Node(int val_, Node *next_ = nullptr) : val(val_), next(next_) {}
        };

        Node *head_, *tail_;
        };

        void testNThread(int n) {
        const int N = 10;
        vector<thread> threads(n);
        LockBasedLinkedList lst;
        for (int i = 0; i < n; i++) {
        threads[i] = thread([i, &lst, N]() {
        for (int j = 0; j < N; j++) {
        lst.insert(i, 0);
        }
        });
        }
        for (int i = 0; i < n; i++) {
        threads[i].join();
        }
        for (int i = 0; i < N * n; i++) {
        int val = lst.get(0);
        lst.erase(0);
        cout << val << " ";
        }
        cout << "n";
        }

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








      c++ multithreading thread-safety






      share|improve this question







      New contributor




      nhtrnm is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
      Check out our Code of Conduct.











      share|improve this question







      New contributor




      nhtrnm is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
      Check out our Code of Conduct.









      share|improve this question




      share|improve this question






      New contributor




      nhtrnm is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
      Check out our Code of Conduct.









      asked 6 hours ago









      nhtrnm

      1161




      1161




      New contributor




      nhtrnm is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
      Check out our Code of Conduct.





      New contributor





      nhtrnm is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
      Check out our Code of Conduct.






      nhtrnm is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
      Check out our Code of Conduct.






















          1 Answer
          1






          active

          oldest

          votes

















          up vote
          2
          down vote













          Congratulations — I think this is the first concurrency-related code I've reviewed on CodeReview in which I have not managed to find any concurrency-related bugs! It looks like a solid implementation of "hand-over-hand" traversal. (Now that I've said that, I bet someone else will find a bug. :))



          First, a few stylistic nitpicks:



          using namespace std;


          Never! Just write std::vector<std::thread> if that's what you mean. The small space savings isn't worth dealing with all the people like me who'll tell you over and over not to write using namespace std; at global scope. :)



          const int N = 10;


          Make this constexpr int N = 10; and you won't need to capture a copy of it in your lambda.



          for (int i = 0; i < n; i++) {
          threads[i].join();
          }


          Prefer to write this with a ranged for loop:



          for (auto&& thread : threads) {
          thread.join();
          }




          struct Node {
          int val;
          Node *next;
          mutex m;

          Node(int val_, Node *next_ = nullptr) : val(val_), next(next_) {}
          };


          Defaulted function arguments are the devil. And, completely independently, so are implicit conversions! I would prefer to write this as:



          struct Node {
          int val;
          Node *next = nullptr;
          mutex m;

          explicit Node(int val_) : val(val_) {}
          explicit Node(int val_, Node *next_) : val(val_), next(next_) {}
          };


          or even



          struct Node {
          int val;
          Node *next;
          mutex m;

          explicit Node(int val_) : Node(val_, nullptr) {}
          explicit Node(int val_, Node *next_) : val(val_), next(next_) {}
          };




          Okay, now for the serious stuff.



          void erase(int pos);


          This is a really weird API for a linked list. For one thing, even for a non-concurrent linked list, you're turning "erase a node" into an O(n) operation. More importantly for a concurrent linked list, I can't see how this operation is useful at all! Suppose some thread says "please erase the 42nd element." How does it even know what the 42nd element is, given that any other thread could modify the list at any time?





          Your use of raw new and delete looks safe to me, but it would be a lot easier to verify its safety if you just didn't use raw new and delete! Consider this trivial rewrite of insert:



          void insert(int val, int pos) {
          auto new_node = std::make_unique<Node>(val);
          Node *prev = head_;
          unique_lock<mutex> prev_lk(prev->m);
          Node *node = prev->next;
          unique_lock<mutex> node_lk(node->m);
          for (int i = 0; i < pos && node != tail_; i++) {
          prev = node;
          node = node->next;
          prev_lk.swap(node_lk);
          node_lk = unique_lock<mutex>(node->m);
          }
          new_node->next = node;
          prev->next = new_node.release();
          }


          Only two lines changed, but now it's obvious that we never leak memory...





          ...except in the destructor of LockBasedLinkedList, which (being defaulted) just leaks every node remaining in the list! Was that intentional, or an oversight?





          Also consider the behavior of



          LockBasedLinkedList a;
          auto b = a;


          This certainly doesn't do what you want. What do you want it to do? If the answer is "I want it not to compile," then you should =delete your copy operations:



          LockBasedLinkedList(const LockBasedLinkedList&) = delete;
          LockBasedLinkedList& operator=(const LockBasedLinkedList&) = delete;




          In get:



              if (node == tail_) {
          return 0;
          }


          How would your user distinguish the 0 that means "no such node" from the 0 that means "this node exists and has value 0"? I would strongly recommend designing your API so that this distinction is obvious to the user. For example:



          std::optional<int> get(int);





          How to return unique_lock in a function...?




          I think this is related to your get API, yeah? That is, maybe you want something like



          LockedNodePointer get(int);

          class LockedNodePointer {
          public:
          LockedNodePointer(std::nullptr_t) : ptr_(nullptr) {}
          Node& operator*() const { return *ptr_; }
          Node& operator->() const { return ptr_; }
          private:
          friend class LockBasedLinkedList;
          using Lock = std::unique_lock<std::mutex>;
          explicit LockedNodePointer(Lock lk, Node *ptr) : lk_(lk), ptr_(ptr) {}
          Lock lk_;
          Node *ptr_;
          };


          This is an even cleaner way to distinguish "node with value 0" from "no such node."





          It would also be interesting to consider: do you need a version of your get member function that is const-qualified? (Getting an element doesn't really modify the list, right?)



          Do you need two overloads of get — one const and one non-const?






          share|improve this answer





















            Your Answer





            StackExchange.ifUsing("editor", function () {
            return StackExchange.using("mathjaxEditing", function () {
            StackExchange.MarkdownEditor.creationCallbacks.add(function (editor, postfix) {
            StackExchange.mathjaxEditing.prepareWmdForMathJax(editor, postfix, [["\$", "\$"]]);
            });
            });
            }, "mathjax-editing");

            StackExchange.ifUsing("editor", function () {
            StackExchange.using("externalEditor", function () {
            StackExchange.using("snippets", function () {
            StackExchange.snippets.init();
            });
            });
            }, "code-snippets");

            StackExchange.ready(function() {
            var channelOptions = {
            tags: "".split(" "),
            id: "196"
            };
            initTagRenderer("".split(" "), "".split(" "), channelOptions);

            StackExchange.using("externalEditor", function() {
            // Have to fire editor after snippets, if snippets enabled
            if (StackExchange.settings.snippets.snippetsEnabled) {
            StackExchange.using("snippets", function() {
            createEditor();
            });
            }
            else {
            createEditor();
            }
            });

            function createEditor() {
            StackExchange.prepareEditor({
            heartbeatType: 'answer',
            convertImagesToLinks: false,
            noModals: true,
            showLowRepImageUploadWarning: true,
            reputationToPostImages: null,
            bindNavPrevention: true,
            postfix: "",
            imageUploader: {
            brandingHtml: "Powered by u003ca class="icon-imgur-white" href="https://imgur.com/"u003eu003c/au003e",
            contentPolicyHtml: "User contributions licensed under u003ca href="https://creativecommons.org/licenses/by-sa/3.0/"u003ecc by-sa 3.0 with attribution requiredu003c/au003e u003ca href="https://stackoverflow.com/legal/content-policy"u003e(content policy)u003c/au003e",
            allowUrls: true
            },
            onDemand: true,
            discardSelector: ".discard-answer"
            ,immediatelyShowMarkdownHelp:true
            });


            }
            });






            nhtrnm is a new contributor. Be nice, and check out our Code of Conduct.










             

            draft saved


            draft discarded


















            StackExchange.ready(
            function () {
            StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f207626%2flinked-list-node-level-locking%23new-answer', 'question_page');
            }
            );

            Post as a guest
































            1 Answer
            1






            active

            oldest

            votes








            1 Answer
            1






            active

            oldest

            votes









            active

            oldest

            votes






            active

            oldest

            votes








            up vote
            2
            down vote













            Congratulations — I think this is the first concurrency-related code I've reviewed on CodeReview in which I have not managed to find any concurrency-related bugs! It looks like a solid implementation of "hand-over-hand" traversal. (Now that I've said that, I bet someone else will find a bug. :))



            First, a few stylistic nitpicks:



            using namespace std;


            Never! Just write std::vector<std::thread> if that's what you mean. The small space savings isn't worth dealing with all the people like me who'll tell you over and over not to write using namespace std; at global scope. :)



            const int N = 10;


            Make this constexpr int N = 10; and you won't need to capture a copy of it in your lambda.



            for (int i = 0; i < n; i++) {
            threads[i].join();
            }


            Prefer to write this with a ranged for loop:



            for (auto&& thread : threads) {
            thread.join();
            }




            struct Node {
            int val;
            Node *next;
            mutex m;

            Node(int val_, Node *next_ = nullptr) : val(val_), next(next_) {}
            };


            Defaulted function arguments are the devil. And, completely independently, so are implicit conversions! I would prefer to write this as:



            struct Node {
            int val;
            Node *next = nullptr;
            mutex m;

            explicit Node(int val_) : val(val_) {}
            explicit Node(int val_, Node *next_) : val(val_), next(next_) {}
            };


            or even



            struct Node {
            int val;
            Node *next;
            mutex m;

            explicit Node(int val_) : Node(val_, nullptr) {}
            explicit Node(int val_, Node *next_) : val(val_), next(next_) {}
            };




            Okay, now for the serious stuff.



            void erase(int pos);


            This is a really weird API for a linked list. For one thing, even for a non-concurrent linked list, you're turning "erase a node" into an O(n) operation. More importantly for a concurrent linked list, I can't see how this operation is useful at all! Suppose some thread says "please erase the 42nd element." How does it even know what the 42nd element is, given that any other thread could modify the list at any time?





            Your use of raw new and delete looks safe to me, but it would be a lot easier to verify its safety if you just didn't use raw new and delete! Consider this trivial rewrite of insert:



            void insert(int val, int pos) {
            auto new_node = std::make_unique<Node>(val);
            Node *prev = head_;
            unique_lock<mutex> prev_lk(prev->m);
            Node *node = prev->next;
            unique_lock<mutex> node_lk(node->m);
            for (int i = 0; i < pos && node != tail_; i++) {
            prev = node;
            node = node->next;
            prev_lk.swap(node_lk);
            node_lk = unique_lock<mutex>(node->m);
            }
            new_node->next = node;
            prev->next = new_node.release();
            }


            Only two lines changed, but now it's obvious that we never leak memory...





            ...except in the destructor of LockBasedLinkedList, which (being defaulted) just leaks every node remaining in the list! Was that intentional, or an oversight?





            Also consider the behavior of



            LockBasedLinkedList a;
            auto b = a;


            This certainly doesn't do what you want. What do you want it to do? If the answer is "I want it not to compile," then you should =delete your copy operations:



            LockBasedLinkedList(const LockBasedLinkedList&) = delete;
            LockBasedLinkedList& operator=(const LockBasedLinkedList&) = delete;




            In get:



                if (node == tail_) {
            return 0;
            }


            How would your user distinguish the 0 that means "no such node" from the 0 that means "this node exists and has value 0"? I would strongly recommend designing your API so that this distinction is obvious to the user. For example:



            std::optional<int> get(int);





            How to return unique_lock in a function...?




            I think this is related to your get API, yeah? That is, maybe you want something like



            LockedNodePointer get(int);

            class LockedNodePointer {
            public:
            LockedNodePointer(std::nullptr_t) : ptr_(nullptr) {}
            Node& operator*() const { return *ptr_; }
            Node& operator->() const { return ptr_; }
            private:
            friend class LockBasedLinkedList;
            using Lock = std::unique_lock<std::mutex>;
            explicit LockedNodePointer(Lock lk, Node *ptr) : lk_(lk), ptr_(ptr) {}
            Lock lk_;
            Node *ptr_;
            };


            This is an even cleaner way to distinguish "node with value 0" from "no such node."





            It would also be interesting to consider: do you need a version of your get member function that is const-qualified? (Getting an element doesn't really modify the list, right?)



            Do you need two overloads of get — one const and one non-const?






            share|improve this answer

























              up vote
              2
              down vote













              Congratulations — I think this is the first concurrency-related code I've reviewed on CodeReview in which I have not managed to find any concurrency-related bugs! It looks like a solid implementation of "hand-over-hand" traversal. (Now that I've said that, I bet someone else will find a bug. :))



              First, a few stylistic nitpicks:



              using namespace std;


              Never! Just write std::vector<std::thread> if that's what you mean. The small space savings isn't worth dealing with all the people like me who'll tell you over and over not to write using namespace std; at global scope. :)



              const int N = 10;


              Make this constexpr int N = 10; and you won't need to capture a copy of it in your lambda.



              for (int i = 0; i < n; i++) {
              threads[i].join();
              }


              Prefer to write this with a ranged for loop:



              for (auto&& thread : threads) {
              thread.join();
              }




              struct Node {
              int val;
              Node *next;
              mutex m;

              Node(int val_, Node *next_ = nullptr) : val(val_), next(next_) {}
              };


              Defaulted function arguments are the devil. And, completely independently, so are implicit conversions! I would prefer to write this as:



              struct Node {
              int val;
              Node *next = nullptr;
              mutex m;

              explicit Node(int val_) : val(val_) {}
              explicit Node(int val_, Node *next_) : val(val_), next(next_) {}
              };


              or even



              struct Node {
              int val;
              Node *next;
              mutex m;

              explicit Node(int val_) : Node(val_, nullptr) {}
              explicit Node(int val_, Node *next_) : val(val_), next(next_) {}
              };




              Okay, now for the serious stuff.



              void erase(int pos);


              This is a really weird API for a linked list. For one thing, even for a non-concurrent linked list, you're turning "erase a node" into an O(n) operation. More importantly for a concurrent linked list, I can't see how this operation is useful at all! Suppose some thread says "please erase the 42nd element." How does it even know what the 42nd element is, given that any other thread could modify the list at any time?





              Your use of raw new and delete looks safe to me, but it would be a lot easier to verify its safety if you just didn't use raw new and delete! Consider this trivial rewrite of insert:



              void insert(int val, int pos) {
              auto new_node = std::make_unique<Node>(val);
              Node *prev = head_;
              unique_lock<mutex> prev_lk(prev->m);
              Node *node = prev->next;
              unique_lock<mutex> node_lk(node->m);
              for (int i = 0; i < pos && node != tail_; i++) {
              prev = node;
              node = node->next;
              prev_lk.swap(node_lk);
              node_lk = unique_lock<mutex>(node->m);
              }
              new_node->next = node;
              prev->next = new_node.release();
              }


              Only two lines changed, but now it's obvious that we never leak memory...





              ...except in the destructor of LockBasedLinkedList, which (being defaulted) just leaks every node remaining in the list! Was that intentional, or an oversight?





              Also consider the behavior of



              LockBasedLinkedList a;
              auto b = a;


              This certainly doesn't do what you want. What do you want it to do? If the answer is "I want it not to compile," then you should =delete your copy operations:



              LockBasedLinkedList(const LockBasedLinkedList&) = delete;
              LockBasedLinkedList& operator=(const LockBasedLinkedList&) = delete;




              In get:



                  if (node == tail_) {
              return 0;
              }


              How would your user distinguish the 0 that means "no such node" from the 0 that means "this node exists and has value 0"? I would strongly recommend designing your API so that this distinction is obvious to the user. For example:



              std::optional<int> get(int);





              How to return unique_lock in a function...?




              I think this is related to your get API, yeah? That is, maybe you want something like



              LockedNodePointer get(int);

              class LockedNodePointer {
              public:
              LockedNodePointer(std::nullptr_t) : ptr_(nullptr) {}
              Node& operator*() const { return *ptr_; }
              Node& operator->() const { return ptr_; }
              private:
              friend class LockBasedLinkedList;
              using Lock = std::unique_lock<std::mutex>;
              explicit LockedNodePointer(Lock lk, Node *ptr) : lk_(lk), ptr_(ptr) {}
              Lock lk_;
              Node *ptr_;
              };


              This is an even cleaner way to distinguish "node with value 0" from "no such node."





              It would also be interesting to consider: do you need a version of your get member function that is const-qualified? (Getting an element doesn't really modify the list, right?)



              Do you need two overloads of get — one const and one non-const?






              share|improve this answer























                up vote
                2
                down vote










                up vote
                2
                down vote









                Congratulations — I think this is the first concurrency-related code I've reviewed on CodeReview in which I have not managed to find any concurrency-related bugs! It looks like a solid implementation of "hand-over-hand" traversal. (Now that I've said that, I bet someone else will find a bug. :))



                First, a few stylistic nitpicks:



                using namespace std;


                Never! Just write std::vector<std::thread> if that's what you mean. The small space savings isn't worth dealing with all the people like me who'll tell you over and over not to write using namespace std; at global scope. :)



                const int N = 10;


                Make this constexpr int N = 10; and you won't need to capture a copy of it in your lambda.



                for (int i = 0; i < n; i++) {
                threads[i].join();
                }


                Prefer to write this with a ranged for loop:



                for (auto&& thread : threads) {
                thread.join();
                }




                struct Node {
                int val;
                Node *next;
                mutex m;

                Node(int val_, Node *next_ = nullptr) : val(val_), next(next_) {}
                };


                Defaulted function arguments are the devil. And, completely independently, so are implicit conversions! I would prefer to write this as:



                struct Node {
                int val;
                Node *next = nullptr;
                mutex m;

                explicit Node(int val_) : val(val_) {}
                explicit Node(int val_, Node *next_) : val(val_), next(next_) {}
                };


                or even



                struct Node {
                int val;
                Node *next;
                mutex m;

                explicit Node(int val_) : Node(val_, nullptr) {}
                explicit Node(int val_, Node *next_) : val(val_), next(next_) {}
                };




                Okay, now for the serious stuff.



                void erase(int pos);


                This is a really weird API for a linked list. For one thing, even for a non-concurrent linked list, you're turning "erase a node" into an O(n) operation. More importantly for a concurrent linked list, I can't see how this operation is useful at all! Suppose some thread says "please erase the 42nd element." How does it even know what the 42nd element is, given that any other thread could modify the list at any time?





                Your use of raw new and delete looks safe to me, but it would be a lot easier to verify its safety if you just didn't use raw new and delete! Consider this trivial rewrite of insert:



                void insert(int val, int pos) {
                auto new_node = std::make_unique<Node>(val);
                Node *prev = head_;
                unique_lock<mutex> prev_lk(prev->m);
                Node *node = prev->next;
                unique_lock<mutex> node_lk(node->m);
                for (int i = 0; i < pos && node != tail_; i++) {
                prev = node;
                node = node->next;
                prev_lk.swap(node_lk);
                node_lk = unique_lock<mutex>(node->m);
                }
                new_node->next = node;
                prev->next = new_node.release();
                }


                Only two lines changed, but now it's obvious that we never leak memory...





                ...except in the destructor of LockBasedLinkedList, which (being defaulted) just leaks every node remaining in the list! Was that intentional, or an oversight?





                Also consider the behavior of



                LockBasedLinkedList a;
                auto b = a;


                This certainly doesn't do what you want. What do you want it to do? If the answer is "I want it not to compile," then you should =delete your copy operations:



                LockBasedLinkedList(const LockBasedLinkedList&) = delete;
                LockBasedLinkedList& operator=(const LockBasedLinkedList&) = delete;




                In get:



                    if (node == tail_) {
                return 0;
                }


                How would your user distinguish the 0 that means "no such node" from the 0 that means "this node exists and has value 0"? I would strongly recommend designing your API so that this distinction is obvious to the user. For example:



                std::optional<int> get(int);





                How to return unique_lock in a function...?




                I think this is related to your get API, yeah? That is, maybe you want something like



                LockedNodePointer get(int);

                class LockedNodePointer {
                public:
                LockedNodePointer(std::nullptr_t) : ptr_(nullptr) {}
                Node& operator*() const { return *ptr_; }
                Node& operator->() const { return ptr_; }
                private:
                friend class LockBasedLinkedList;
                using Lock = std::unique_lock<std::mutex>;
                explicit LockedNodePointer(Lock lk, Node *ptr) : lk_(lk), ptr_(ptr) {}
                Lock lk_;
                Node *ptr_;
                };


                This is an even cleaner way to distinguish "node with value 0" from "no such node."





                It would also be interesting to consider: do you need a version of your get member function that is const-qualified? (Getting an element doesn't really modify the list, right?)



                Do you need two overloads of get — one const and one non-const?






                share|improve this answer












                Congratulations — I think this is the first concurrency-related code I've reviewed on CodeReview in which I have not managed to find any concurrency-related bugs! It looks like a solid implementation of "hand-over-hand" traversal. (Now that I've said that, I bet someone else will find a bug. :))



                First, a few stylistic nitpicks:



                using namespace std;


                Never! Just write std::vector<std::thread> if that's what you mean. The small space savings isn't worth dealing with all the people like me who'll tell you over and over not to write using namespace std; at global scope. :)



                const int N = 10;


                Make this constexpr int N = 10; and you won't need to capture a copy of it in your lambda.



                for (int i = 0; i < n; i++) {
                threads[i].join();
                }


                Prefer to write this with a ranged for loop:



                for (auto&& thread : threads) {
                thread.join();
                }




                struct Node {
                int val;
                Node *next;
                mutex m;

                Node(int val_, Node *next_ = nullptr) : val(val_), next(next_) {}
                };


                Defaulted function arguments are the devil. And, completely independently, so are implicit conversions! I would prefer to write this as:



                struct Node {
                int val;
                Node *next = nullptr;
                mutex m;

                explicit Node(int val_) : val(val_) {}
                explicit Node(int val_, Node *next_) : val(val_), next(next_) {}
                };


                or even



                struct Node {
                int val;
                Node *next;
                mutex m;

                explicit Node(int val_) : Node(val_, nullptr) {}
                explicit Node(int val_, Node *next_) : val(val_), next(next_) {}
                };




                Okay, now for the serious stuff.



                void erase(int pos);


                This is a really weird API for a linked list. For one thing, even for a non-concurrent linked list, you're turning "erase a node" into an O(n) operation. More importantly for a concurrent linked list, I can't see how this operation is useful at all! Suppose some thread says "please erase the 42nd element." How does it even know what the 42nd element is, given that any other thread could modify the list at any time?





                Your use of raw new and delete looks safe to me, but it would be a lot easier to verify its safety if you just didn't use raw new and delete! Consider this trivial rewrite of insert:



                void insert(int val, int pos) {
                auto new_node = std::make_unique<Node>(val);
                Node *prev = head_;
                unique_lock<mutex> prev_lk(prev->m);
                Node *node = prev->next;
                unique_lock<mutex> node_lk(node->m);
                for (int i = 0; i < pos && node != tail_; i++) {
                prev = node;
                node = node->next;
                prev_lk.swap(node_lk);
                node_lk = unique_lock<mutex>(node->m);
                }
                new_node->next = node;
                prev->next = new_node.release();
                }


                Only two lines changed, but now it's obvious that we never leak memory...





                ...except in the destructor of LockBasedLinkedList, which (being defaulted) just leaks every node remaining in the list! Was that intentional, or an oversight?





                Also consider the behavior of



                LockBasedLinkedList a;
                auto b = a;


                This certainly doesn't do what you want. What do you want it to do? If the answer is "I want it not to compile," then you should =delete your copy operations:



                LockBasedLinkedList(const LockBasedLinkedList&) = delete;
                LockBasedLinkedList& operator=(const LockBasedLinkedList&) = delete;




                In get:



                    if (node == tail_) {
                return 0;
                }


                How would your user distinguish the 0 that means "no such node" from the 0 that means "this node exists and has value 0"? I would strongly recommend designing your API so that this distinction is obvious to the user. For example:



                std::optional<int> get(int);





                How to return unique_lock in a function...?




                I think this is related to your get API, yeah? That is, maybe you want something like



                LockedNodePointer get(int);

                class LockedNodePointer {
                public:
                LockedNodePointer(std::nullptr_t) : ptr_(nullptr) {}
                Node& operator*() const { return *ptr_; }
                Node& operator->() const { return ptr_; }
                private:
                friend class LockBasedLinkedList;
                using Lock = std::unique_lock<std::mutex>;
                explicit LockedNodePointer(Lock lk, Node *ptr) : lk_(lk), ptr_(ptr) {}
                Lock lk_;
                Node *ptr_;
                };


                This is an even cleaner way to distinguish "node with value 0" from "no such node."





                It would also be interesting to consider: do you need a version of your get member function that is const-qualified? (Getting an element doesn't really modify the list, right?)



                Do you need two overloads of get — one const and one non-const?







                share|improve this answer












                share|improve this answer



                share|improve this answer










                answered 5 hours ago









                Quuxplusone

                10.5k11853




                10.5k11853






















                    nhtrnm is a new contributor. Be nice, and check out our Code of Conduct.










                     

                    draft saved


                    draft discarded


















                    nhtrnm is a new contributor. Be nice, and check out our Code of Conduct.













                    nhtrnm is a new contributor. Be nice, and check out our Code of Conduct.












                    nhtrnm is a new contributor. Be nice, and check out our Code of Conduct.















                     


                    draft saved


                    draft discarded














                    StackExchange.ready(
                    function () {
                    StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f207626%2flinked-list-node-level-locking%23new-answer', 'question_page');
                    }
                    );

                    Post as a guest




















































































                    Popular posts from this blog

                    Morgemoulin

                    Scott Moir

                    Souastre