Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Unbounded Mpsc channels issue #19

Closed
mratsim opened this issue Nov 22, 2019 · 6 comments
Closed

Unbounded Mpsc channels issue #19

mratsim opened this issue Nov 22, 2019 · 6 comments
Labels
bug 🪲 Something isn't working concurrency 💥

Comments

@mratsim
Copy link
Owner

mratsim commented Nov 22, 2019

There is 2 strange issues in the branch https://github.com/mratsim/weave/tree/test-lockless-unbounded-mpsc.

edit: This issue will focus on the unbounded channel issue

@mratsim
Copy link
Owner Author

mratsim commented Nov 22, 2019

Detailed logs on the second problem with only 4 threads:

Worker 3: schedloop 1 - task from local deque
Worker 3: schedloop 2 - becoming a thief
Worker 2: schedloop 1 - task from local deque
>>> Worker 0 enters barrier <<<
Worker 0: globalsync 1 - task from local deque
Worker 1: schedloop 1 - task from local deque
Worker 1: schedloop 2 - becoming a thief
Worker 1: sending own steal request to 2
654321 - SUCCESS
123456 - SUCCESS
Worker 0: globalsync 2 - becoming a thief
Worker 0: sending own steal request to 1
Worker 1: receives request from 0 with 3 potential victims.
Worker 1: relay steal request to 2 from 0
Worker 3: sending own steal request to 1
Worker 1: receives request from 3 with 3 potential victims.
Worker 1: relay steal request to 2 from 3
Worker 2: receives request from 1 with 3 potential victims.
Worker 2: relay steal request to 0 from 1
Worker 2: schedloop 2 - becoming a thief
Worker 2: sending own steal request to 3
Worker 3: receives request from 2 with 3 potential victims.
Worker 3: relay steal request to 0 from 2
Worker 2: receives request from 2 with 2 potential victims.
Worker 0: receives request from 1 with 2 potential victims.
Worker 0: relay steal request to 3 from 1
victims: [0, 1]
Worker 3: receives request from 1 with 1 potential victims.
Worker 3: relay steal request to 1 from 1
Worker 1: receives request from 1 with 0 potential victims.
victims: []
Worker 1: received own request (req.state: Stealing, left: 0, right 1)
Worker 1: relay steal request to 0 from 1
/home/beta/Programming/Nim/weave/weave/scheduler.nim(182) worker_entry_fn
/home/beta/Programming/Nim/weave/weave/scheduler.nim(124) schedulingLoop
/home/beta/Programming/Nim/weave/weave/scheduler.nim(90) declineAll
/home/beta/Programming/Nim/weave/weave/victims.nim(116) decline
/home/beta/Programming/Nim/weave/weave/instrumentation/contracts.nim(66) declineOwn
/home/beta/.choosenim/toolchains/nim-1.0.2/lib/system/assertions.nim(27) failedAssertImpl
/home/beta/.choosenim/toolchains/nim-1.0.2/lib/system/assertions.nim(20) raiseAssert
/home/beta/.choosenim/toolchains/nim-1.0.2/lib/system/fatal.nim(39) sysFatal
Error: unhandled exception: /home/beta/Programming/Nim/weave/weave/instrumentation/contracts.nim(66, 13) `
ID > -1 and req.victims.isEmpty(myID())` 
    Contract violated for pre-condition at victims.nim:77
        ID > -1 and req.victims.isEmpty(myID())
    The following values are contrary to expectations:
        2 > -1 and req.victims.isEmpty(myID())
 [AssertionError]

@mratsim
Copy link
Owner Author

mratsim commented Nov 22, 2019

Need gdb for debugging, seems like they send termination requests to themselves :/

DeepinScreenshot_select-area_20191122224054

Even though the channels are different, ...
DeepinScreenshot_select-area_20191122225015

@mratsim
Copy link
Owner Author

mratsim commented Nov 22, 2019

Well seems like the bug may be linked to nim-lang/Nim#12695.

In particular the casting may create a copy or something and I need to cast via pointers as Nim may do union type casting.

cast[T](oldBack).next.store(src, moRelaxed) # Workaround generic atomics bug: https://github.com/nim-lang/Nim/issues/12695
discard chan.count.fetchAdd(1, moRelaxed)
return true
proc tryRecv*[T](chan: var ChannelMpscUnbounded[T], dst: var T): bool =
## Try receiving the next item buffered in the channel
## Returns true if successful (channel was not empty)
assert not(chan.front.isNil)
assert not(chan.back.load(moRelaxed).isNil)
let first = chan.front # dummy
let next = cast[T](first.next.load(moRelaxed)) # Workaround generic atomics bug: https://github.com/nim-lang/Nim/issues/12695

@mratsim
Copy link
Owner Author

mratsim commented Nov 22, 2019

Reading symbols from ./build/async_lockless...
(gdb) run
Starting program: /home/beta/Programming/Nim/weave/build/async_lockless 
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/usr/lib/libthread_db.so.1".
[New Thread 0x7ffff7b32700 (LWP 1406849)]
Worker 1: schedloop 1 - task from local deque
>>> Worker 0 enters barrier <<<
Worker 0: globalsync 1 - task from local deque
Worker 1: schedloop 2 - becoming a thief
654321Worker 1: sending own steal request to 0
Worker 1: sending request 0xf0000d10 to 0 (Channel: 0x555b15e0)
Channel 0x555b15e0 sending 0xf0000d10
 - SUCCESS
Channel 0x555b15e0 receiving 0xf0000d10
Worker 0: receives request 0xf0000d10 from 1 with 1 potential victims. (Channel: 0x555b15e0)
Worker 0: relay steal request to 1 from 1
Worker 0: sending request 0xf0000d10 to 1 (Channel: 0x555b16e8)
Channel 0x555b16e8 sending 0xf0000d10
123456Channel 0x555b16e8 receiving 0xf0000d10
Worker 1: receives request 0xf0000d10 from 1 with 0 potential victims. (Channel: 0x555b16e8)
 - SUCCESS
Worker 0: globalsync 2 - becoming a thief
Worker 0: sending own steal request to 1
Worker 0: sending request 0x555b1cd0 to 1 (Channel: 0x555b16e8)
Channel 0x555b16e8 sending 0x555b1cd0
victims: []
Worker 1: received own request (req.state: Stealing, left: 1, right 1)
Worker 1 sends state passively WAITING to its parent worker 0
Worker 1: sending request 0xf0000d10 to 0 (Channel: 0x555b15e0)
Channel 0x555b15e0 sending 0xf0000d10
Channel 0x555b16e8 receiving 0xf0000d10
Worker 1: receives request 0xf0000d10 from 1 with 0 potential victims. (Channel: 0x555b16e8)
Worker 1 receives state passively WAITING from its child worker 1
/home/beta/Programming/Nim/weave/weave/scheduler.nim(182) worker_entry_fn
/home/beta/Programming/Nim/weave/weave/scheduler.nim(124) schedulingLoop
/home/beta/Programming/Nim/weave/weave/scheduler.nim(87) declineAll
/home/beta/Programming/Nim/weave/weave/instrumentation/contracts.nim(66) recv
/home/beta/.choosenim/toolchains/nim-1.0.2/lib/system/assertions.nim(27) failedAssertImpl
/home/beta/.choosenim/toolchains/nim-1.0.2/lib/system/assertions.nim(20) raiseAssert
/home/beta/.choosenim/toolchains/nim-1.0.2/lib/system/fatal.nim(39) sysFatal
Error: unhandled exception: /home/beta/Programming/Nim/weave/weave/instrumentation/contracts.nim(66, 13) `
req.thiefID == myWorker().left or req.thiefID == myWorker().right` 
    Contract violated for transient condition at victims.nim:54
        req.thiefID == myWorker().left or req.thiefID == myWorker().right
    The following values are contrary to expectations:
        req.thiefID == myWorker().left or req.thiefID == myWorker().right
 [AssertionError]
Couldn't read debug register: No such process.
(gdb) [Thread 0x7ffff7b32700 (LWP 1406849) exited]

@mratsim mratsim changed the title Sparsesets, strange issue in contains, incl, excl Unbounded Mpsc channels and/or victims Sparsesets, strange issue in contains, incl, excl, isEmpty Nov 22, 2019
@mratsim
Copy link
Owner Author

mratsim commented Nov 23, 2019

With some modification

proc trySend*[T](chan: var ChannelMpscUnbounded[T], src: sink T): bool =
  ## Send an item to the back of the channel
  ## As the channel as unbounded capacity, this should never fail
  assert not(chan.front.isNil)
  assert not(chan.back.load(moRelaxed).isNil)

  log("Channel 0x%.08x sending 0x%.08x (next: 0x%.08x)\n", chan.addr, cast[ByteAddress](src), cast[ByteAddress](src.next))

  src.next.store(nil, moRelaxed)
  fence(moRelease)
  let oldBack = chan.back.exchange(src, moRelaxed)

  # Workaround generic atomics bug: https://github.com/nim-lang/Nim/issues/12695
  # And make sure we cast through pointers to avoid leaving wild copies around
  # https://github.com/mratsim/weave/issues/19#issuecomment-557705071
  let oldBackT = cast[ptr T](oldBack.unsafeAddr)
  oldBackT.next.store(src, moRelaxed)
  discard chan.count.fetchAdd(1, moRelaxed)

  return true

proc tryRecv*[T](chan: var ChannelMpscUnbounded[T], dst: var T): bool =
  ## Try receiving the next item buffered in the channel
  ## Returns true if successful (channel was not empty)
  assert not(chan.front.isNil)
  assert not(chan.back.load(moRelaxed).isNil)

  let first = chan.front # dummy
  # Workaround generic atomics bug: https://github.com/nim-lang/Nim/issues/12695
  # And make sure we cast through pointers to avoid leaving wild copies around
  # https://github.com/mratsim/weave/issues/19#issuecomment-557705071
  let nextTypeErased = first.next.load(moRelaxed)
  let next = cast[ptr T](nextTypeErased.unsafeAddr)[]

  if next.isNil:
    fence(moAcquire)
    dst = nil
    return false

  log("Channel 0x%.08x receiving 0x%.08x (next: 0x%.08x)\n", chan.addr, cast[ByteAddress](next), cast[ByteAddress](next.next))

  chan.front = next
  prefetch(first.next.load(moRelaxed))
  fence(moAcquire)
  dst = next

  discard chan.count.fetchSub(1, moRelaxed)
  return true
Starting program: /home/beta/Programming/Nim/weave/build/async_lockless 
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/usr/lib/libthread_db.so.1".
[New Thread 0x7ffff7b32700 (LWP 1409536)]
Worker 1: schedloop 1 - task from local deque
>>> Worker 0 enters barrier <<<
Worker 0: globalsync 1 - task from local deque
Worker 1: schedloop 2 - becoming a thief
Worker 1: sending own steal request to 0
Worker 1: sending request 0xf0000d10 to 0 (Channel: 0x555b15e0)
Channel 0x555b15e0 sending 0xf0000d10 (next: 0x00000000)
654321 - SUCCESS
Channel 0x555b15e0 receiving 0xf0000d10 (next: 0x00000000)
Worker 0: receives request 0xf0000d10 from 1 with 1 potential victims. (Channel: 0x555b15e0)
Worker 0: relay steal request to 1 from 1
Worker 0: sending request 0xf0000d10 to 1 (Channel: 0x555b16e8)
Channel 0x555b16e8 sending 0xf0000d10 (next: 0x00000000)
Channel 0x555b16e8 receiving 0xf0000d10 (next: 0x00000000)
Worker 1: receives request 0xf0000d10 from 1 with 0 potential victims. (Channel: 0x555b16e8)
123456victims: []
Worker 1: received own request (req.state: Stealing, left: 1, right 1)
Worker 1 sends state passively WAITING to its parent worker 0
Worker 1: sending request 0xf0000d10 to 0 (Channel: 0x555b15e0)
Channel 0x555b15e0 sending 0xf0000d10 (next: 0x00000000)
Channel 0x555b16e8 receiving 0xf0000d10 (next: 0xf0000d10)
Worker 1: receives request 0xf0000d10 from 1 with 0 potential victims. (Channel: 0x555b16e8)
Worker 1 receives state passively WAITING from its child worker 1
 - SUCCESS
Channel 0x555b15e0 receiving 0xf0000d10 (next: 0xf0000d10)
Worker 0: receives request 0xf0000d10 from 1 with 0 potential victims. (Channel: 0x555b15e0)
Worker 0 receives state passively WAITING from its child worker 1
Channel 0x555b15e0 receiving 0xf0000d10 (next: 0xf0000d10)
Worker 0 receives state passively WAITING from its child worker 1
/home/beta/Programming/Nim/weave/weave/scheduler.nim(182) worker_entry_fn
/home/beta/Programming/Nim/weave/weave/scheduler.nim(124) schedulingLoop
/home/beta/Programming/Nim/weave/weave/scheduler.nim(87) declineAll
/home/beta/Programming/Nim/weave/weave/instrumentation/contracts.nim(66) recv
/home/beta/.choosenim/toolchains/nim-1.0.2/lib/system/assertions.nim(27) failedAssertImpl
/home/beta/.choosenim/toolchains/nim-1.0.2/lib/system/assertions.nim(20) raiseAssert
/home/beta/.choosenim/toolchains/nim-1.0.2/lib/system/fatal.nim(39) sysFatal
Error: unhandled exception: /home/beta/Programming/Nim/weave/weave/instrumentation/contracts.nim(66, 13) `
req.thiefID == myWorker().left or req.thiefID == myWorker().right` 
    Contract violated for transient condition at victims.nim:54
        req.thiefID == myWorker().left or req.thiefID == myWorker().right
    The following values are contrary to expectations:
        req.thiefID == myWorker().left or req.thiefID == myWorker().right
 [AssertionError]
/home/beta/Programming/Nim/weave/weave/async.nim(151) async
/home/beta/Programming/Nim/weave/weave/async.nim(148) main
/home/beta/Programming/Nim/weave/weave/runtime.nim(118) sync
/home/beta/Programming/Nim/weave/weave/scheduler.nim(71) nextTask
/home/beta/Programming/Nim/weave/weave/instrumentation/contracts.nim(66) recv
/home/beta/.choosenim/toolchains/nim-1.0.2/lib/system/assertions.nim(27) failedAssertImpl
/home/beta/.choosenim/toolchains/nim-1.0.2/lib/system/assertions.nim(20) raiseAssert
/home/beta/.choosenim/toolchains/nim-1.0.2/lib/system/fatal.nim(39) sysFatal
Error: unhandled exception: /home/beta/Programming/Nim/weave/weave/instrumentation/contracts.nim(66, 13) `
not myWorker().leftIsWaiting` 
    Contract violated for transient condition at victims.nim:57
        not myWorker().leftIsWaiting
    The following values are contrary to expectations:
        not myWorker().leftIsWaiting
 [AssertionError]
[Thread 0x7ffff7b32700 (LWP 1409536) exited]
[Inferior 1 (process 1409532) exited with code 01]

@mratsim mratsim changed the title Unbounded Mpsc channels and/or victims Sparsesets, strange issue in contains, incl, excl, isEmpty Unbounded Mpsc channels issue Nov 23, 2019
@mratsim
Copy link
Owner Author

mratsim commented Nov 23, 2019

It seems like there is an issue with snmalloc channels (adapted from Pony's) or I adapted them improperly.

snmalloc pseudo code from paper

DeepinScreenshot_select-area_20191123105253

DeepinScreenshot_select-area_20191123105346

image

Code from: https://github.com/microsoft/snmalloc/blob/7faefbbb0ed69554d0e19bfe901ec5b28e046a82/src/ds/mpscq.h#L29-L83

    void init(T* stub)
    {
      stub->next.store(nullptr, std::memory_order_relaxed);
      front = stub;
      back.store(stub, std::memory_order_relaxed);
      invariant();
    }

    T* destroy()
    {
      T* fnt = front;
      back.store(nullptr, std::memory_order_relaxed);
      front = nullptr;
      return fnt;
    }

    inline bool is_empty()
    {
      T* bk = back.load(std::memory_order_relaxed);

      return bk == front;
    }

    void enqueue(T* first, T* last)
    {
      // Pushes a list of messages to the queue. Each message from first to
      // last should be linked together through their next pointers.
      invariant();
      last->next.store(nullptr, std::memory_order_relaxed);
      std::atomic_thread_fence(std::memory_order_release);
      T* prev = back.exchange(last, std::memory_order_relaxed);
      prev->next.store(first, std::memory_order_relaxed);
    }

    std::pair<T*, bool> dequeue()
    {
      // Returns the front message, or null if not possible to return a message.
      invariant();
      T* first = front;
      T* next = first->next.load(std::memory_order_relaxed);

      if (next != nullptr)
      {
        front = next;
        AAL::prefetch(&(next->next));
        assert(front);
        std::atomic_thread_fence(std::memory_order_acquire);
        invariant();
        return std::pair(first, true);
      }

      return std::pair(nullptr, false);
    }
  };
} // namespace snmalloc

Pony: https://qconlondon.com/london-2016/system/files/presentation-slides/sylvanclebsch.pdf

DeepinScreenshot_select-area_20191123110607

DeepinScreenshot_select-area_20191123110621

Code from https://github.com/ponylang/ponyc/blob/7145c2a84b68ae5b307f0756eee67c222aa02fda/src/libponyrt/actor/messageq.c

Note that Pony enqueue at the head and dequeue at the tail

static bool messageq_push(messageq_t* q, pony_msg_t* first, pony_msg_t* last)
{
  atomic_store_explicit(&last->next, NULL, memory_order_relaxed);

  // Without that fence, the store to last->next above could be reordered after
  // the exchange on the head and after the store to prev->next done by the
  // next push, which would result in the pop incorrectly seeing the queue as
  // empty.
  // Also synchronise with the pop on prev->next.
  atomic_thread_fence(memory_order_release);

  pony_msg_t* prev = atomic_exchange_explicit(&q->head, last,
    memory_order_relaxed);

  bool was_empty = ((uintptr_t)prev & 1) != 0;
  prev = (pony_msg_t*)((uintptr_t)prev & ~(uintptr_t)1);

#ifdef USE_VALGRIND
  // Double fence with Valgrind since we need to have prev in scope for the
  // synchronisation annotation.
  ANNOTATE_HAPPENS_BEFORE(&prev->next);
  atomic_thread_fence(memory_order_release);
#endif
  atomic_store_explicit(&prev->next, first, memory_order_relaxed);

  return was_empty;
}

void ponyint_messageq_init(messageq_t* q)
{
  pony_msg_t* stub = POOL_ALLOC(pony_msg_t);
  stub->index = POOL_INDEX(sizeof(pony_msg_t));
  atomic_store_explicit(&stub->next, NULL, memory_order_relaxed);

  atomic_store_explicit(&q->head, (pony_msg_t*)((uintptr_t)stub | 1),
    memory_order_relaxed);
  q->tail = stub;

#ifndef PONY_NDEBUG
  messageq_size_debug(q);
#endif
}

pony_msg_t* ponyint_thread_messageq_pop(messageq_t* q
#ifdef USE_DYNAMIC_TRACE
  , uintptr_t thr
#endif
  )
{
  pony_msg_t* tail = q->tail;
  pony_msg_t* next = atomic_load_explicit(&tail->next, memory_order_relaxed);

  if(next != NULL)
  {
    DTRACE2(THREAD_MSG_POP, (uint32_t) next->id, (uintptr_t) thr);
    q->tail = next;
    atomic_thread_fence(memory_order_acquire);
#ifdef USE_VALGRIND
    ANNOTATE_HAPPENS_AFTER(&tail->next);
    ANNOTATE_HAPPENS_BEFORE_FORGET_ALL(tail);
#endif
    ponyint_pool_free(tail->index, tail);
  }

  return next;
}

bool ponyint_messageq_markempty(messageq_t* q)
{
  pony_msg_t* tail = q->tail;
  pony_msg_t* head = atomic_load_explicit(&q->head, memory_order_relaxed);

  if(((uintptr_t)head & 1) != 0)
    return true;

  if(head != tail)
    return false;

  head = (pony_msg_t*)((uintptr_t)head | 1);

#ifdef USE_VALGRIND
  ANNOTATE_HAPPENS_BEFORE(&q->head);
#endif
  return atomic_compare_exchange_strong_explicit(&q->head, &tail, head,
    memory_order_release, memory_order_relaxed);
}

However, I probably have missed something here but it seems to me like:

  • The back of the queue (head in Pony) is never modified in the enqueue/pop routines.
    This is problematic as it would still points to the item that was just dequeued
  • In snmalloc the queue front is overwritten by next, meaning the initial dummy node would be overwritten on first dequeue. But its memory is never freed/recycled.
  • As they both erase the front in the dequeue/pop, but the dequeuing only advances when there is more than one item in the queue, the consumer will get blocked on the last message.
  • Snmalloc keeps an atomic count of enqueued objects next to the queue.
    In our case, both for the memory pool and for steal requests (for the adaptative steal strategy) we also need to keep an approximate count of enqueued items, we might as well integrate it in the queue.

mratsim added a commit that referenced this issue Nov 23, 2019
- they hold on the last item of a queue (breaking for steal requests)
- they require memory management of the dummy node (snmalloc deletes it and its memory doesn't seem to be reclaimed)
- they never touch the "back" pointer of the queue when dequeuing, meaning if an item was last, dequeuing will still points to it. Pony has an emptiness check via tagged pointer and snmalloc does ???
@mratsim mratsim added bug 🪲 Something isn't working concurrency 💥 labels Jan 1, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug 🪲 Something isn't working concurrency 💥
Projects
None yet
Development

No branches or pull requests

1 participant