MMORPG Data Storage

Started by
38 comments, last by elFarto 15 years, 8 months ago
Quote:Original post by ToohrVyk
Quote:Original post by elFarto
A VersionedValue class stores it's values as a linked-list of values and the Transaction object that committed it.


So, if two different threads attempt to change a value, both will have to change the linked list, which means there will be a lock involved.

Quote:When you attempt to set a value that has already been set by a newer Transaction it just throws an exception, and the current Transaction is rolled-back.
This sounds like a (somewhat awkward) lock-based system.

No lock. It's an atomic compare-and-set. One thread will succeed, the other will fail. The exception is thrown when the CAS fails.

I also only insert new nodes into the linked-list's head. So I construct a new node, pointing to the current head, then CAS the new node with the current head.

Regards
elFarto
Advertisement
Quote:Original post by elFarto
No lock. It's an atomic compare-and-set. One thread will succeed, the other will fail. The exception is thrown when the CAS fails.


It is a lock. I'm not saying you used a lock, I'm saying you unknowingly implemented one. What should a thread do to get its modifications across? Basically, it will be
while (exception is thrown)   attempt to change data
This looks suspiciously like the typical naïve implementation of a lock:
while (compare-and-swap failed)   attempt compare-and-swap;


It's a consequence of transaction-based memory modifications: since transactions have to be serialized, any transaction will have to wait for all other previous transactions to be done, which in effect is equivalent to the transactions locking the modified resource. My point here is that, regardless of the transaction handling techniques you use (with or without locks, or whatever), transactions related to the same pieace of data still have to be done one after another, and thus cannot benefit from parallel processing.

I agree with your other point (lock-less atomic insertion at the head of a singly linked list).
Quote:Original post by ToohrVyk

  • Thread B begins a transaction for the same value, modifies the value,
  • OK.

    Quote:then it checks if another transaction already committed and gets the answer "no",
    Exactly after this point, the value is changed by another thread.

    Quote: so it prepares to commit.
    And you commited stale value.

  • Thread A also determines that no commit happened yet,
    After exactly this point, the change happens.

    Quote:and so it commits its modification.
    And it too commits stale value.

    Quote:
  • Thread B commits its modification.
  • During which point, actual value may be anything, since two threads already modified the data in between.

    Quote:So, the modifications from Thread A were dropped.


    No, they succeeded, since atomic swap doesn't prevent other threads from moving on, and atomic swap succeeded.

    The problem here isn't in atomic swap, those can check for validity. The problem lies in the fact that there's no transaction-wide guarantees.
    if (atomic_compare(a,b)) {  commit();}
    is not safe, unless entire statement is within a lock, or it atomic_compare is commit by itself. But as soon as you have two statements, you no longer have transactional guarantees.

    To me, this sounds exactly like the problem with the following:
    LockelssLinkedList ll;1) ll.add("An Item");2) assert(ll.pop_first() == "An Item");3) assert(ll.count() == 0);

    Though modifications are lock-less, the whole algorithm isn't a transaction.

    Count() is a nice example of an operation which is hard/impossible to implement on lock-less structures, or at best, has no real meaning. Although the list maintains integrity through entire process, the users operate on some non-deterministic data set.

    One problem that arises in relation to this is how to dynamically allocate the data (internal nodes) used by linked list. Even though you attempt to delete a pointer you retrieved from the list, there's no guarantee those contents are not being currently accessed by another thread within the LinkedList.

    IMHO, the whole system sounds like STM, and those are notoriously hard to get working right, especially with decent performance during contention. But it's been a while since I checked on progress, perhaps something has changed.

    Then again, I could be missing something.
    Quote:Original post by Antheus

    Er... Antheus, my post already was an example of how things could go wrong... [smile] and the modifications by thread A were dropped (that's the whole issue: they were commited, yet dropped) since the final value is the result of the commit by thread B, ignoring the modifications done by thread A.
    Quote:Original post by ToohrVyk
    It is a lock. I'm not saying you used a lock, I'm saying you unknowingly implemented one. What should a thread do to get its modifications across? Basically, it will be
    while (exception is thrown)   attempt to change data
    This looks suspiciously like the typical naïve implementation of a lock:
    while (compare-and-swap failed)   attempt compare-and-swap;


    True, I guess I've never considered it a lock, as it doesn't cause threads to be stopped waiting to aquire the lock. When a transaction fails, it's simply rescheduled to be re-run.

    The advantage to this is 1 transaction can be used across multiple fields.

    Quote:Original post by ToohrVyk
    It's a consequence of transaction-based memory modifications: since transactions have to be serialized, any transaction will have to wait for all other previous transactions to be done, which in effect is equivalent to the transactions locking the modified resource.

    That's not entirely true in a system with more than one field. Most transactions will be independent of each other. I apologise for not making it clear that 1 transaction covers multiple fields, e.g.

    Transaction t = new Transaction();Enemy e = EntityDB.get(id);e.setFoo(t, "new foo");Object o = e.getBar(t);EntityDB.add(t, new SuperEnemy());t.commit();

    All those changes will only become visible when the commit line is reached.

    Also, the example you (ToohrVyk) posted cannot happen. Which ever thread attempted to set (not commit) the value second would lose.

    Regards
    elFarto
    Quote:Original post by elFarto
    True, I guess I've never considered it a lock, as it doesn't cause threads to be stopped waiting to aquire the lock. When a transaction fails, it's simply rescheduled to be re-run.

    The advantage to this is 1 transaction can be used across multiple fields.
    Well, a lock doesn't need to stop threads. It could also let the threads do something useless. The benefits of a lock over attempt-change-and-throw-exception are: 1° the attempt-change might take longer than simply waiting for the lock to be released, 2° in the absence of locks long transactions tend to be interrupted more frequently and thus take longer to be completed and 3° a thread which rolls back used a processor for nothing when it could have been used by another thread.

    The multiple-fields idea is good, though, and avoids the usual deadlock problems, provided that the commit procedure works correctly.

    Quote:Original post by ToohrVyk
    That's not entirely true in a system with more than one field. Most transactions will be independent of each other. I apologise for not making it clear that 1 transaction covers multiple fields, e.g.
    I did consider this in my assertion: when transactions are independent, using locks will have the same effect as not using locks (since no transaction will wait for another to complete).

    Quote:Also, the example you (ToohrVyk) posted cannot happen. Which ever thread attempted to set (not commit) the value second would lose.
    How would it lose? Please explain the commit process better. EDIT: or are you saying that a thread will be rolled back if there is already an uncommitted transaction handling that data? This would make deadlocks a pain in the arse.
    Quote:Original post by Antheus
    One problem that arises in relation to this is how to dynamically allocate the data (internal nodes) used by linked list. Even though you attempt to delete a pointer you retrieved from the list, there's no guarantee those contents are not being currently accessed by another thread within the LinkedList.

    Cleaning all the old data is completely safe. You simply find the oldest running transaction, then step through every VersionValue instance, then find the oldest value it can see, and chop off all older values.

    You're gaurenteed that no other transaction will be using older values, since you already have the oldest transaction.

    Quote:Original post by AntheusIMHO, the whole system sounds like STM, and those are notoriously hard to get working right, especially with decent performance during contention. But it's been a while since I checked on progress, perhaps something has changed.

    That's because it is an STM, essentially.

    I'm not saying my idea is new or perfect in everyway. It just seems good enough for what I need.

    Regards
    elFarto
    Quote:Original post by ToohrVyk
    EDIT: or are you saying that a thread will be rolled back if there is already an uncommitted transaction handling that data? This would make deadlocks a pain in the arse.

    Correct, and I realise this. However it's the quickest thing todo, as soon as you know a transaction has touch the data you want, that transaction get terminated.

    In the case that Transactions A touches Object A, and Transactions B touches Object B, then they attempt to set the other object, it's possible for both transactions to be killed (although it's a slim time-frame for it to occur in).

    Regards
    elFarto
    Quote:Original post by elFarto

    You're gaurenteed that no other transaction will be using older values, since you already have the oldest transaction.


    Not the transaction, the linked list. The update procedures might be touching the nodes that you consider safe.

    This depends on the implementation obviously, but that is an annoying problem with some data structures. One solution is to use only pre-allocated nodes, or some GC system. The problem doesn't appear in Java, for example.

    The in-use here applies to implementation of linked list and its internal storage, not the data.
    Quote:Original post by Antheus
    The problem doesn't appear in Java, for example.

    Which is what I'm using, so obviously no problem for me.

    Regards
    elFarto

    This topic is closed to new replies.

    Advertisement