假设我有一个多线程C ++程序,它以handleRequest(string key)
函数调用的形式处理请求。对handleRequest
的每次调用都发生在一个单独的线程中,key
有任意数量的可能值。
我想要以下行为:
handleRequest(key)
具有相同的值时,key
的同时通话会被序列化。 handleRequest
的正文可能如下所示:
void handleRequest(string key) {
KeyLock lock(key);
// Handle the request.
}
问题:如何实施KeyLock
以获得所需的行为?
一个天真的实现可能会像这样开始:
KeyLock::KeyLock(string key) {
global_lock->Lock();
internal_lock_ = global_key_map[key];
if (internal_lock_ == NULL) {
internal_lock_ = new Lock();
global_key_map[key] = internal_lock_;
}
global_lock->Unlock();
internal_lock_->Lock();
}
KeyLock::~KeyLock() {
internal_lock_->Unlock();
// Remove internal_lock_ from global_key_map iff no other threads are waiting for it.
}
...但是这需要在每个请求的开头和结尾都有一个全局锁,并为每个请求创建一个单独的Lock
对象。如果对handleRequest
的调用之间的争用很高,那可能不是问题,但如果争用率很低,则可能会产生大量开销。
答案 0 :(得分:12)
你可以做一些与你的问题类似的东西,但是没有一个global_key_map有几个(可能在一个数组或向量中) - 使用哪个是由字符串上的一些简单的哈希函数决定的。
通过这种方式而不是单一的全局锁定,你可以将它分散到几个独立的锁定中。
这是一种常用于内存分配器的模式(我不知道模式是否有名称 - 它应该)。当一个请求进来时,某些东西决定了分配将来自哪个池(通常是请求的大小,但其他参数也可以考虑),那么只需要锁定该池。如果分配请求来自将使用不同池的另一个线程,则没有锁争用。
答案 1 :(得分:2)
这将取决于平台,但我尝试的两种技术将是:
这两种技术都取决于操作系统的细节。试验看哪个有效。 。
答案 2 :(得分:2)
也许std::map<std::string, MutexType>
可能是您想要的,其中MutexType
是您想要的互斥锁的类型。您可能必须在另一个互斥锁中包含对映射的访问,以确保没有其他线程同时插入(并且记住在锁定互斥锁后再次执行检查以确保另一个线程未添加在等待互斥锁时按键!)。
同样的原则可以应用于任何其他同步方法,例如关键部分。
答案 3 :(得分:2)
提高粒度并锁定整个键范围
这是Mike B的答案的变体,其中没有多个流体锁定图,而是有一个固定的锁定数组,适用于键范围而不是单个键。
简化示例:在启动时创建256个锁的数组,然后使用键的第一个字节来确定要获取的锁的索引(即所有以'k'开头的键将由locks[107]
保护)。
为了维持最佳吞吐量,您应该分析密钥的分配和争用率。这种方法的好处是零动态分配和简单清理;你也避免两步锁定。如果密钥分配随着时间的推移而变得偏斜,那么缺点就是潜在的争用高峰。
答案 4 :(得分:0)
在考虑之后,另一种方法可能是这样的:
handleRequest
中,创建一个执行实际工作的Callback
。multimap<string, Callback*> global_key_map
。key
已被处理,则会将其Callback*
添加到global_key_map
并返回。实现了类似的东西:
LockAndCall(string key, Callback* callback) {
global_lock.Lock();
if (global_key_map.contains(key)) {
iterator iter = global_key_map.insert(key, callback);
while (true) {
global_lock.Unlock();
iter->second->Call();
global_lock.Lock();
global_key_map.erase(iter);
iter = global_key_map.find(key);
if (iter == global_key_map.end()) {
global_lock.Unlock();
return;
}
}
} else {
global_key_map.insert(key, callback);
global_lock.Unlock();
}
}
这样做的好处是可以释放原本等待密钥锁定的线程,但除此之外它与我在问题中发布的天真解决方案几乎相同。
它可以与Mike B和Constantin给出的答案相结合。
答案 5 :(得分:0)
/**
* StringLock class for string based locking mechanism
* e.g. usage
* StringLock strLock;
* strLock.Lock("row1");
* strLock.UnLock("row1");
*/
class StringLock {
public:
/**
* Constructor
* Initializes the mutexes
*/
StringLock() {
pthread_mutex_init(&mtxGlobal, NULL);
}
/**
* Lock Function
* The thread will return immediately if the string is not locked
* The thread will wait if the string is locked until it gets a turn
* @param string the string to lock
*/
void Lock(string lockString) {
pthread_mutex_lock(&mtxGlobal);
TListIds *listId = NULL;
TWaiter *wtr = new TWaiter;
wtr->evPtr = NULL;
wtr->threadId = pthread_self();
if (lockMap.find(lockString) == lockMap.end()) {
listId = new TListIds();
listId->insert(listId->end(), wtr);
lockMap[lockString] = listId;
pthread_mutex_unlock(&mtxGlobal);
} else {
wtr->evPtr = new Event(false);
listId = lockMap[lockString];
listId->insert(listId->end(), wtr);
pthread_mutex_unlock(&mtxGlobal);
wtr->evPtr->Wait();
}
}
/**
* UnLock Function
* @param string the string to unlock
*/
void UnLock(string lockString) {
pthread_mutex_lock(&mtxGlobal);
TListIds *listID = NULL;
if (lockMap.find(lockString) != lockMap.end()) {
lockMap[lockString]->pop_front();
listID = lockMap[lockString];
if (!(listID->empty())) {
TWaiter *wtr = listID->front();
Event *thdEvent = wtr->evPtr;
thdEvent->Signal();
} else {
lockMap.erase(lockString);
delete listID;
}
}
pthread_mutex_unlock(&mtxGlobal);
}
protected:
struct TWaiter {
Event *evPtr;
long threadId;
};
StringLock(StringLock &);
void operator=(StringLock&);
typedef list TListIds;
typedef map TMapLockHolders;
typedef map TMapLockWaiters;
private:
pthread_mutex_t mtxGlobal;
TMapLockWaiters lockMap;
};