| // Copyright 2017 The Cobalt Authors. All Rights Reserved. |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| #include "starboard/common/rwlock.h" |
| #include "starboard/configuration.h" |
| #include "starboard/nplb/thread_helpers.h" |
| #include "starboard/thread.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| |
| // Increasing threads by 2x increases testing time by 3x, due to write |
| // thread starvation. The test that relies on this number already runs |
| // at about ~300ms. |
| #define NUM_STRESS_THREADS 4 |
| |
| namespace starboard { |
| namespace nplb { |
| |
| // Basic usage of RWLock. |
| TEST(RWLock, Use) { |
| RWLock rw_lock; |
| // Read lock means we can acquire it multiple times. |
| rw_lock.AcquireReadLock(); |
| rw_lock.AcquireReadLock(); |
| rw_lock.ReleaseReadLock(); |
| rw_lock.ReleaseReadLock(); |
| |
| rw_lock.AcquireWriteLock(); |
| rw_lock.ReleaseWriteLock(); |
| } |
| |
| // Enters a RWLock as a reader and then increments a counter indicating that it |
| class ReadAndSignalTestThread : public AbstractTestThread { |
| public: |
| struct SharedData { |
| SharedData() |
| : read_count(0), sema_read_acquisition(0), sema_finish_run(0) {} |
| |
| int read_count; |
| RWLock rw_mutex; |
| Semaphore sema_read_acquisition; |
| Semaphore sema_finish_run; |
| }; |
| |
| explicit ReadAndSignalTestThread(SharedData* shared_data) |
| : shared_data_(shared_data) {} |
| |
| void Run() override { |
| shared_data_->rw_mutex.AcquireReadLock(); |
| // Indicate that this thread has entered the read state. |
| shared_data_->read_count++; |
| |
| // Signals the this thread has finished entering the read state. |
| shared_data_->sema_read_acquisition.Put(); |
| // Now waiting for the thread to allow us to finish. |
| shared_data_->sema_finish_run.Take(); |
| shared_data_->read_count--; |
| shared_data_->rw_mutex.ReleaseReadLock(); |
| } |
| |
| private: |
| SharedData* shared_data_; |
| }; |
| |
| // Tests the expectation that two reader threads can enter a rw_lock. |
| TEST(RWLock, ReadAcquisitionTwoThreads) { |
| ReadAndSignalTestThread::SharedData data; |
| |
| ReadAndSignalTestThread read_thread(&data); |
| read_thread.Start(); |
| |
| // Wait until the reader thread has finished entering into the read state. |
| data.sema_read_acquisition.Take(); |
| EXPECT_EQ(1, data.read_count); |
| |
| // The rw_lock is still being held now, therefore we test that we can |
| // acquire another access to the read. |
| data.rw_mutex.AcquireReadLock(); |
| data.rw_mutex.ReleaseReadLock(); |
| |
| // Release the other thread so that it can exit its run function. |
| data.sema_finish_run.Put(); |
| read_thread.Join(); |
| |
| EXPECT_EQ(0, data.read_count); |
| } |
| |
| // Tests the expectation that a read lock will be blocked for X milliseconds |
| // while the thread is holding the write lock. |
| class ThreadHoldsWriteLockForTime : public AbstractTestThread { |
| public: |
| struct SharedData { |
| explicit SharedData(SbTime time_hold) : time_to_hold(time_hold) {} |
| SbTime time_to_hold; |
| Semaphore signal_write_lock; |
| RWLock rw_lock; |
| }; |
| explicit ThreadHoldsWriteLockForTime(SharedData* shared_data) |
| : shared_data_(shared_data) {} |
| |
| void Run() override { |
| ScopedWriteLock write_lock(&shared_data_->rw_lock); |
| shared_data_->signal_write_lock.Put(); |
| SbThreadSleep(shared_data_->time_to_hold); |
| } |
| |
| private: |
| SharedData* shared_data_; |
| }; |
| TEST(RWLock, HoldsLockForTime) { |
| const SbTime kTimeToHold = kSbTimeMillisecond * 5; |
| const SbTime kAllowedError = kSbTimeMillisecond * 10; |
| |
| ThreadHoldsWriteLockForTime::SharedData shared_data(kTimeToHold); |
| ThreadHoldsWriteLockForTime thread(&shared_data); |
| |
| thread.Start(); |
| shared_data.signal_write_lock.Take(); // write lock was taken, start timer. |
| const SbTime start_time = SbTimeGetMonotonicNow(); |
| shared_data.rw_lock.AcquireReadLock(); // Blocked by thread for kTimeToHold. |
| shared_data.rw_lock.ReleaseReadLock(); |
| const SbTime delta_time = SbTimeGetMonotonicNow() - start_time; |
| thread.Join(); |
| |
| SbTime time_diff = delta_time - kTimeToHold; |
| if (time_diff < 0) { |
| time_diff = -time_diff; |
| } |
| EXPECT_LT(time_diff, kAllowedError); |
| } |
| |
| // This thread tests RWLock by generating numbers and writing to a |
| // shared set<int32_t>. Additionally readbacks are interleaved in writes. |
| class ThreadRWLockStressTest : public AbstractTestThread { |
| public: |
| struct SharedData { |
| RWLock rw_lock; |
| std::set<int32_t> data; |
| }; |
| |
| ThreadRWLockStressTest(int32_t begin_value, |
| int32_t end_value, |
| SharedData* shared_data) |
| : begin_value_(begin_value), |
| end_value_(end_value), |
| shared_data_(shared_data) {} |
| |
| void Run() override { |
| SbThread current_thread = SbThreadGetCurrent(); |
| SB_UNREFERENCED_PARAMETER(current_thread); |
| |
| for (int32_t i = begin_value_; i < end_value_; ++i) { |
| DoReadAll(); |
| DoWrite(i); |
| } |
| } |
| |
| private: |
| void DoReadAll() { |
| typedef std::set<int32_t>::const_iterator ConstIterator; |
| volatile int32_t dummy = 0; |
| |
| shared_data_->rw_lock.AcquireReadLock(); |
| for (ConstIterator it = shared_data_->data.begin(); |
| it != shared_data_->data.end(); ++it) { |
| dummy = *it; |
| } |
| shared_data_->rw_lock.ReleaseReadLock(); |
| } |
| |
| void DoWrite(int32_t value) { |
| shared_data_->rw_lock.AcquireWriteLock(); |
| const bool added = shared_data_->data.insert(value).second; |
| shared_data_->rw_lock.ReleaseWriteLock(); |
| ASSERT_TRUE(added); |
| } |
| |
| int32_t begin_value_; |
| int32_t end_value_; |
| SharedData* shared_data_; |
| }; |
| |
| TEST(RWLock, RWLockStressTest) { |
| const int32_t kNumValuesEachThread = 500; |
| // Expected number of values that will be generated by all threads. |
| const size_t kTotalValuesGenerated = |
| kNumValuesEachThread * NUM_STRESS_THREADS; |
| |
| ThreadRWLockStressTest::SharedData shared_data; |
| std::vector<AbstractTestThread*> threads; |
| |
| for (int i = 0; i < NUM_STRESS_THREADS; ++i) { |
| int32_t start_value = i * kNumValuesEachThread; |
| int32_t end_value = (i + 1) * kNumValuesEachThread; |
| ThreadRWLockStressTest* thread = |
| new ThreadRWLockStressTest(start_value, end_value, &shared_data); |
| threads.push_back(thread); |
| } |
| |
| for (size_t i = 0; i < threads.size(); ++i) { |
| threads[i]->Start(); |
| } |
| for (size_t i = 0; i < threads.size(); ++i) { |
| threads[i]->Join(); |
| delete threads[i]; |
| } |
| |
| std::vector<int32_t> values(shared_data.data.begin(), shared_data.data.end()); |
| |
| // We expect that the generators will generate [0...kTotalValuesGenerated). |
| ASSERT_EQ(kTotalValuesGenerated, values.size()); |
| for (size_t i = 0; i < values.size(); ++i) { |
| int32_t value = values[i]; |
| ASSERT_EQ(value, i); |
| } |
| } |
| |
| } // namespace nplb |
| } // namespace starboard |