<p><code>memory_order_acquire</code> makes only sense for operations that <em>read</em> a value, and <code>memory_order_release</code> makes only sense for operations that <em>write</em> a value. Since a read-modify-write operations reads and writes, it is possible to combine these memory orders, but it is not always necessary.</p>

<p>The <code>m_event.m_state.compare_exchange_weak</code> uses <code>memory_order_release</code> to write the new value, because it tries to replace a value that has previously been read using memory_order_acquire:</p>

<pre><code>  // load initial value using memory_order_acquire
  void* oldValue = m_event.m_state.load(std::memory_order_acquire);
  do {
    ...
  } while (!m_event.m_state.compare_exchange_weak(oldValue, this,
             std::memory_order_release,
             // in case of failure, load new value using memory_order_acquire
             std::memory_order_acquire));
</code></pre>

<p>IMHO in this case it is not even necessary to use memory_order_acquire at all, since oldValue is never de-referenced, but only stored as next pointer, i.e., it would be perfectly find to replace these two memory_order_acquire with memory_order_relaxed.</p>

<p>In <code>async_manual_reset_event::set()</code> the situtation is different:</p>

<pre><code>  void* oldValue = m_state.exchange(this, std::memory_order_acq_rel);
  if (oldValue != this)
  {
    auto* waiters = static_cast&lt;awaiter*&gt;(oldValue);
    while (waiters != nullptr)
    {
      // we are de-referencing the pointer read from m_state!
      auto* next = waiters-&gt;m_next;
      waiters-&gt;m_awaitingCoroutine.resume();
      waiters = next;
    }
</code></pre>

<p>Since we are de-referencing the pointer we read from <code>m_state</code>, we have to ensure that these reads <em>happen after</em> the writes to these waiter objects. This is ensured via the synchronize-with relation on <code>m_state</code>. The writer is added via the previously discussed compare_exchange using <code>memory_order_release</code>. The acquire-part of the exchange synchronizes with the release-compare_exchange (and in fact all prior release-compare_exchange that are part of the release sequence), thus providing the necessary happens-before relation.</p>

<p>To be honest, I am not sure why this exchange would need the release part. I think the author might have wanted to be on "the safe side", since several other operations are also stronger than necessary (I already mentioned that <code>await_suspend</code> does not need memory_order_acquire, and the same goes for <code>is_set</code> and <code>reset</code>).</p>

<p>For your lock implementation it is very simple - when you want to acquire the lock (<code>try_lock_shared</code>/<code>try_lock</code>) use <code>memory_order_acquire</code> for the compare-exchange operation only. Releasing the lock has to use <code>memory_order_release</code>.</p>

<p>The argument is also quite simple: you have to ensure that when you have acquired the lock, any changes previously made to the data protected by the lock is visible to the current owner, that is, you have to ensure that these changes <em>happened before</em> the operations you are about to perform <em>after acquiring the lock</em>. This is achieved by establishing a synchronize-with relation between the <code>try_lock</code> (acquire-CAS) and the previous <code>unlock</code> (release-store).</p>

<p>When trying to argue about the correctness of an implementation based on the semantics of the C++ memory model I usually do this as follows:</p>

<ol>
<li>identify the necessary happens-before relations (like for your lock)</li>
<li>make sure that these happens-before relations are established correctly on all code paths</li>
</ol>

<p>And I always annotate the atomic operations to document how these relations are established (i.e., which other operations are involved). For example:</p>

<pre><code>  // (1) - this acquire-load synchronizes-with the release-CAS (11)
  auto n = head.load(std::memory_order_acquire);

  // (8) - this acquire-load synchronizes-with the release-CAS (11)
  h.acquire(head, std::memory_order_acquire);

  // (11) - this release-CAS synchronizes-with the acquire-load (1, 8)
  if (head.compare_exchange_weak(expected, next, std::memory_order_release, std::memory_order_relaxed))
</code></pre>

<p>(see <a href="https://github.com/mpoeter/xenium/blob/master/xenium/michael_scott_queue.hpp" rel="nofollow noreferrer">https://github.com/mpoeter/xenium/blob/master/xenium/michael_scott_queue.hpp</a> for the full code)</p>

<p>For more details about the C++ memory model I can recommend this paper which I have co-authored: <a href="https://arxiv.org/abs/1803.04432" rel="nofollow noreferrer">Memory Models for C/C++ Programmers</a></p>
When can memory_order_acquire or memory_order_release be safely removed from compare_exchange?
<p>I refer to the code in Lewiss Baker's coroutine tutorial.</p>

<p><a href="https://lewissbaker.github.io/2017/11/17/understanding-operator-co-await" rel="nofollow noreferrer">https://lewissbaker.github.io/2017/11/17/understanding-operator-co-await</a></p>

<pre><code>bool async_manual_reset_event::awaiter::await_suspend(
  std::experimental::coroutine_handle&lt;&gt; awaitingCoroutine) noexcept
{
  // Special m_state value that indicates the event is in the 'set' state.
  const void* const setState = &amp;m_event;

  // Remember the handle of the awaiting coroutine.
  m_awaitingCoroutine = awaitingCoroutine;

  // Try to atomically push this awaiter onto the front of the list.
  void* oldValue = m_event.m_state.load(std::memory_order_acquire);
  do
  {
    // Resume immediately if already in 'set' state.
    if (oldValue == setState) return false; 

    // Update linked list to point at current head.
    m_next = static_cast&lt;awaiter*&gt;(oldValue);

    // Finally, try to swap the old list head, inserting this awaiter
    // as the new list head.
  } while (!m_event.m_state.compare_exchange_weak(
             oldValue,
             this,
             std::memory_order_release,
             std::memory_order_acquire));

  // Successfully enqueued. Remain suspended.
  return true;
}
</code></pre>

<p>where m_state is just a <code>std::atomic&lt;void *&gt;</code>.</p>

<pre><code>bool async_manual_reset_event::is_set() const noexcept
{
  return m_state.load(std::memory_order_acquire) == this;
}
void async_manual_reset_event::reset() noexcept
{
  void* oldValue = this;
  m_state.compare_exchange_strong(oldValue, nullptr, std::memory_order_acquire);
}
void async_manual_reset_event::set() noexcept
{
  // Needs to be 'release' so that subsequent 'co_await' has
  // visibility of our prior writes.
  // Needs to be 'acquire' so that we have visibility of prior
  // writes by awaiting coroutines.
  void* oldValue = m_state.exchange(this, std::memory_order_acq_rel);
  if (oldValue != this)
  {
    // Wasn't already in 'set' state.
    // Treat old value as head of a linked-list of waiters
    // which we have now acquired and need to resume.
    auto* waiters = static_cast&lt;awaiter*&gt;(oldValue);
    while (waiters != nullptr)
    {
      // Read m_next before resuming the coroutine as resuming
      // the coroutine will likely destroy the awaiter object.
      auto* next = waiters-&gt;m_next;
      waiters-&gt;m_awaitingCoroutine.resume();
      waiters = next;
    }
  }
}
</code></pre>

<p>Note in <code>m_state.exchange</code> of the <code>set()</code> method, the comment above shows clearly why the call to exchange requires both acquire and release.</p>

<p>I wonder why in the <code>m_state.compare_exchange_weak</code> of the <code>await_suspend()</code> method, the third parameter is a std::memory_order_release but not a memory_order_acq_rel (the acquire is removed).</p>

<p>The author (Lewis) did explain that we need release in the compare_exchange_weak because we need to let later set() see the writes in compare_exchange_weak. But why don't we require other compare_exchange_weak in other threads to see the writes in the current compare_exchange_weak?</p>

<p>Is it because of release sequence? I.e.,  in a release chain (write release at first, and all the middle operations are "read acquire then write release" operations, and the final operation is read acquire), then you don't need to tell them to acquire in the middle?</p>

<p>In the following code, I tried to implement a shared lock,</p>

<pre><code>    struct lock {
        uint64_t exclusive : 1;
        uint64_t id : 48;
        uint64_t shared_count : 15;
    };
    std::atomic&lt;lock&gt; lock_ { {0, 0, 0} };
    bool try_lock_shared() noexcept {
        lock currentlock = lock_.load(std::memory_order_acquire);
        if (currentlock.exclusive == 1) {
            return false;
        }
        lock newlock;
        do {
            newlock = currentlock;
            newlock.shared_count++;
        }
        while(!lock_.compare_exchange_weak(currentlock, newlock, std::memory_order_acq_rel) &amp;&amp; currentlock.exclusive == 0);

        return currentlock.exclusive == 0;
    }
    bool try_lock() noexcept {
        uint64_t id = utils::get_thread_id();
        lock currentlock = lock_.load(std::memory_order_acquire);
        if (currentlock.exclusive == 1) {
            assert(currentlock.id != id);
            return false;
        }

        bool result = false;
        lock newlock { 1, id, 0 };
        do {
            newlock.shared_count = currentlock.shared_count;
        }
        while(!(result = lock_.compare_exchange_weak(currentlock, newlock, std::memory_order_acq_rel)) &amp;&amp; currentlock.exclusive == 0);

        return result;
    }
</code></pre>

<p>I used <code>lock_.compare_exchange_weak(currentlock, newlock, std::memory_order_acq_rel)</code> everywhere, can I safely replace them to <code>compare_exchange_weak(currentlock, newlock, std::memory_order_release, std::memory_order_acquire)</code> ?</p>

<p>I could also see examples that <code>memory_order_release</code> is removed from <code>compare_exchange_strong</code> (see the <code>compare_exchange_strong</code> in <code>reset()</code> function of Lewis code), where you only need std::memory_order_acquire for compare_exchange_strong (but not release). I didn't really see memory_order_release is removed from weak nor memory_order_acquire is removed from strong.</p>

<p>This made me wonder whether there's deeper rule that I didn't understand or not.</p>

<p>Thanks.</p>