On 2011-11-26 05:00:56 +0000, Steven Watanabe said:
AMDG
On 11/24/2011 04:00 PM, Kelvin Chung wrote:
I need assistance on how to properly use boost::shared_mutex. Suppose I have this routine, which I want to make thread-safe:
class FunctionEvaluationCache { // Assume singleton std::unordered_map
> cache; // cache of results to Function::evaluate() } class Function { // Copyable bool operator==(const Function& fn2); // All Functions that operator==() share a cache entry
int evaluate(const Input& in) { // Expensive operation, hence the cache if (cache.count(*this) == 0 || cache[*this].count(in) == 0) { // Not in cache, do expensive operation
// Add result to cache cache[*this][in] = result; }
return cache[*this][in]; } }
Clearly, the big obstacle to a threadsafe version of Function::evaluate() is this cache. I believe that I will need a boost::shared_mutex for cache, as well as each cache entry.
It's probably easier to use only one mutex. Every call has to lock this mutex anyway, so it's doubtful whether you'd gain anything by having a separate mutex for each entry.
I was hoping that the outer mutex (for FunctionEvaluationCache) would only be needed to retrieve a reference to the inner table, so that a cache lookup for one Function wouldn't block a cache lookup for a different Function. However, it would appear that the inner mutexes (for the Function-specific std::map) would make the Function-specific cache noncopyable; which is fine, since the cache need only return references. However, I have to compile on a C++03 compiler (so I'm using boost::unordered_map), and it would seem that in this environment, inserting a value into a boost::unordered_map would involve copying the value (which I can't do since I have a mutex).
I'm not sure on how to use boost::shared_mutex, however: the straightforward way would be to expose the boost::shared_mutex instances publicly and modify evaluate() to get the proper locks, while the other line of thinking is that I should not expose the boost::shared_mutex instances at all and have a series of methods that evaluate() can use. But what I would like to do is not modify evaluate() in any way. Is it possible to do this?
It's possible, but probably not a good idea.
You're going to want the code to work something like this:
typedef std::map CacheItem;
struct Cache { typedef std::unordered_map
cache_type; typedef cache_type::iterator iterator; cache_type cache; boost::shared_mutex mutex; }; FunctionEvaluationCache global_cache;
int Function::evaluate(const Input& in) { { boost::shared_lockboost::shared_mutex lock( global_cache.mutex ); Cache::iterator pos = global_cache.cache.find( *this ); if ( pos != global_cache.end() ) { CacheItem::iterator inner = pos->second.find( in ); if ( inner != pos->second.end() ) { return inner->second; } } } { // do expensive operation
boost::unique_lockboost::shared_mutex lock( global_cache.mutex ); global_cache.cache[*this][in] = result; return result; } }
The main limitation of this, that you should be aware of is that <expensive operation> can run more than once if multiple threads reach it at the same time. I'm assuming that this is acceptable.
I'd think not, but given my circumstances (the expensive operation returns the same value every time the same input is passed in), I'd have to reconsider.
It's possible to make evaluate work as is, but it's a bit tricky, and wouldn't recommend it. The problem is that cache[*this][in] is used for both reading and writing, so you'd need to create a proxy reference.
How would that work?