هزینهٔ پنهان RwLock توی سرویسهای پرخواندن
خلاصهٔ کاملتر
نویسنده تعریف میکنه که توی یه مصاحبه، مصاحبهکننده پرسیده بود ساختارهای lock-free (یعنی بدون قفل، با عملیات atomic) واقعاً چقدر فایده دارن. جوابش یه مثال سادهشده از سرویسی بود که روش کار کرده بودن. یه Store از جنس HashMap که دادهها رو نگه میداشت و یه Block که لیستی از Arc بود. هر درخواست خواندن کل Block رو پیمایش میکرد و همزمان چندین خواندن اجرا میشد، ولی فقط یه thread نویسنده مقدار Metrics رو عوض میکرد.
روش اول RwLock از کتابخونهٔ parking_lot بود. نویسنده میگه std::sync::RwLock اولویت خواننده و نویسنده رو به سیستمعامل میسپره و tokio::sync::RwLock هم async هست و ممکنه وسط کار yield کنه، پس parking_lot بهتر جواب میداد. روش دوم Atomic از crossbeam_epoch بود: مقدار Metrics پشت یه اشارهگر atomic میمونه، thread نویسنده با swap مقدار جدید رو جایگزین میکنه و مقدار قدیمی با defer_destroy بعداً، وقتی هیچ خوانندهای دستش نیست، پاک میشه.
نتیجهٔ بنچمارک (۵۰ خواننده، یه نویسنده با ۵۰۰ نوشتن در ثانیه، Block با ۱۶٬۳۸۴ عضو، روی MacBook Air): RwLock حدود ۱۵.۹۳ هزار خواندن در ثانیه با p50 برابر ۴۴۱ میکروثانیه، و نسخهٔ atomic حدود ۲۲۹ هزار خواندن با p50 حدود ۲۲ میکروثانیه. نکتهٔ جالب اینه که وقتی thread نویسنده رو خاموش کردن، سرعت RwLock تقریباً عوض نشد (۱۶.۰۱ هزار). یعنی مشکل رقابت نبود.
به گفتهٔ نویسنده، دلیلش اینه که parking_lot تعداد خوانندهها رو با یه AtomicUsize نگه میداره. هر بار گرفتن قفل یه compare-and-exchange هست و هر بار آزاد کردنش یه تفریق atomic. برای یه پیمایش این یعنی ۱۶٬۳۸۴ بار گرفتن و آزاد کردن، و با ۱۶ هزار پیمایش در ثانیه حدود ۵۲۴ میلیون عملیات atomic. این عملیاتها حتی بدون رقابت هم از خواندن و نوشتن معمولی گرونترن.
در عوض Crossbeam با pin() فقط یه بار برای کل پیمایش thread رو به یه epoch وصل میکنه (یعنی به کتابخونه میگه فعلاً دادههایی که دستمه رو پاک نکن). بعدش برای هر عضو فقط یه load ساده از اشارهگر atomic لازمه:
let guard = crossbeam_epoch::pin();
for data in block {
let metrics = data.metrics.load(Ordering::Acquire, &guard);
// ...
}نویسنده گزینههای دیگه رو هم بررسی کرده: ArcSwap برای هر عضو هنوز یه هزینهٔ جدا داره، و left-right با نگه داشتن دو کپی از داده خواندن رو سریعتر میکنه، ولی با حدود ۴ گیگابایت داده حافظه دو برابر میشد. برای خود Block که فقط یکی دو بار در ثانیه عضو جدید میگرفت، یه RwLock ساده دور کل Vec کافی بود و سرعت حدود ۲۰۴ هزار خواندن موند، چون قفل فقط یه بار برای کل پیمایش گرفته میشه.
نکات کلیدی:
- RwLock حدود ۱۵.۹۳ هزار و Crossbeam Atomic حدود ۲۲۹ هزار خواندن در ثانیه داشتن.
- p99 از ۱.۸۱ میلیثانیه به ۲۴۲ میکروثانیه رسید.
- خاموش کردن thread نویسنده سرعت RwLock رو تقریباً عوض نکرد؛ هزینه از ۱۶٬۳۸۴ قفل برای هر پیمایش بود.
- left-right به خاطر دو برابر شدن ۴ گیگابایت حافظه کنار گذاشته شد.
- یه RwLock دور کل Block با ۱ تا ۲ درج در ثانیه سرعت رو حدود ۲۰۴ هزار نگه داشت.
- نویسنده میگه این نتایج مخصوص همین workload هست و lock-free همیشه سریعتر نیست.




