title: Bad lock optimization in asyncio
body: <p><strong>Update: Edited title to focus on the main problem. See my answer for full update.</strong></p>
  
  <p>In the following code, <code>a()</code> and <code>b()</code> are identical. Each of them counts from 0 to 9 concurrently while acquiring and yielding a lock every 2 counts.</p>
  
  <pre><code>import asyncio
  
  lock = asyncio.Lock()
  
  def a ():
   yield from lock.acquire()
   for i in range(10):
    print('a: ' + str(i))
    if i % 2 == 0:
     lock.release()
     yield from lock.acquire()
   lock.release()
  
  def b ():
   yield from lock.acquire()
   for i in range(10):
    print('b: ' + str(i))
    if i % 2 == 0:
     lock.release()
     yield from lock.acquire()
   lock.release()
  
  asyncio.get_event_loop().run_until_complete(asyncio.gather(a(), b()))
  
  print('done')
  </code></pre>
  
  <p>I expected interleaved output, but instead I get:</p>
  
  <pre><code>b: 0
  b: 1
  b: 2
  b: 3
  b: 4
  b: 5
  b: 6
  b: 7
  b: 8
  b: 9
  a: 0
  a: 1
  a: 2
  a: 3
  a: 4
  a: 5
  a: 6
  a: 7
  a: 8
  a: 9
  done
  </code></pre>
  
  <p>It seems that the second <code>yield</code> does not actually yield but instead reacquires the lock immediately and continues.</p>
  
  <p><strong>That seems like a bug to me. Am I right? or is there another explanation?</strong></p>
  
  <p>The following code, modified with an extra initial "noop" <code>yield</code>, works fine as expected. That makes me believe that the lock is indeed fair and probably correct.</p>
  
  <pre><code>import asyncio
  
  lock = asyncio.Lock()
  
  def a ():
   yield from lock.acquire()
   yield from asyncio.sleep(0)
   for i in range(10):
    print('a: ' + str(i))
    if i % 2 == 0:
     lock.release()
     yield from lock.acquire()
   lock.release()
  
  def b ():
   yield from lock.acquire()
   yield from asyncio.sleep(0)
   for i in range(10):
    print('b: ' + str(i))
    if i % 2 == 0:
     lock.release()
     yield from lock.acquire()
   lock.release()
  
  asyncio.get_event_loop().run_until_complete(asyncio.gather(a(), b()))
  
  print('done')
  </code></pre>
  
  <p>Output:</p>
  
  <pre><code>a: 0
  b: 0
  a: 1
  a: 2
  b: 1
  b: 2
  a: 3
  a: 4
  b: 3
  b: 4
  a: 5
  a: 6
  b: 5
  b: 6
  a: 7
  a: 8
  b: 7
  b: 8
  a: 9
  b: 9
  done
  </code></pre>
  
  <p>Note that I am doing the no-op yield just once at the beginning, and not every 2 counts. Yet doing so causes the interleaving every 2 counts as expected in the first piece of code.</p>
  
  <p>There is just some optimization (a bug in my opinion) in the scheduler that doesn't really <code>yield</code> when acquiring a lock that nobody else is waiting for.</p>
  
  <p>How else to explain the first output?</p>