همزمانی در سی++: اشتراک گذاری داده ها(۲)

پیدا و رفع کردن Race Condition ها

همزمانی در سی++: اشتراک گذاری داده ها(۲)

در پست قبل درباره اینکه میوتکس(mutex) ها چی هستن و چطور می‌تونیم ازشون استفاده کنیم صحبت کردیم. رسیدیم به جایی که قرار بود Race Condition هایی که در ذات interface ما وجود داشتن رو پیدا و رفع کنیم :) بریم ببینیم چی میشه.

این پست رو دارم با ماوس و کیبورد و هدست(که یه موسیقی بی‌کلام ملایم پخش می‌کنه که باهاش صدای بیرون رو نشنوم. درسته که خودش موجب می‌شه تمرکزم کم باشه اما حداقل کافیه که با یه صدا مبارزه کنم نه با صدجور صدای مختلف) جدیدم که دوستای بسیار عزیزم برام خریدن می‌نویسم و بابتش ذوق هم دارم.

یافتن Race condition ذاتی در interface ها

صرف استفاده از میوتکس ها یا هر مکانیزم حفاظتی دیگه ای دلیل نمیشه که به کلی از race condition ها در امان باشیم. هنوز هم نیاز به انجام اعمال دیگری داریم. یک std::stack رو در نظر می‌گیریم که اعمال درونیش(push و pop و top و empty) با استفاده از میوتکس حفاظت شده.فرض کنیم ترد های ما همچین کدی رو می‌خوان اجرا بکنن:

if(!s.empty()) {
	const int value=s.top(); // checkpoint 1
	s.pop(); // checkpoint 2
	do_something(value);
}

فرض کنیم داخل استک ما فقط یک عضو وجود داشته باشه و هر دو ترد ما رسیده باشن به checkpoint 1. ترد اول میاد و عملیات pop() رو انجام می‌ده. حالا وقتی ترد دوم به pop() می‌رسه چه اتفاقی رخ می‌ده؟ مشخصه! !undefined behavior

به این می‌گن یک race condition ذاتی که در رابط یا همون interface وجود داره. حتی اگه lock-free هم برنامه نویسی کرده باشیم باز هم این مشکل پابرجاست و ربطی به میوتکس و ... نداره. مشکل از رابط است.راه حلش چیه؟ اینکه بیایم و تغییراتی که میخوایم انجام بدیم رو در یک قدم خلاصه کنیم. یعنی مثلا تابعی داشته باشیم که هم بالاترین عضو پشته رو حذف کنه و هم عضو حذف شده رو برگردونه. چرا خود نویسندگان عزیز کتابخانه استاندارد اینطوری ننوشتن که ما راحت باشیم؟ برای اینکه چیز های دیگه ای مثل Exception Safety هم وجود داره عزیزجان! و دقیقا بخاطر همین مسئله باید این دو کار به شکل جدا از هم انجام بشن! بهرحال برای اینکه ما بتونیم یک اینترفیس امن برای ترد هامون داشته باشیم نیاز داریم که این دو عملیات(top و pop ) رو یکی کنیم. راه حل چیست؟

راه اول: پاس دادن یک ارجاع(رفرنس) به تابع

راه اول اینه که بیایم و یک رفرنس از یک متغییر به تابع بدیم که برامون با محتویات عضوی که میخواد از استک حذف بشه، پُرش کنه.مثلا به این شکل:

std::vector<int> result;
some_stack.pop(result);

البته این راه هم مشکلات خودش رو داره. از جمله اینکه، نیازمند این هست که یک متغییر از نوع متغییر های موجود در استک ساخته بشه(که بتونیم پاسش بدیم). خب این برای Type هایی که ساختنشون منابع زیادی مصرف میشه مناسب نیست.

محدودیت بعدی اینه که اون Type مورد نظر باید Assignable باشه.

راه دوم: استفاده از copy/move constructor هایی که استثنا پرتاب نمی‌کنند

تنها دلیلی که نیاز داریم top و pop از هم جدا باشن همین مسئله Exception Safety هست. حالا اگر Type ای که می‌خوایم استفاده کنیم دارای Move Constructor و Copy Constructor ای باشه که استثنا(Exception) پرتاب نکنه، دیگه مشکلی نیست و میتونیم به راحتی این دو عملیات رو یکی کنیم.

بنابراین می‌تونیم این محدودیت رو بذاریم که فقط Type هایی که یکی از سازنده‌های(Constructors) بالا رو دارن قابل استفاده در استک باشن. اما خب این راه هم زیادی محدود کننده‌ست.

راه سوم: یک اشاره‌گر به آیتمی که حذف می‌شه برگردونیم

راه سوم اینه که بجای اینکه محتوای آیتم رو کپی کنیم، بیایم و صرفا یک اشاره‌گر از اون آیتم رو برگردونیم و به این ترتیب مشکل استثنا رو حل کنیم چرا که کپی کردن یه پوینتر ایجاد استثنا نمی‌کنه. ولی خب این راه هم مشکلات خودش رو داره:

  • باید حواسمون به مدیریت حافظه باشه
  • مدیریت حافظه برای Type های کوچیکی مثل Integer ها نمی‌صرفه و فقط بار اضافه‌ست

برای اولی می‌تونیم از اشاره‌گر های هوشمند استفاده بکنیم؛ مخصوصا std::shared_ptr اینجا خیلی میتونه مفید باشه. برای دومی هم میتونیم یه کار دیگه بکنیم؛ اونم اینکه از راه اول یا دوم هم در کنار راه سوم استفاده کنیم (:

در نهایت کد استک ما این شکلی می‌شه:

#include <exception>
#include <memory>
#include <mutex>
#include <stack>

struct empty_stack: std::exception
{
	const char* what() const throw();
};

template<typename T>
class threadsafe_stack
{

private:
	std::stack<T> data;
	mutable std::mutex m;
	
public:
	threadsafe_stack(){}
	threadsafe_stack(const threadsafe_stack& other)
	{
		std::lock_guard<std::mutex> lock(other.m);
		data=other.data;
	}
	
	threadsafe_stack& operator=(const threadsafe_stack&) = delete;
	void push(T new_value)
	{
		std::lock_guard<std::mutex> lock(m);
		data.push(std::move(new_value));
	}
	
	std::shared_ptr<T> pop()
	{
		std::lock_guard<std::mutex> lock(m);
		if(data.empty()) throw empty_stack();
		std::shared_ptr<T> const res(std::make_shared<T>(data.top()));
		data.pop();
		return res;
	}

	void pop(T& value)
	{
		std::lock_guard<std::mutex> lock(m);
		if(data.empty()) throw empty_stack();
		value=data.top();
		data.pop();
	}
	
	bool empty() const
	{
		std::lock_guard<std::mutex> lock(m);
		return data.empty();
	}
};

فکر می‌کنم که کد واضح باشه و نیاز به توضیح نیست. بهرحال، در این کد ما از راه اول و سوم باهمدیگه استفاده کردیم (:

پایان

پُست بعدی هم درباره یک مشکل معروف خواهد بود که یجورایی برعکس Race Condition هست و اصطلاحا بهش می‌گن: Dead Lock

امروز روز خوبی بود. با اینکه به همه برنامه هام نرسیدم ولی احساسات بدی نداشتم. کاش همه روزا اینطوری باشن!