// Licensed to the Apache Software Foundation (ASF) under one // or more contributor license agreements. See the NOTICE file // distributed with this work for additional information // regarding copyright ownership. The ASF licenses this file // to you 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 #include "gperftools_helper.h" #include "butil/atomicops.h" #include namespace { long start_time = butil::cpuwide_time_ms(); int c = 0; void* rdlocker(void* arg) { auto rw = (bthread_rwlock_t*)arg; bthread_rwlock_rdlock(rw); LOG(INFO) << butil::string_printf("[%" PRIu64 "] I'm rdlocker, %d, %" PRId64 "ms\n", pthread_numeric_id(), ++c, butil::cpuwide_time_ms() - start_time); bthread_usleep(10000); bthread_rwlock_unlock(rw); return NULL; } void* wrlocker(void* arg) { auto rw = (bthread_rwlock_t*)arg; bthread_rwlock_wrlock(rw); LOG(INFO) << butil::string_printf("[%" PRIu64 "] I'm wrlocker, %d, %" PRId64 "ms\n", pthread_numeric_id(), ++c, butil::cpuwide_time_ms() - start_time); bthread_usleep(10000); bthread_rwlock_unlock(rw); return NULL; } TEST(RWLockTest, sanity) { bthread_rwlock_t rw; ASSERT_EQ(0, bthread_rwlock_init(&rw, NULL)); ASSERT_EQ(0, bthread_rwlock_rdlock(&rw)); ASSERT_EQ(0, bthread_rwlock_unlock(&rw)); ASSERT_EQ(0, bthread_rwlock_wrlock(&rw)); ASSERT_EQ(0, bthread_rwlock_unlock(&rw)); bthread_t rdth; bthread_t rwth; ASSERT_EQ(0, bthread_start_urgent(&rdth, NULL, rdlocker, &rw)); ASSERT_EQ(0, bthread_start_urgent(&rwth, NULL, wrlocker, &rw)); ASSERT_EQ(0, bthread_join(rdth, NULL)); ASSERT_EQ(0, bthread_join(rwth, NULL)); ASSERT_EQ(0, bthread_rwlock_destroy(&rw)); } TEST(RWLockTest, used_in_pthread) { bthread_rwlock_t rw; ASSERT_EQ(0, bthread_rwlock_init(&rw, NULL)); pthread_t rdth[8]; pthread_t wrth[8]; for (size_t i = 0; i < ARRAY_SIZE(rdth); ++i) { ASSERT_EQ(0, pthread_create(&rdth[i], NULL, rdlocker, &rw)); } for (size_t i = 0; i < ARRAY_SIZE(wrth); ++i) { ASSERT_EQ(0, pthread_create(&wrth[i], NULL, wrlocker, &rw)); } for (size_t i = 0; i < ARRAY_SIZE(rdth); ++i) { pthread_join(rdth[i], NULL); } for (size_t i = 0; i < ARRAY_SIZE(rdth); ++i) { pthread_join(wrth[i], NULL); } ASSERT_EQ(0, bthread_rwlock_destroy(&rw)); } void* do_timedrdlock(void *arg) { struct timespec t = { -2, 0 }; EXPECT_EQ(ETIMEDOUT, bthread_rwlock_timedrdlock((bthread_rwlock_t*)arg, &t)); return NULL; } void* do_timedwrlock(void *arg) { struct timespec t = { -2, 0 }; EXPECT_EQ(ETIMEDOUT, bthread_rwlock_timedwrlock((bthread_rwlock_t*)arg, &t)); LOG(INFO) << 10; return NULL; } TEST(RWLockTest, timedlock) { bthread_rwlock_t rw; ASSERT_EQ(0, bthread_rwlock_init(&rw, NULL)); ASSERT_EQ(0, bthread_rwlock_rdlock(&rw)); bthread_t th; ASSERT_EQ(0, bthread_start_urgent(&th, NULL, do_timedwrlock, &rw)); ASSERT_EQ(0, bthread_join(th, NULL)); ASSERT_EQ(0, bthread_rwlock_unlock(&rw)); ASSERT_EQ(0, bthread_rwlock_wrlock(&rw)); ASSERT_EQ(0, bthread_start_urgent(&th, NULL, do_timedwrlock, &rw)); ASSERT_EQ(0, bthread_join(th, NULL)); ASSERT_EQ(0, bthread_start_urgent(&th, NULL, do_timedrdlock, &rw)); ASSERT_EQ(0, bthread_join(th, NULL)); ASSERT_EQ(0, bthread_rwlock_unlock(&rw)); ASSERT_EQ(0, bthread_rwlock_destroy(&rw)); } struct TrylockArgs { bthread_rwlock_t* rw; int rc; }; void* do_tryrdlock(void *arg) { auto trylock_args = (TrylockArgs*)arg; EXPECT_EQ(trylock_args->rc, bthread_rwlock_tryrdlock(trylock_args->rw)); if (0 != trylock_args->rc) { return NULL; } EXPECT_EQ(trylock_args->rc, bthread_rwlock_unlock(trylock_args->rw)); return NULL; } void* do_trywrlock(void *arg) { auto trylock_args = (TrylockArgs*)arg; EXPECT_EQ(trylock_args->rc, bthread_rwlock_trywrlock(trylock_args->rw)); if (0 != trylock_args->rc) { return NULL; } EXPECT_EQ(trylock_args->rc, bthread_rwlock_unlock(trylock_args->rw)); return NULL; } TEST(RWLockTest, trylock) { bthread_rwlock_t rw; ASSERT_EQ(0, bthread_rwlock_init(&rw, NULL)); ASSERT_EQ(0, bthread_rwlock_tryrdlock(&rw)); ASSERT_EQ(0, bthread_rwlock_unlock(&rw)); ASSERT_EQ(0, bthread_rwlock_rdlock(&rw)); bthread_t th; TrylockArgs args{&rw, 0}; ASSERT_EQ(0, bthread_start_urgent(&th, NULL, do_tryrdlock, &args)); ASSERT_EQ(0, bthread_join(th, NULL)); args.rc = EBUSY; ASSERT_EQ(0, bthread_start_urgent(&th, NULL, do_trywrlock, &args)); ASSERT_EQ(0, bthread_join(th, NULL)); ASSERT_EQ(0, bthread_rwlock_unlock(&rw)); ASSERT_EQ(0, bthread_rwlock_trywrlock(&rw)); ASSERT_EQ(0, bthread_rwlock_unlock(&rw)); ASSERT_EQ(0, bthread_rwlock_wrlock(&rw)); ASSERT_EQ(0, bthread_start_urgent(&th, NULL, do_tryrdlock, &args)); ASSERT_EQ(0, bthread_join(th, NULL)); ASSERT_EQ(0, bthread_start_urgent(&th, NULL, do_trywrlock, &args)); ASSERT_EQ(0, bthread_join(th, NULL)); ASSERT_EQ(0, bthread_rwlock_unlock(&rw)); ASSERT_EQ(0, bthread_rwlock_destroy(&rw)); } TEST(RWLockTest, cpp_wrapper) { bthread::RWLock rw; ASSERT_TRUE(rw.try_rdlock()); rw.unlock(); rw.rdlock(); rw.unlock(); ASSERT_TRUE(rw.try_wrlock()); rw.unlock(); rw.wrlock(); rw.unlock(); struct timespec t = { -2, 0 }; ASSERT_TRUE(rw.timed_rdlock(&t)); rw.unlock(); ASSERT_TRUE(rw.timed_wrlock(&t)); rw.unlock(); { bthread::RWLockRdGuard guard(rw); } { bthread::RWLockWrGuard guard(rw); } { std::lock_guard guard(rw, true); } { std::lock_guard guard(rw, false); } { std::lock_guard guard(*rw.native_handler(), true); } { std::lock_guard guard(*rw.native_handler(), false); } } bool g_started = false; bool g_stopped = false; void read_op(bthread_rwlock_t* rw, int64_t sleep_us) { ASSERT_EQ(0, bthread_rwlock_rdlock(rw)); if (0 != sleep_us) { bthread_usleep(sleep_us); } ASSERT_EQ(0, bthread_rwlock_unlock(rw)); } void write_op(bthread_rwlock_t* rw, int64_t sleep_us) { ASSERT_EQ(0, bthread_rwlock_wrlock(rw)); if (0 != sleep_us) { bthread_usleep(sleep_us); } ASSERT_EQ(0, bthread_rwlock_unlock(rw)); } typedef void (*OP)(bthread_rwlock_t* rw, int64_t sleep_us); struct MixThreadArg { bthread_rwlock_t* rw; OP op; }; void* loop_until_stopped(void* arg) { auto args = (MixThreadArg*)arg; while (!g_stopped) { args->op(args->rw, 20); } return NULL; } TEST(RWLockTest, mix_thread_types) { g_stopped = false; bthread_rwlock_t rw; ASSERT_EQ(0, bthread_rwlock_init(&rw, NULL)); const int N = 16; const int M = N * 2; pthread_t pthreads[N]; bthread_t bthreads[M]; // reserve enough workers for test. This is a must since we have // BTHREAD_ATTR_PTHREAD bthreads which may cause deadlocks (the // bhtread_usleep below can't be scheduled and g_stopped is never // true, thus loop_until_stopped spins forever) bthread_setconcurrency(M); std::vector args; args.reserve(N + M); for (int i = 0; i < N; ++i) { if (i % 2 == 0) { args.push_back({&rw, read_op}); } else { args.push_back({&rw, write_op}); } ASSERT_EQ(0, pthread_create(&pthreads[i], NULL, loop_until_stopped, &args.back())); } for (int i = 0; i < M; ++i) { if (i % 2 == 0) { args.push_back({&rw, read_op}); } else { args.push_back({&rw, write_op}); } const bthread_attr_t* attr = i % 2 ? NULL : &BTHREAD_ATTR_PTHREAD; ASSERT_EQ(0, bthread_start_urgent(&bthreads[i], attr, loop_until_stopped, &args.back())); } bthread_usleep(1000L * 1000); g_stopped = true; for (int i = 0; i < M; ++i) { bthread_join(bthreads[i], NULL); } for (int i = 0; i < N; ++i) { pthread_join(pthreads[i], NULL); } ASSERT_EQ(0, bthread_rwlock_destroy(&rw)); } // Tests below verify the writer-priority semantics and the cleanup path // guarded by the design notes in bthread/rwlock.cpp. struct WriterPriorityArgs { bthread_rwlock_t* rw; butil::atomic* order; int my_order; // sequence number captured inside the critical section int hold_us; }; void* wp_writer_fn(void* arg) { auto* a = (WriterPriorityArgs*)arg; EXPECT_EQ(0, bthread_rwlock_wrlock(a->rw)); a->my_order = a->order->fetch_add(1, butil::memory_order_relaxed); bthread_usleep(a->hold_us); EXPECT_EQ(0, bthread_rwlock_unlock(a->rw)); return NULL; } void* wp_reader_fn(void* arg) { auto* a = (WriterPriorityArgs*)arg; EXPECT_EQ(0, bthread_rwlock_rdlock(a->rw)); a->my_order = a->order->fetch_add(1, butil::memory_order_relaxed); bthread_usleep(a->hold_us); EXPECT_EQ(0, bthread_rwlock_unlock(a->rw)); return NULL; } // Verifies the writer-priority invariant guarded by the order // "unlock writer_queue_mutex BEFORE fetch_sub(writer_wait_count)" in // rwlock_unwrlock(): once a writer is queued, any new reader arriving // later MUST yield to that writer. TEST(RWLockTest, writer_priority) { bthread_setconcurrency(8); bthread_rwlock_t rw; ASSERT_EQ(0, bthread_rwlock_init(&rw, NULL)); // (1) Main thread holds the read lock first. ASSERT_EQ(0, bthread_rwlock_rdlock(&rw)); butil::atomic order(0); WriterPriorityArgs warg {&rw, &order, -1, 5000}; WriterPriorityArgs r2arg {&rw, &order, -1, 0}; // (2) Start a writer; it should park inside wrlock() because the read // lock is held. Sleep long enough for it to fetch_add into // writer_wait_count and reach the butex_wait on `lock_word'. bthread_t wth; ASSERT_EQ(0, bthread_start_urgent(&wth, NULL, wp_writer_fn, &warg)); bthread_usleep(50 * 1000); // (3) Now spawn a fresh reader. By writer-priority it MUST observe // writer_wait_count > 0 and park on it (NOT join the active read // lock). bthread_t r2th; ASSERT_EQ(0, bthread_start_urgent(&r2th, NULL, wp_reader_fn, &r2arg)); bthread_usleep(50 * 1000); // (4) Release the original read lock. The writer should win the race // and complete BEFORE the queued reader. ASSERT_EQ(0, bthread_rwlock_unlock(&rw)); bthread_join(wth, NULL); bthread_join(r2th, NULL); EXPECT_GE(warg.my_order, 0); EXPECT_GE(r2arg.my_order, 0); EXPECT_LT(warg.my_order, r2arg.my_order) << "Writer-priority violated: writer entered with order=" << warg.my_order << " but late reader entered with order=" << r2arg.my_order; ASSERT_EQ(0, bthread_rwlock_destroy(&rw)); } void* wp_timed_wrlock_short(void* arg) { auto* rw = (bthread_rwlock_t*)arg; timespec ts = butil::milliseconds_from_now(50); EXPECT_EQ(ETIMEDOUT, bthread_rwlock_timedwrlock(rw, &ts)); return NULL; } // Verifies the cleanup path of rwlock_wrlock_cleanup(): after multiple // writers fail with ETIMEDOUT, writer_wait_count must be back to 0 so // that subsequent readers are not blocked by leftover "ghost shares". TEST(RWLockTest, wrlock_failure_does_not_leak_writer_count) { bthread_setconcurrency(8); bthread_rwlock_t rw; ASSERT_EQ(0, bthread_rwlock_init(&rw, NULL)); // Hold the read lock so every wrlock attempt must block on `lock_word'. ASSERT_EQ(0, bthread_rwlock_rdlock(&rw)); const int N = 8; bthread_t wth[N]; for (int i = 0; i < N; ++i) { ASSERT_EQ(0, bthread_start_urgent(&wth[i], NULL, wp_timed_wrlock_short, &rw)); } // Wait for all timed wrlock attempts to time out and run cleanup. for (int i = 0; i < N; ++i) { bthread_join(wth[i], NULL); } // Release the read lock; from this point on no writer is in flight, // so a new reader MUST acquire the lock immediately. ASSERT_EQ(0, bthread_rwlock_unlock(&rw)); timespec ts = butil::milliseconds_from_now(500); butil::Timer t; t.start(); ASSERT_EQ(0, bthread_rwlock_timedrdlock(&rw, &ts)); t.stop(); EXPECT_LT(t.m_elapsed(), 100) << "Reader was blocked for " << t.m_elapsed() << "ms; " << "writer_wait_count was likely leaked by the cleanup path."; ASSERT_EQ(0, bthread_rwlock_unlock(&rw)); ASSERT_EQ(0, bthread_rwlock_destroy(&rw)); } struct DataConsistencyArgs { bthread_rwlock_t* rw; int64_t* shared; // protected by rw int64_t local_inc; // writer: number of increments this thread did int64_t observed_max; // reader: max value observed bool is_writer; }; void* dc_worker(void* arg) { auto* a = (DataConsistencyArgs*)arg; while (!g_stopped) { if (a->is_writer) { EXPECT_EQ(0, bthread_rwlock_wrlock(a->rw)); ++(*a->shared); ++a->local_inc; EXPECT_EQ(0, bthread_rwlock_unlock(a->rw)); } else { EXPECT_EQ(0, bthread_rwlock_rdlock(a->rw)); int64_t v = *a->shared; if (v > a->observed_max) { a->observed_max = v; } EXPECT_EQ(0, bthread_rwlock_unlock(a->rw)); } } return NULL; } // Verifies the release/acquire memory ordering pair on `lock_word'. // If the CAS in unwrlock()/unrdlock() weren't release-ordered, or the // CAS in rdlock()/wrlock() weren't acquire-ordered, writes done inside // the critical section could appear lost or inconsistent to other // threads, causing the final counter to disagree with total writer ops. TEST(RWLockTest, data_consistency) { bthread_rwlock_t rw; ASSERT_EQ(0, bthread_rwlock_init(&rw, NULL)); g_stopped = false; const int W = 4; const int R = 8; bthread_setconcurrency(W + R + 4); int64_t shared = 0; std::vector args(W + R); std::vector threads(W + R); for (int i = 0; i < W + R; ++i) { args[i].rw = &rw; args[i].shared = &shared; args[i].local_inc = 0; args[i].observed_max = -1; args[i].is_writer = (i < W); ASSERT_EQ(0, bthread_start_urgent(&threads[i], NULL, dc_worker, &args[i])); } bthread_usleep(500 * 1000); g_stopped = true; int64_t total_inc = 0; for (int i = 0; i < W + R; ++i) { bthread_join(threads[i], NULL); if (args[i].is_writer) { total_inc += args[i].local_inc; } } // No lost updates: every writer's increment is reflected in `shared'. EXPECT_EQ(total_inc, shared) << "Lost updates: total writer ops=" << total_inc << " but shared counter=" << shared; // No reader saw a value greater than the final counter. for (int i = W; i < W + R; ++i) { EXPECT_LE(args[i].observed_max, shared) << "Reader " << i << " observed_max=" << args[i].observed_max << " > final shared=" << shared; } ASSERT_EQ(0, bthread_rwlock_destroy(&rw)); } void* ws_reader_loop(void* arg) { auto* rw = (bthread_rwlock_t*)arg; while (!g_stopped) { EXPECT_EQ(0, bthread_rwlock_rdlock(rw)); // Hold the read lock briefly to keep the lock continuously busy. bthread_usleep(100); EXPECT_EQ(0, bthread_rwlock_unlock(rw)); } return NULL; } // Verifies that under a continuous read load, a writer can still acquire // the lock in bounded time. This is the end-to-end guarantee of the // writer-priority strategy: any reader arriving AFTER the writer entered // wrlock() must yield, ensuring the writer never starves. TEST(RWLockTest, no_writer_starvation) { bthread_rwlock_t rw; ASSERT_EQ(0, bthread_rwlock_init(&rw, NULL)); g_stopped = false; const int R = 16; bthread_setconcurrency(R + 4); bthread_t rth[R]; for (int i = 0; i < R; ++i) { ASSERT_EQ(0, bthread_start_urgent(&rth[i], NULL, ws_reader_loop, &rw)); } // Let the readers ramp up and saturate the lock. bthread_usleep(50 * 1000); // A single writer must succeed within a generous budget. butil::Timer t; t.start(); ASSERT_EQ(0, bthread_rwlock_wrlock(&rw)); t.stop(); EXPECT_LT(t.m_elapsed(), 1000) << "Writer starved for " << t.m_elapsed() << "ms under " << R << " concurrent readers; writer-priority is broken."; ASSERT_EQ(0, bthread_rwlock_unlock(&rw)); g_stopped = true; for (int i = 0; i < R; ++i) { bthread_join(rth[i], NULL); } ASSERT_EQ(0, bthread_rwlock_destroy(&rw)); } struct BAIDU_CACHELINE_ALIGNMENT PerfArgs { bthread_rwlock_t* rw; int64_t counter; int64_t elapse_ns; bool ready; PerfArgs() : rw(NULL), counter(0), elapse_ns(0), ready(false) {} }; template void* add_with_mutex(void* void_arg) { auto args = (PerfArgs*)void_arg; args->ready = true; butil::Timer t; while (!g_stopped) { if (g_started) { break; } bthread_usleep(10); } t.start(); while (!g_stopped) { if (Reader) { bthread_rwlock_rdlock(args->rw); } else { bthread_rwlock_wrlock(args->rw); } ++args->counter; bthread_rwlock_unlock(args->rw); } t.stop(); args->elapse_ns = t.n_elapsed(); return NULL; } int g_prof_name_counter = 0; template void PerfTest(uint32_t writer_ratio, ThreadId* /*dummy*/, int thread_num, const ThreadCreateFn& create_fn, const ThreadJoinFn& join_fn) { ASSERT_LE(writer_ratio, 100U); g_started = false; g_stopped = false; bthread_setconcurrency(thread_num + 4); std::vector threads(thread_num); std::vector args(thread_num); bthread_rwlock_t rw; bthread_rwlock_init(&rw, NULL); int writer_num = thread_num * writer_ratio / 100; int reader_num = thread_num - writer_num; for (int i = 0; i < thread_num; ++i) { args[i].rw = &rw; if (i < writer_num) { ASSERT_EQ(0, create_fn(&threads[i], NULL, add_with_mutex, &args[i])); } else { ASSERT_EQ(0, create_fn(&threads[i], NULL, add_with_mutex, &args[i])); } } while (true) { bool all_ready = true; for (int i = 0; i < thread_num; ++i) { if (!args[i].ready) { all_ready = false; break; } } if (all_ready) { break; } usleep(1000); } g_started = true; char prof_name[32]; snprintf(prof_name, sizeof(prof_name), "bthread_rwlock_perf_%d.prof", ++g_prof_name_counter); ProfilerStart(prof_name); usleep(1000 * 1000); ProfilerStop(); g_stopped = true; int64_t read_wait_time = 0; int64_t read_count = 0; int64_t write_wait_time = 0; int64_t write_count = 0; for (int i = 0; i < thread_num; ++i) { ASSERT_EQ(0, join_fn(threads[i], NULL)); if (i < writer_num) { write_wait_time += args[i].elapse_ns; write_count += args[i].counter; } else { read_wait_time += args[i].elapse_ns; read_count += args[i].counter; } } LOG(INFO) << "bthread rwlock in " << ((void*)create_fn == (void*)pthread_create ? "pthread" : "bthread") << " thread_num=" << thread_num << " writer_ratio=" << writer_ratio << " reader_num=" << reader_num << " read_count=" << read_count << " read_average_time=" << (read_count == 0 ? 0 : read_wait_time / (double)read_count) << "ns" << " writer_num=" << writer_num << " write_count=" << write_count << " write_average_time=" << (write_count == 0 ? 0 : write_wait_time / (double)write_count) << "ns"; } TEST(RWLockTest, performance) { bthread_setconcurrency(16); const int thread_num = 12; PerfTest(0, (pthread_t*)NULL, thread_num, pthread_create, pthread_join); PerfTest(0, (bthread_t*)NULL, thread_num, bthread_start_background, bthread_join); PerfTest(10, (pthread_t*)NULL, thread_num, pthread_create, pthread_join); PerfTest(20, (bthread_t*)NULL, thread_num, bthread_start_background, bthread_join); PerfTest(100, (pthread_t*)NULL, thread_num, pthread_create, pthread_join); PerfTest(100, (bthread_t*)NULL, thread_num, bthread_start_background, bthread_join); } void* read_thread(void* arg) { const size_t N = 10000; #ifdef CHECK_RWLOCK pthread_rwlock_t* lock = (pthread_rwlock_t*)arg; #else pthread_mutex_t* lock = (pthread_mutex_t*)arg; #endif const long t1 = butil::cpuwide_time_ns(); for (size_t i = 0; i < N; ++i) { #ifdef CHECK_RWLOCK pthread_rwlock_rdlock(lock); pthread_rwlock_unlock(lock); #else pthread_mutex_lock(lock); pthread_mutex_unlock(lock); #endif } const long t2 = butil::cpuwide_time_ns(); return new long((t2 - t1)/N); } void* write_thread(void*) { return NULL; } TEST(RWLockTest, pthread_rdlock_performance) { #ifdef CHECK_RWLOCK pthread_rwlock_t lock1; ASSERT_EQ(0, pthread_rwlock_init(&lock1, NULL)); #else pthread_mutex_t lock1; ASSERT_EQ(0, pthread_mutex_init(&lock1, NULL)); #endif pthread_t rth[16]; pthread_t wth; for (size_t i = 0; i < ARRAY_SIZE(rth); ++i) { ASSERT_EQ(0, pthread_create(&rth[i], NULL, read_thread, &lock1)); } ASSERT_EQ(0, pthread_create(&wth, NULL, write_thread, &lock1)); for (size_t i = 0; i < ARRAY_SIZE(rth); ++i) { long* res = NULL; pthread_join(rth[i], (void**)&res); printf("read thread %lu = %ldns\n", i, *res); delete res; } pthread_join(wth, NULL); #ifdef CHECK_RWLOCK pthread_rwlock_destroy(&lock1); #else pthread_mutex_destroy(&lock1); #endif } } // namespace