//===--- Mutex.cpp - Mutex and ReadWriteLock Tests ------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2017 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//

#include "swift/Runtime/Mutex.h"
#include "gtest/gtest.h"
#include <atomic>
#include <chrono>
#include <map>
#include <random>
#include <thread>

using namespace swift;

// When true many of the threaded tests log activity to help triage issues.
static bool trace = false;

template <typename ThreadBody, typename AfterSpinRelease>
void threadedExecute(int threadCount, ThreadBody threadBody,
                     AfterSpinRelease afterSpinRelease) {

  std::vector<std::thread> threads;

  // Block the threads we are about to create.
  std::atomic<bool> spinWait(true);
  std::atomic<int> readyCount(0);
  std::atomic<int> activeCount(0);

  for (int i = 0; i < threadCount; ++i) {
    threads.push_back(std::thread([&, i] {
      readyCount++;

      while (spinWait) {
        std::this_thread::sleep_for(std::chrono::microseconds(10));
      }
      std::this_thread::sleep_for(std::chrono::milliseconds(1));

      activeCount++;

      threadBody(i);
    }));
  }

  while (readyCount < threadCount) {
    std::this_thread::sleep_for(std::chrono::milliseconds(1));
  }

  // Allow our threads to fight for the lock.
  spinWait = false;

  while (activeCount < threadCount) {
    std::this_thread::sleep_for(std::chrono::milliseconds(1));
  }

  afterSpinRelease();

  // Wait until all of our threads have finished.
  for (auto &thread : threads) {
    thread.join();
  }
}

template <typename ThreadBody>
void threadedExecute(int threadCount, ThreadBody threadBody) {
  threadedExecute(threadCount, threadBody, [] {});
}

template <typename M, typename C, typename ConsumerBody, typename ProducerBody>
void threadedExecute(M &mutex, C &condition, bool &doneCondition,
                     ConsumerBody consumerBody, ProducerBody producerBody) {

  std::vector<std::thread> producers;
  std::vector<std::thread> consumers;

  // Block the threads we are about to create.
  std::atomic<bool> spinWait(true);

  for (int i = 1; i <= 8; ++i) {
    consumers.push_back(std::thread([&, i] {
      while (spinWait) {
        std::this_thread::sleep_for(std::chrono::microseconds(10));
      }
      std::this_thread::sleep_for(std::chrono::milliseconds(1));

      consumerBody(i);

      if (trace)
        printf("### Consumer[%d] thread exiting.\n", i);
    }));
  }

  for (int i = 1; i <= 5; ++i) {
    producers.push_back(std::thread([&, i] {
      while (spinWait) {
        std::this_thread::sleep_for(std::chrono::microseconds(10));
      }
      std::this_thread::sleep_for(std::chrono::milliseconds(1));

      producerBody(i);

      if (trace)
        printf("### Producer[%d] thread exiting.\n", i);
    }));
  }

  // Poor mans attempt to get as many threads ready as possible before
  // dropping spinWait, it doesn't have to be perfect.
  std::this_thread::sleep_for(std::chrono::milliseconds(100));

  // Allow our threads to fight for the lock.
  spinWait = false;

  // Wait until all of our producer threads have finished.
  for (auto &thread : producers) {
    thread.join();
  }

  // Inform consumers that producers are done.
  mutex.withLockThenNotifyAll(condition, [&] {
    if (trace)
      printf("### Informing consumers we are done.\n");
    doneCondition = true;
  });

  // Wait for consumers to finish.
  for (auto &thread : consumers) {
    thread.join();
  }
}

// -----------------------------------------------------------------------------

template <typename M> void basicLockableThreaded(M &mutex) {
  int count1 = 0;
  int count2 = 0;

  threadedExecute(10, [&](int) {
    for (int j = 0; j < 50; ++j) {
      mutex.lock();
      auto count = count2;
      count1++;
      count2 = count + 1;
      mutex.unlock();
    }
  });

  ASSERT_EQ(count1, 500);
  ASSERT_EQ(count2, 500);
}

TEST(MutexTest, BasicLockableThreaded) {
  Mutex mutex(/* checked = */ true);
  basicLockableThreaded(mutex);
}

TEST(StaticMutexTest, BasicLockableThreaded) {
  static StaticMutex mutex;
  basicLockableThreaded(mutex);
}

TEST(StaticUnsafeMutexTest, BasicLockableThreaded) {
  static StaticUnsafeMutex mutex;
  basicLockableThreaded(mutex);
}

template <typename M> void lockableThreaded(M &mutex) {
  mutex.lock();
  threadedExecute(5, [&](int) { ASSERT_FALSE(mutex.try_lock()); });
  mutex.unlock();
  threadedExecute(1, [&](int) {
    ASSERT_TRUE(mutex.try_lock());
    mutex.unlock();
  });

  int count1 = 0;
  int count2 = 0;
  threadedExecute(10, [&](int) {
    for (int j = 0; j < 50; ++j) {
      if (mutex.try_lock()) {
        auto count = count2;
        count1++;
        count2 = count + 1;
        mutex.unlock();
      } else {
        j--;
      }
    }
  });

  ASSERT_EQ(count1, 500);
  ASSERT_EQ(count2, 500);
}

TEST(MutexTest, LockableThreaded) {
  Mutex mutex(/* checked = */ true);
  lockableThreaded(mutex);
}

TEST(StaticMutexTest, LockableThreaded) {
  static StaticMutex Mutex;
  lockableThreaded(Mutex);
}

template <typename SL, typename M> void scopedLockThreaded(M &mutex) {
  int count1 = 0;
  int count2 = 0;

  threadedExecute(10, [&](int) {
    for (int j = 0; j < 50; ++j) {
      SL guard(mutex);
      auto count = count2;
      count1++;
      count2 = count + 1;
    }
  });

  ASSERT_EQ(count1, 500);
  ASSERT_EQ(count2, 500);
}

TEST(MutexTest, ScopedLockThreaded) {
  Mutex mutex(/* checked = */ true);
  scopedLockThreaded<ScopedLock>(mutex);
}

TEST(StaticMutexTest, ScopedLockThreaded) {
  static StaticMutex Mutex;
  scopedLockThreaded<StaticScopedLock>(Mutex);
}

template <typename SL, typename SU, typename M>
void scopedUnlockUnderScopedLockThreaded(M &mutex) {
  int count1 = 0;
  int count2 = 0;
  int badCount = 0;

  threadedExecute(10, [&](int) {
    for (int j = 0; j < 50; ++j) {
      SL guard(mutex);
      {
        SU unguard(mutex);
        badCount++;
      }
      auto count = count2;
      count1++;
      count2 = count + 1;
    }
  });

  ASSERT_EQ(count1, 500);
  ASSERT_EQ(count2, 500);
}

TEST(MutexTest, ScopedUnlockUnderScopedLockThreaded) {
  Mutex mutex(/* checked = */ true);
  scopedUnlockUnderScopedLockThreaded<ScopedLock, ScopedUnlock>(mutex);
}

TEST(StaticMutexTest, ScopedUnlockUnderScopedLockThreaded) {
  static StaticMutex Mutex;
  scopedUnlockUnderScopedLockThreaded<StaticScopedLock, StaticScopedUnlock>(
      Mutex);
}

template <typename M> void criticalSectionThreaded(M &mutex) {
  int count1 = 0;
  int count2 = 0;

  threadedExecute(10, [&](int) {
    for (int j = 0; j < 50; ++j) {
      mutex.withLock([&] {
        auto count = count2;
        count1++;
        count2 = count + 1;
      });
    }
  });

  ASSERT_EQ(count1, 500);
  ASSERT_EQ(count2, 500);
}

TEST(MutexTest, CriticalSectionThreaded) {
  Mutex mutex(/* checked = */ true);
  criticalSectionThreaded(mutex);
}

TEST(StaticMutexTest, CriticalSectionThreaded) {
  static StaticMutex Mutex;
  criticalSectionThreaded(Mutex);
}

template <typename SL, typename SU, typename M, typename C>
void conditionThreaded(M &mutex, C &condition) {
  bool doneCondition = false;
  int count = 200;

  threadedExecute(
      mutex, condition, doneCondition,
      [&](int index) {
        SL guard(mutex);
        while (true) {
          if (count > 50) {
            count -= 1;
            {
              // To give other consumers a chance.
              SU unguard(mutex);
            }
            if (trace)
              printf("Consumer[%d] count = %d.\n", index, count);
            continue; // keep trying to consume before waiting again.
          } else if (doneCondition && count == 50) {
            if (trace)
              printf("Consumer[%d] count == %d and done!\n", index, count);
            break;
          }
          mutex.wait(condition);
        }
      },
      [&](int index) {
        for (int j = 0; j < 10; j++) {
          mutex.lock();
          count += index;
          if (trace)
            printf("Producer[%d] count = %d.\n", index, count);
          condition.notifyOne();
          mutex.unlock();
        }
        if (trace)
          printf("Producer[%d] done!\n", index);
      });

  ASSERT_EQ(count, 50);
}

TEST(MutexTest, ConditionThreaded) {
  Mutex mutex(/* checked = */ true);
  ConditionVariable condition;
  conditionThreaded<ScopedLock, ScopedUnlock>(mutex, condition);
}

TEST(StaticMutexTest, ConditionThreaded) {
  static StaticMutex mutex;
  static StaticConditionVariable condition;
  conditionThreaded<StaticScopedLock, StaticScopedUnlock>(mutex, condition);
}

template <typename SU, typename M, typename C>
void conditionLockOrWaitLockThenNotifyThreaded(M &mutex, C &condition) {
  bool doneCondition = false;
  int count = 200;

  threadedExecute(
      mutex, condition, doneCondition,
      [&](int index) {
        mutex.withLockOrWait(condition, [&, index] {
          while (true) {
            if (count > 50) {
              count -= 1;
              {
                // To give other consumers a chance.
                SU unguard(mutex);
              }
              if (trace)
                printf("Consumer[%d] count = %d.\n", index, count);
              continue; // keep trying to consume before waiting again.
            } else if (doneCondition && count == 50) {
              if (trace)
                printf("Consumer[%d] count == %d and done!\n", index, count);
              return true;
            }
            return false;
          }
        });
      },
      [&](int index) {
        for (int j = 0; j < 10; j++) {
          mutex.withLockThenNotifyOne(condition, [&, index] {
            count += index;
            if (trace)
              printf("Producer[%d] count = %d.\n", index, count);
          });
        }
        if (trace)
          printf("Producer[%d] done!\n", index);
      });

  ASSERT_EQ(count, 50);
}

TEST(MutexTest, ConditionLockOrWaitLockThenNotifyThreaded) {
  Mutex mutex(/* checked = */ true);
  ConditionVariable condition;
  conditionLockOrWaitLockThenNotifyThreaded<ScopedUnlock>(mutex, condition);
}

TEST(StaticMutexTest, ConditionLockOrWaitLockThenNotifyThreaded) {
  static StaticMutex mutex;
  static StaticConditionVariable condition;
  conditionLockOrWaitLockThenNotifyThreaded<StaticScopedUnlock>(mutex,
                                                                condition);
}

template <typename SRL, bool Locking, typename RW>
void scopedReadThreaded(RW &lock) {
  const int threadCount = 10;

  std::set<int> writerHistory;
  std::vector<std::set<int>> readerHistory;
  readerHistory.assign(threadCount, std::set<int>());

  int protectedValue = 0;
  writerHistory.insert(protectedValue);

  threadedExecute(threadCount,
                  [&](int index) {
                    if (Locking) {
                      for (int i = 0; i < 50; ++i) {
                        {
                          SRL guard(lock);
                          readerHistory[index].insert(protectedValue);
                        }
                        std::this_thread::yield();
                      }
                    } else {
                      lock.readLock();
                      for (int i = 0; i < 50; ++i) {
                        readerHistory[index].insert(protectedValue);

                        {
                          SRL unguard(lock);
                          std::this_thread::yield();
                        }
                      }
                      lock.readUnlock();
                    }
                  },
                  [&] {
                    for (int i = 0; i < 25; ++i) {
                      lock.writeLock();
                      protectedValue += i;
                      writerHistory.insert(protectedValue);
                      lock.writeUnlock();
                    }
                  });

  for (auto &history : readerHistory) {
    for (auto value : history) {
      ASSERT_EQ(writerHistory.count(value), 1U);
    }
  }
}

TEST(ReadWriteLockTest, ScopedReadLockThreaded) {
  ReadWriteLock lock;
  scopedReadThreaded<ScopedReadLock, true>(lock);
}

TEST(StaticReadWriteLockTest, ScopedReadLockThreaded) {
  static StaticReadWriteLock lock;
  scopedReadThreaded<StaticScopedReadLock, true>(lock);
}

TEST(ReadWriteLockTest, ScopedReadUnlockThreaded) {
  ReadWriteLock lock;
  scopedReadThreaded<ScopedReadUnlock, false>(lock);
}

TEST(StaticReadWriteLockTest, ScopedReadUnlockThreaded) {
  static StaticReadWriteLock lock;
  scopedReadThreaded<StaticScopedReadUnlock, false>(lock);
}

template <typename SWL, bool Locking, typename RW>
void scopedWriteLockThreaded(RW &lock) {
  const int threadCount = 10;

  std::set<int> readerHistory;
  std::vector<std::set<int>> writerHistory;
  writerHistory.assign(threadCount, std::set<int>());

  int protectedValue = 0;
  readerHistory.insert(protectedValue);

  threadedExecute(threadCount,
                  [&](int index) {
                    if (Locking) {
                      for (int i = 0; i < 20; ++i) {
                        {
                          SWL guard(lock);
                          protectedValue += index * i;
                          writerHistory[index].insert(protectedValue);
                        }
                        std::this_thread::yield();
                      }
                    } else {
                      lock.writeLock();
                      for (int i = 0; i < 20; ++i) {
                        protectedValue += index * i;
                        writerHistory[index].insert(protectedValue);
                        {
                          SWL unguard(lock);
                          std::this_thread::yield();
                        }
                      }
                      lock.writeUnlock();
                    }
                  },
                  [&] {
                    for (int i = 0; i < 100; ++i) {
                      lock.readLock();
                      readerHistory.insert(protectedValue);
                      lock.readUnlock();
                    }
                  });

  std::set<int> mergedHistory;
  for (auto &history : writerHistory) {
    mergedHistory.insert(history.begin(), history.end());
  }

  for (auto value : readerHistory) {
    ASSERT_EQ(mergedHistory.count(value), 1U);
  }
}

TEST(ReadWriteLockTest, ScopedWriteLockThreaded) {
  ReadWriteLock lock;
  scopedWriteLockThreaded<ScopedWriteLock, true>(lock);
}

TEST(StaticReadWriteLockTest, ScopedWriteLockThreaded) {
  static StaticReadWriteLock lock;
  scopedWriteLockThreaded<StaticScopedWriteLock, true>(lock);
}

TEST(ReadWriteLockTest, ScopedWriteUnlockThreaded) {
  ReadWriteLock lock;
  scopedWriteLockThreaded<ScopedWriteUnlock, false>(lock);
}

TEST(StaticReadWriteLockTest, ScopedWriteUnlockThreaded) {
  static StaticReadWriteLock lock;
  scopedWriteLockThreaded<StaticScopedWriteUnlock, false>(lock);
}

template <typename RW> void readLockWhileReadLockedThreaded(RW &lock) {
  lock.readLock();

  std::vector<bool> results;
  results.assign(10, false);

  std::atomic<bool> done(false);
  threadedExecute(10,
                  [&](int index) {
                    while (!done) {
                      lock.withReadLock([&] {
                        results[index] = true;
                        std::this_thread::sleep_for(
                            std::chrono::milliseconds(5));
                      });
                      std::this_thread::sleep_for(std::chrono::milliseconds(1));
                    }
                  },
                  [&] {
                    std::this_thread::sleep_for(std::chrono::milliseconds(100));
                    done = true;
                  });

  lock.readUnlock();

  for (auto result : results) {
    ASSERT_TRUE(result);
  }
}

TEST(ReadWriteLockTest, ReadLockWhileReadLockedThreaded) {
  ReadWriteLock lock;
  readLockWhileReadLockedThreaded(lock);
}

TEST(StaticReadWriteLockTest, ReadLockWhileReadLockedThreaded) {
  static StaticReadWriteLock lock;
  readLockWhileReadLockedThreaded(lock);
}

template <typename RW> void readLockWhileWriteLockedThreaded(RW &lock) {
  lock.writeLock();

  std::vector<int> results;
  results.assign(10, 0);

  std::atomic<bool> done(false);
  threadedExecute(10,
                  [&](int index) {
                    while (!done) {
                      lock.withReadLock([&] {
                        results[index] += 1;
                        std::this_thread::sleep_for(
                            std::chrono::milliseconds(5));
                      });
                      std::this_thread::sleep_for(std::chrono::milliseconds(1));
                    }
                  },
                  [&] {
                    std::this_thread::sleep_for(std::chrono::milliseconds(100));
                    done = true;
                    lock.writeUnlock();
                  });

  for (auto result : results) {
    ASSERT_EQ(result, 1);
  }
}

TEST(ReadWriteLockTest, ReadLockWhileWriteLockedThreaded) {
  ReadWriteLock lock;
  readLockWhileWriteLockedThreaded(lock);
}

TEST(StaticReadWriteLockTest, ReadLockWhileWriteLockedThreaded) {
  static StaticReadWriteLock lock;
  readLockWhileWriteLockedThreaded(lock);
}

template <typename RW> void writeLockWhileReadLockedThreaded(RW &lock) {
  lock.readLock();

  const int threadCount = 10;

  std::vector<int> results;
  results.assign(threadCount, 0);

  std::atomic<bool> done(false);
  threadedExecute(threadCount,
                  [&](int index) {
                    while (!done) {
                      lock.withWriteLock([&] {
                        results[index] += 1;
                        std::this_thread::sleep_for(
                            std::chrono::milliseconds(5));
                      });
                      std::this_thread::sleep_for(std::chrono::milliseconds(1));
                    }
                  },
                  [&] {
                    std::this_thread::sleep_for(std::chrono::milliseconds(100));
                    done = true;
                    lock.readUnlock();
                  });

  for (auto result : results) {
    ASSERT_EQ(result, 1);
  }
}

TEST(ReadWriteLockTest, WriteLockWhileReadLockedThreaded) {
  ReadWriteLock lock;
  writeLockWhileReadLockedThreaded(lock);
}

TEST(StaticReadWriteLockTest, WriteLockWhileReadLockedThreaded) {
  static StaticReadWriteLock lock;
  writeLockWhileReadLockedThreaded(lock);
}

template <typename RW> void writeLockWhileWriteLockedThreaded(RW &lock) {
  lock.writeLock();

  const int threadCount = 10;

  std::vector<int> results;
  results.assign(threadCount, 0);

  std::atomic<bool> done(false);
  threadedExecute(threadCount,
                  [&](int index) {
                    while (!done) {
                      lock.withWriteLock([&] {
                        results[index] += 1;
                        std::this_thread::sleep_for(
                            std::chrono::milliseconds(5));
                      });
                      std::this_thread::sleep_for(std::chrono::milliseconds(1));
                    }
                  },
                  [&] {
                    std::this_thread::sleep_for(std::chrono::milliseconds(100));
                    done = true;
                    lock.writeUnlock();
                  });

  for (auto result : results) {
    ASSERT_EQ(result, 1);
  }
}

TEST(ReadWriteLockTest, WriteLockWhileWriteLockedThreaded) {
  ReadWriteLock lock;
  writeLockWhileWriteLockedThreaded(lock);
}

TEST(StaticReadWriteLockTest, WriteLockWhileWriteLockedThreaded) {
  static StaticReadWriteLock lock;
  writeLockWhileWriteLockedThreaded(lock);
}

template <typename RW> void tryReadLockWhileWriteLockedThreaded(RW &lock) {
  lock.writeLock();

  std::atomic<bool> done(false);
  threadedExecute(10,
                  [&](int) {
                    while (!done) {
                      ASSERT_FALSE(lock.try_readLock());
                      std::this_thread::sleep_for(std::chrono::milliseconds(1));
                    }
                  },
                  [&] {
                    std::this_thread::sleep_for(std::chrono::milliseconds(100));
                    done = true;
                  });

  lock.writeUnlock();
}

TEST(ReadWriteLockTest, TryReadLockWhileWriteLockedThreaded) {
  ReadWriteLock lock;
  tryReadLockWhileWriteLockedThreaded(lock);
}

TEST(StaticReadWriteLockTest, TryReadLockWhileWriteLockedThreaded) {
  static StaticReadWriteLock lock;
  tryReadLockWhileWriteLockedThreaded(lock);
}

template <typename RW> void tryReadLockWhileReadLockedThreaded(RW &lock) {
  lock.readLock();

  const int threadCount = 10;

  std::vector<bool> results;
  results.assign(threadCount, false);

  std::atomic<bool> done(false);
  threadedExecute(threadCount,
                  [&](int index) {
                    while (!done) {
                      ASSERT_TRUE(lock.try_readLock());
                      results[index] = true;
                      std::this_thread::sleep_for(std::chrono::milliseconds(5));
                      lock.readUnlock();
                      std::this_thread::sleep_for(std::chrono::milliseconds(1));
                    }
                  },
                  [&] {
                    std::this_thread::sleep_for(std::chrono::milliseconds(100));
                    done = true;
                  });

  lock.readUnlock();

  for (auto result : results) {
    ASSERT_TRUE(result);
  }
}

TEST(ReadWriteLockTest, TryReadLockWhileReadLockedThreaded) {
  ReadWriteLock lock;
  tryReadLockWhileReadLockedThreaded(lock);
}

TEST(StaticReadWriteLockTest, TryReadLockWhileReadLockedThreaded) {
  static StaticReadWriteLock lock;
  tryReadLockWhileReadLockedThreaded(lock);
}

template <typename RW> void tryWriteLockWhileWriteLockedThreaded(RW &lock) {
  lock.writeLock();

  std::atomic<bool> done(false);
  threadedExecute(10,
                  [&](int) {
                    while (!done) {
                      ASSERT_FALSE(lock.try_writeLock());
                      std::this_thread::sleep_for(std::chrono::milliseconds(1));
                    }
                  },
                  [&] {
                    std::this_thread::sleep_for(std::chrono::milliseconds(100));
                    done = true;
                  });

  lock.writeUnlock();
}

TEST(ReadWriteLockTest, TryWriteLockWhileWriteLockedThreaded) {
  ReadWriteLock lock;
  tryWriteLockWhileWriteLockedThreaded(lock);
}

TEST(StaticReadWriteLockTest, TryWriteLockWhileWriteLockedThreaded) {
  static StaticReadWriteLock lock;
  tryWriteLockWhileWriteLockedThreaded(lock);
}

template <typename RW> void tryWriteLockWhileReadLockedThreaded(RW &lock) {
  lock.readLock();

  std::atomic<bool> done(false);
  threadedExecute(10,
                  [&](int) {
                    while (!done) {
                      ASSERT_FALSE(lock.try_writeLock());
                      std::this_thread::sleep_for(std::chrono::milliseconds(1));
                    }
                  },
                  [&] {
                    std::this_thread::sleep_for(std::chrono::milliseconds(100));
                    done = true;
                  });

  lock.readUnlock();
}

TEST(ReadWriteLockTest, TryWriteLockWhileReadLockedThreaded) {
  ReadWriteLock lock;
  tryWriteLockWhileReadLockedThreaded(lock);
}

TEST(StaticReadWriteLockTest, TryWriteLockWhileReadLockedThreaded) {
  static StaticReadWriteLock lock;
  tryWriteLockWhileReadLockedThreaded(lock);
}

template <typename RW> void readWriteLockCacheExampleThreaded(RW &lock) {
  std::map<uint8_t, uint32_t> cache;
  std::vector<std::thread> workers;
  std::vector<std::set<uint8_t>> workerHistory;

  std::random_device rd;
  std::mt19937 gen(rd());
  std::uniform_int_distribution<> dis(0, UINT8_MAX);

  workerHistory.push_back(std::set<uint8_t>());
  for (int i = 0; i < 16; i++) {
    uint8_t key = dis(gen);
    cache[key] = 0;
    workerHistory[0].insert(key);

    if (trace)
      printf("WarmUp create for key = %d, value = %d.\n", key, 0);
  }

  // Block the threads we are about to create.
  const int threadCount = 20;
  std::atomic<bool> spinWait(true);
  std::atomic<int> readyCount(0);

  for (int i = 1; i <= threadCount; ++i) {
    workerHistory.push_back(std::set<uint8_t>());
    workers.push_back(std::thread([&, i] {
      readyCount++;

      // Block ourself until we are released to start working.
      while (spinWait) {
        std::this_thread::sleep_for(std::chrono::microseconds(10));
      }
      std::this_thread::sleep_for(std::chrono::milliseconds(1));

      for (int j = 0; j < 50; j++) {
        uint8_t key = dis(gen);
        bool found = false;

        auto cacheLookupSection = [&] {
          auto value = cache.find(key);
          if (value == cache.end()) {
            if (trace)
              printf("Worker[%d] miss for key = %d.\n", i, key);
            found = false; // cache miss, need to grab write lock
          }
          if (trace)
            printf("Worker[%d] HIT for key = %d, value = %d.\n", i, key,
                   value->second);
          found = true; // cache hit, no need to grab write lock
        };

        lock.withReadLock(cacheLookupSection);
        if (found) {
          continue;
        }

        lock.withWriteLock([&] {
          cacheLookupSection();
          if (!found) {
            if (trace)
              printf("Worker[%d] create for key = %d, value = %d.\n", i, key,
                     i);
            cache[key] = i;
            workerHistory[i].insert(key);
          }
        });
      }

      if (trace)
        printf("### Worker[%d] thread exiting.\n", i);
    }));
  }

  while (readyCount < threadCount) {
    std::this_thread::sleep_for(std::chrono::milliseconds(1));
  }

  // Allow our threads to fight for the lock.
  spinWait = false;

  // Wait until all of our workers threads have finished.
  for (auto &thread : workers) {
    thread.join();
  }

  for (auto &entry : cache) {
    if (trace)
      printf("### Cache dump key = %d, value = %d.\n", entry.first,
             entry.second);
    ASSERT_EQ(workerHistory[entry.second].count(entry.first), 1U);
  }
}

TEST(ReadWriteLockTest, ReadWriteLockCacheExampleThreaded) {
  ReadWriteLock lock;
  readWriteLockCacheExampleThreaded(lock);
}

TEST(StaticReadWriteLockTest, ReadWriteLockCacheExampleThreaded) {
  static StaticReadWriteLock lock;
  readWriteLockCacheExampleThreaded(lock);
}
