| // Copyright 2016 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 <algorithm> |
| #include <numeric> |
| #include <random> |
| #include <vector> |
| |
| #include "starboard/common/atomic.h" |
| #include "starboard/common/mutex.h" |
| #include "starboard/configuration.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| |
| #define NUM_THREADS 4 |
| |
| namespace starboard { |
| namespace nplb { |
| namespace { |
| |
| // TestThread that is a bare bones class wrapper around Starboard |
| // thread. Subclasses must override Run(). |
| class TestThread { |
| public: |
| TestThread() : thread_(kSbThreadInvalid) {} |
| virtual ~TestThread() {} |
| |
| // Subclasses should override the Run method. |
| virtual void Run() = 0; |
| |
| // Calls SbThreadCreate() with default parameters. |
| void Start() { |
| SbThreadEntryPoint entry_point = ThreadEntryPoint; |
| |
| thread_ = SbThreadCreate(0, // default stack_size. |
| kSbThreadNoPriority, // default priority. |
| kSbThreadNoAffinity, // default affinity. |
| true, // joinable. |
| "TestThread", entry_point, this); |
| |
| if (kSbThreadInvalid == thread_) { |
| ADD_FAILURE_AT(__FILE__, __LINE__) << "Invalid thread."; |
| } |
| return; |
| } |
| |
| void Join() { |
| if (!SbThreadJoin(thread_, NULL)) { |
| ADD_FAILURE_AT(__FILE__, __LINE__) << "Could not join thread."; |
| } |
| } |
| |
| private: |
| static void* ThreadEntryPoint(void* ptr) { |
| TestThread* this_ptr = static_cast<TestThread*>(ptr); |
| this_ptr->Run(); |
| return NULL; |
| } |
| |
| SbThread thread_; |
| |
| TestThread(const TestThread&) = delete; |
| void operator=(const TestThread&) = delete; |
| }; |
| |
| /////////////////////////////////////////////////////////////////////////////// |
| // Boilerplate for test setup. |
| /////////////////////////////////////////////////////////////////////////////// |
| |
| // Defines a typelist for all atomic types. |
| typedef ::testing::Types<atomic_int32_t, |
| atomic_int64_t, |
| atomic_float, |
| atomic_double, |
| atomic_bool, |
| atomic_pointer<int*> > |
| AllAtomicTypes; |
| |
| // Defines a typelist for just atomic number types. |
| typedef ::testing:: |
| Types<atomic_int32_t, atomic_int64_t, atomic_float, atomic_double> |
| AtomicNumberTypes; |
| |
| // Defines a typelist for just atomic number types. |
| typedef ::testing::Types<atomic_int32_t, atomic_int64_t> AtomicIntegralTypes; |
| |
| // Defines test type that will be instantiated using each type in |
| // AllAtomicTypes type list. |
| template <typename T> |
| class AtomicBaseTest : public ::testing::Test {}; |
| TYPED_TEST_CASE(AtomicBaseTest, AllAtomicTypes); // Registration. |
| |
| // Defines test type that will be instantiated using each type in |
| // AtomicNumberTypes type list. |
| template <typename T> |
| class AtomicNumberTest : public ::testing::Test {}; |
| TYPED_TEST_CASE(AtomicNumberTest, AtomicNumberTypes); // Registration. |
| |
| template <typename T> |
| class AtomicIntegralTest : public ::testing::Test {}; |
| TYPED_TEST_CASE(AtomicIntegralTest, AtomicIntegralTypes); // Registration. |
| |
| /////////////////////////////////////////////////////////////////////////////// |
| // Singlethreaded tests. |
| /////////////////////////////////////////////////////////////////////////////// |
| |
| // Tests default constructor and single-argument constructor. |
| TYPED_TEST(AtomicBaseTest, Constructors) { |
| typedef TypeParam AtomicT; |
| typedef typename AtomicT::ValueType T; |
| |
| const T zero(0); |
| const T one = zero + 1; // Allows AtomicPointer<T*>. |
| |
| AtomicT atomic_default; |
| |
| // Tests that default value is zero. |
| ASSERT_EQ(atomic_default.load(), zero); |
| AtomicT atomic_val(one); |
| ASSERT_EQ(one, atomic_val.load()); |
| } |
| |
| // Tests load() and exchange(). |
| TYPED_TEST(AtomicBaseTest, Load_Exchange_SingleThread) { |
| typedef TypeParam AtomicT; |
| typedef typename AtomicT::ValueType T; |
| |
| const T zero(0); |
| const T one = zero + 1; // Allows AtomicPointer<T*>. |
| |
| AtomicT atomic; |
| ASSERT_EQ(atomic.load(), zero); // Default is 0. |
| ASSERT_EQ(zero, atomic.exchange(one)); // Old value was 0. |
| |
| // Tests that AtomicType has const get function. |
| const AtomicT& const_atomic = atomic; |
| ASSERT_EQ(one, const_atomic.load()); |
| } |
| |
| // Tests compare_exchange_strong(). |
| TYPED_TEST(AtomicNumberTest, CompareExchangeStrong_SingleThread) { |
| typedef TypeParam AtomicT; |
| typedef typename AtomicT::ValueType T; |
| |
| const T zero(0); |
| const T one = zero + 1; // Allows AtomicPointer<T*>. |
| |
| AtomicT atomic; |
| ASSERT_EQ(atomic.load(), zero); // Default is 0. |
| T expected_value = zero; |
| // Should succeed. |
| ASSERT_TRUE(atomic.compare_exchange_strong(&expected_value, |
| one)); // New value. |
| |
| ASSERT_EQ(zero, expected_value); |
| ASSERT_EQ(one, atomic.load()); // Expect that value was set. |
| |
| expected_value = zero; |
| // Asserts that when the expected and actual value is mismatched that the |
| // compare_exchange_strong() fails. |
| ASSERT_FALSE(atomic.compare_exchange_strong(&expected_value, // Mismatched. |
| zero)); // New value. |
| |
| // Failed and this means that expected_value should be set to what the |
| // internal value was. In this case, one. |
| ASSERT_EQ(expected_value, one); |
| ASSERT_EQ(one, atomic.load()); |
| |
| ASSERT_TRUE(atomic.compare_exchange_strong(&expected_value, // Matches. |
| zero)); |
| ASSERT_EQ(expected_value, one); |
| } |
| |
| // Tests atomic fetching and adding. |
| TYPED_TEST(AtomicNumberTest, FetchAdd_SingleThread) { |
| typedef TypeParam AtomicT; |
| typedef typename AtomicT::ValueType T; |
| |
| const T zero(0); |
| const T one = zero + 1; // Allows atomic_pointer<T*>. |
| const T two = zero + 2; |
| |
| AtomicT atomic; |
| ASSERT_EQ(atomic.load(), zero); // Default is 0. |
| ASSERT_EQ(zero, atomic.fetch_add(one)); // Prev value was 0. |
| ASSERT_EQ(one, atomic.load()); // Now value is this. |
| ASSERT_EQ(one, atomic.fetch_add(one)); // Prev value was 1. |
| ASSERT_EQ(two, atomic.exchange(one)); // Old value was 2. |
| } |
| |
| // Tests atomic fetching and subtracting. |
| TYPED_TEST(AtomicNumberTest, FetchSub_SingleThread) { |
| typedef TypeParam AtomicT; |
| typedef typename AtomicT::ValueType T; |
| |
| const T zero(0); |
| const T one = zero + 1; // Allows AtomicPointer<T*>. |
| const T two = zero + 2; |
| const T neg_two(zero - 2); |
| |
| AtomicT atomic; |
| ASSERT_EQ(atomic.load(), zero); // Default is 0. |
| atomic.exchange(two); |
| ASSERT_EQ(two, atomic.fetch_sub(one)); // Prev value was 2. |
| ASSERT_EQ(one, atomic.load()); // New value. |
| ASSERT_EQ(one, atomic.fetch_sub(one)); // Prev value was one. |
| ASSERT_EQ(zero, atomic.load()); // New 0. |
| ASSERT_EQ(zero, atomic.fetch_sub(two)); |
| ASSERT_EQ(neg_two, atomic.load()); // 0-2 = -2 |
| } |
| |
| TYPED_TEST(AtomicIntegralTest, IncrementAndDecrement_SingleThread) { |
| typedef TypeParam AtomicT; |
| typedef typename AtomicT::ValueType T; |
| |
| const T zero(0); |
| const T one = zero + 1; // Allows AtomicPointer<T*>. |
| |
| AtomicT atomic; |
| ASSERT_EQ(atomic.load(), zero); // Default is 0. |
| ASSERT_EQ(zero, atomic.increment()); // Tests for post-increment operation. |
| ASSERT_EQ(one, atomic.decrement()); // Tests for post-decrement operation. |
| } |
| |
| /////////////////////////////////////////////////////////////////////////////// |
| // Multithreaded tests. |
| /////////////////////////////////////////////////////////////////////////////// |
| |
| // A thread that will execute compare_exchange_strong() and write out a result |
| // to a shared output. |
| template <typename AtomicT> |
| class CompareExchangeThread : public TestThread { |
| public: |
| typedef typename AtomicT::ValueType T; |
| CompareExchangeThread(int start_num, |
| int end_num, |
| AtomicT* atomic_value, |
| std::vector<T>* output, |
| starboard::Mutex* output_mutex) |
| : start_num_(start_num), |
| end_num_(end_num), |
| atomic_value_(atomic_value), |
| output_(output), |
| output_mutex_(output_mutex) {} |
| |
| virtual void Run() { |
| std::vector<T> output_buffer; |
| output_buffer.reserve(end_num_ - start_num_); |
| for (int i = start_num_; i < end_num_; ++i) { |
| T new_value = T(i); |
| while (true) { |
| if (std::rand() % 3 == 0) { |
| // 1 in 3 chance of yielding. |
| // Attempt to cause more contention by giving other threads a chance |
| // to run. |
| SbThreadYield(); |
| } |
| |
| const T prev_value = atomic_value_->load(); |
| T expected_value = prev_value; |
| const bool success = |
| atomic_value_->compare_exchange_strong(&expected_value, new_value); |
| if (success) { |
| output_buffer.push_back(prev_value); |
| break; |
| } |
| } |
| } |
| |
| // Lock the output to append this output buffer. |
| starboard::ScopedLock lock(*output_mutex_); |
| output_->insert(output_->end(), output_buffer.begin(), output_buffer.end()); |
| } |
| |
| private: |
| const int start_num_; |
| const int end_num_; |
| AtomicT* const atomic_value_; |
| std::vector<T>* const output_; |
| starboard::Mutex* const output_mutex_; |
| }; |
| |
| // Tests Atomic<T>::compare_exchange_strong(). Each thread has a unique |
| // sequential range [0,1,2,3 ... ), [5,6,8, ...) that it will generate. |
| // The numbers are sequentially written to the shared Atomic type and then |
| // exposed to other threads: |
| // |
| // Generates [0,1,2,...,n/2) |
| // +------+ Thread A <--------+ (Write Exchanged Value) |
| // | | |
| // | compare_exchange() +---> exchanged? ---+ |
| // +----> +------------+ +----+ v |
| // | AtomicType | Output vector |
| // +----> +------------+ +----+ ^ |
| // | compare_exchange() +---> exchanged? ---+ |
| // | | |
| // +------+ Thread B <--------+ |
| // Generates [n/2,n/2+1,...,n) |
| // |
| // By repeatedly calling atomic<T>::compare_exchange_strong() by each of the |
| // threads, each will see the previous value of the shared variable when their |
| // exchange (atomic swap) operation is successful. If all of the swapped out |
| // values are recombined then it will form the original generated sequence from |
| // all threads. |
| // |
| // TEST PHASE |
| // sort( output vector ) AND TEST THAT |
| // output vector Contains [0,1,2,...,n) |
| // |
| // The test passes when the output array is tested that it contains every |
| // expected generated number from all threads. If compare_exchange_strong() is |
| // written incorrectly for an atomic type then the output array will have |
| // duplicates or otherwise not be equal to the expected natural number set. |
| TYPED_TEST(AtomicNumberTest, Test_CompareExchange_MultiThreaded) { |
| typedef TypeParam AtomicT; |
| typedef typename AtomicT::ValueType T; |
| |
| static const int kNumElements = 1000; |
| |
| AtomicT atomic_value(T(-1)); |
| std::vector<TestThread*> threads; |
| std::vector<T> output_values; |
| starboard::Mutex output_mutex; |
| |
| for (int i = 0; i < NUM_THREADS; ++i) { |
| const int start_num = (kNumElements * i) / NUM_THREADS; |
| const int end_num = (kNumElements * (i + 1)) / NUM_THREADS; |
| threads.push_back(new CompareExchangeThread<AtomicT>( |
| start_num, // defines the number range to generate. |
| end_num, &atomic_value, &output_values, &output_mutex)); |
| } |
| |
| // These threads will generate unique numbers in their range and then |
| // write them to the output array. |
| for (int i = 0; i < NUM_THREADS; ++i) { |
| threads[i]->Start(); |
| } |
| |
| for (int i = 0; i < NUM_THREADS; ++i) { |
| threads[i]->Join(); |
| } |
| // Cleanup threads. |
| for (int i = 0; i < NUM_THREADS; ++i) { |
| delete threads[i]; |
| } |
| threads.clear(); |
| |
| // Final value needs to be written out. The last thread to join doesn't |
| // know it's the last and therefore the final value in the shared |
| // has not be pushed to the output array. |
| output_values.push_back(atomic_value.load()); |
| std::sort(output_values.begin(), output_values.end()); |
| |
| // We expect the -1 value because it was the explicit initial value of the |
| // shared atomic. |
| ASSERT_EQ(T(-1), output_values[0]); |
| ASSERT_EQ(T(0), output_values[1]); |
| output_values.erase(output_values.begin()); // Chop off the -1 at the front. |
| |
| // Finally, assert that the output array is equal to the natural numbers |
| // after it has been sorted. |
| ASSERT_EQ(output_values.size(), kNumElements); |
| // All of the elements should be equal too. |
| for (int i = 0; i < output_values.size(); ++i) { |
| ASSERT_EQ(output_values[i], T(i)); |
| } |
| } |
| |
| // A thread that will invoke increment() and decrement() and equal number |
| // of times to atomic_value. The value after this is done should be equal to |
| // 0. |
| template <typename AtomicT> |
| class IncrementAndDecrementThread : public TestThread { |
| public: |
| typedef typename AtomicT::ValueType T; |
| IncrementAndDecrementThread(size_t half_number_of_operations, |
| AtomicT* atomic_value) |
| : atomic_value_(atomic_value) { |
| for (size_t i = 0; i < half_number_of_operations; ++i) { |
| operation_sequence_.push_back(true); |
| } |
| for (size_t i = 0; i < half_number_of_operations; ++i) { |
| operation_sequence_.push_back(false); |
| } |
| std::shuffle(operation_sequence_.begin(), operation_sequence_.end(), |
| std::default_random_engine()); |
| } |
| |
| virtual void Run() { |
| for (size_t i = 0; i < operation_sequence_.size(); ++i) { |
| if (std::rand() % 3 == 0) { |
| // 1 in 3 chance of yielding. |
| // Attempt to cause more contention by giving other threads a chance |
| // to run. |
| SbThreadYield(); |
| } |
| T prev_value = 0; |
| if (operation_sequence_[i]) { |
| prev_value = atomic_value_->increment(); |
| } else { |
| prev_value = atomic_value_->decrement(); |
| } |
| } |
| } |
| |
| private: |
| // Used purely for true/false values. Note that we don't |
| // use std::vector<bool> because some platforms won't support |
| // swapping elements of std::vector<bool>, which is required for |
| // std::random_shuffle(). |
| std::vector<uint8_t> operation_sequence_; |
| AtomicT* const atomic_value_; |
| }; |
| |
| TYPED_TEST(AtomicIntegralTest, Test_IncrementAndDecrement_MultiThreaded) { |
| typedef TypeParam AtomicT; |
| typedef typename AtomicT::ValueType T; |
| |
| static const int kNumOperations = 10000; |
| |
| AtomicT atomic_value(T(0)); |
| std::vector<TestThread*> threads; |
| |
| for (int i = 0; i < NUM_THREADS; ++i) { |
| threads.push_back(new IncrementAndDecrementThread<AtomicT>(kNumOperations, |
| &atomic_value)); |
| } |
| |
| for (int i = 0; i < NUM_THREADS; ++i) { |
| threads[i]->Start(); |
| } |
| |
| for (int i = 0; i < NUM_THREADS; ++i) { |
| threads[i]->Join(); |
| } |
| // Cleanup threads. |
| for (int i = 0; i < NUM_THREADS; ++i) { |
| delete threads[i]; |
| } |
| threads.clear(); |
| |
| // After an equal number of decrements and increments, the final value should |
| // be 0. |
| ASSERT_EQ(0, atomic_value.load()); |
| } |
| |
| template <typename AtomicT> |
| class FetchAddSubThread : public TestThread { |
| public: |
| typedef typename AtomicT::ValueType T; |
| FetchAddSubThread(const int32_t start_value, |
| const int32_t end_value, |
| AtomicT* atomic_value) |
| : start_value_(start_value), |
| end_value_(end_value), |
| atomic_value_(atomic_value) {} |
| |
| virtual void Run() { |
| for (int32_t i = start_value_; i < end_value_; ++i) { |
| if (std::rand() % 3 == 0) { |
| // 1 in 3 chance of yielding. |
| // Attempt to cause more contention by giving other threads a chance |
| // to run.s |
| SbThreadYield(); |
| } |
| |
| if (std::rand() % 2 == 0) { |
| atomic_value_->fetch_add(i); |
| } else { |
| atomic_value_->fetch_sub(-i); |
| } |
| } |
| } |
| |
| private: |
| int32_t start_value_; |
| int32_t end_value_; |
| AtomicT* const atomic_value_; |
| }; |
| |
| TYPED_TEST(AtomicIntegralTest, Test_FetchAdd_MultiThreaded) { |
| typedef TypeParam AtomicT; |
| typedef typename AtomicT::ValueType T; |
| |
| static const int kNumOperations = 10000; |
| |
| AtomicT atomic_value(T(0)); |
| std::vector<TestThread*> threads; |
| |
| // First value is inclusive, second is exclusive. |
| threads.push_back( |
| new FetchAddSubThread<AtomicT>(-kNumOperations, 0, &atomic_value)); |
| |
| threads.push_back( |
| new FetchAddSubThread<AtomicT>(1, kNumOperations + 1, &atomic_value)); |
| |
| for (int i = 0; i < threads.size(); ++i) { |
| threads[i]->Start(); |
| } |
| |
| for (int i = 0; i < threads.size(); ++i) { |
| threads[i]->Join(); |
| } |
| // Cleanup threads. |
| for (int i = 0; i < threads.size(); ++i) { |
| delete threads[i]; |
| } |
| threads.clear(); |
| |
| // After an equal number of decrements and increments, the final value should |
| // be 0. |
| ASSERT_EQ(0, atomic_value.load()); |
| } |
| |
| } // namespace |
| } // namespace nplb |
| } // namespace starboard |