1. 程式人生 > >發現介面中的固有競爭條件

發現介面中的固有競爭條件

 

僅僅因為使用了互斥元或其他其他機制來保護共享資料,未必會免於競爭條件.你仍然需要確定保護了適當的資料

 

首先,考慮一個簡單棧的介面:

為了是這個棧是執行緒安全的,我們很容易想到:在每個成員函式的呼叫之前加鎖,在呼叫結束後釋放鎖.不過,這樣可能仍會存線上程安全的問題(這是一個典型的競爭條件,為了保護棧的內容而在內部使用互斥元,卻為能將其阻止,這就是介面的影響).


考慮下面兩個執行緒併發的呼叫(一行一行的看,比如執行緒A在執行s.empty()時執行緒B什麼都沒執行):

正如你所看到的,一個極端的情況,如果這個棧只有一個元素,此時兩個執行緒都會對這個棧pop()兩次.這種行為是未定義的.
雖然上述情況很少出現,但是一旦出現將會對整個系統造成破壞性的影響.
 

為了解決這一問題,需要對介面進行更加激進的改變(C++ Concurrency in action):
1.傳入引用.
The first option is to pass a reference to a variable in which you wish to receive the popped value as an argument in the call to pop():
std::vector<int> result;
some_stack.pop(result);
This works well for many cases, but it has the distinct disadvantage that it requires the calling code to construct an instance of the stack’s value type prior to the call, in order to pass this in as the target. For some types this is impractical, because constructing an instance is expensive in terms of time or resources. For other types this isn’t always possible, because the constructors require parameters that aren’t necessarily available at this point in the code. Finally, it requires that the stored type is assignable. This is an important restriction: many user-defined types do not support assignment, though they may support move construction or even copy construction (and thus allow return by value).  
2.要求不引發異常的拷貝函式或移動建構函式
There’s only an exception safety problem with a value-returning pop() if the return by value can throw an exception. Many types have copy constructors that don’t throw exceptions, and with the new rvalue-reference support in the C++ Standard , many more types will have a move constructor that doesn’t throw exceptions, even if their copy constructor does. One valid option is to restrict the use of  your thread-safe stack to those types that can safely be returned by value without throwing an exception. 
Although this is safe, it’s not ideal. Even though you can detect at compile time the existence of a copy or move constructor that doesn’t throw an exception using the std::is_nothrow_copy_constructible and std::is_nothrow_move_constructible type traits, it’s quite limiting. There are many more user-defined types with copy constructors that can throw and don’t have move constructors than there are types with copy and/or move constructors that can’t throw (although this might change as people get used to the rvalue-reference support in C++11). It would be unfortunate if such types couldn’t be stored in your thread-safe stack.  
3 返回指向棧頂的指標
The third option is to return a pointer to the popped item rather than return the item by value. The advantage here is that pointers can be freely copied without throwing an exception, so you’ve avoided Cargill’s exception problem. The disadvantage is that returning a pointer requires a means of managing the memory allocated to the object, and for simple types such as integers, the overhead of such memory management can exceed the cost of just returning the type by value. For any interface that uses this option, std::shared_ptr would be a good choice of pointer type; not only does it avoid memory leaks, because the object is destroyed once the last pointer is destroyed,but the library is in full control of the memory allocation scheme and doesn’t have to use new and delete.This can be important for optimization purposes: requiring that each object in the stack be allocated separately with new would impose quite an overhead compared to the original non-thread-safe version.  
4 同時提供選項1 2 3

一個安全的棧實現(C++ Concurrency in action)

對於上述的實現仔細觀察發現還是可以優化的:

template <typename T>
class threadsafe_stack {
private:
	std::stack<T> data; 
	std::mutex m; 
public:
	threadsafe_stack() {}
	threadsafe_stack(const threadsafe_stack& other)
	{
		// 鎖定other物件
		std::lock_guard<std::mutex> lock(other.m);
		data = other.data; 
	}
	void push(const T& value) 
	{
		std::lock_guard<std::mutex> lock(m);
		data.push(value);
	}
	void pop(T& value)
	{
		std::lock_guard<std::mutex> lock(m);
		if (data.empty()) 
			throw std::out_of_range("pop on a empty stack");
                // 這裡使用移動操作
		value = std::move(data.top());
		data.pop();
	}
	std::shared_ptr<T> pop() 
	{
		std::lock_guard<std::mutex> lock(m);
		if (data.empty())
			throw std::out_of_range("pop on a empty stack");
                // 這裡應該去除const 並且使用移動操作
		std::shared_ptr<T> ret(std::make_shared<T>(std::move(data.top())));
		data.pop();
		return ret; 
	}
	bool empty() const
	{
		std::lock_guard<std::mutex> lock(m);
		return data.empty();
	}
	threadsafe_stack& operator=(const threadsafe_stack&) = delete;
};

上面程式碼對pop()函式進行優化,對於返回指標的版本,不應該存在const,若存在const,則函式返回時將不能正確掉用移動建構函式而導致呼叫拷貝建構函式,造成效能上的浪費.

 
top()和pop()的討論表明,介面中有問題的競爭條件基本上因為鎖定的粒度過小而引起的.