我正在开发一个WCF应用程序,该应用程序托管一个自定义对象供许多客户端访问。它基本上工作,但因为我需要处理数以千计的并发客户端,我需要该服务能够处理并发读取调用(更新将很少)。我通过在更新对象时锁定私有字段来添加一些线程安全性。
[ServiceBehavior(ConcurrencyMode = ConcurrencyMode.Multiple)]
public sealed class TestServerConfig : ConfigBase, ITestInterface
{
private object updateLock = new object();
private SortedList<string, DateTime> dates = new SortedList<string, DateTime>();
public DateTime GetDate(string key)
{
if (this.dates.ContainsKey(key))
{
return this.dates[key];
}
else
{
return DateTime.MinValue;
}
}
public void SetDate(string key, DateTime expirationDate)
{
lock (this.updateLock)
{
if (this.dates.ContainsKey(key))
{
this.dates[key] = expirationDate;
}
else
{
this.dates.Add(key, expirationDate);
}
}
}
}
我的问题是如何在没有锁定的情况下使GetDate线程安全,以便可以执行对GetDate的并发调用,但是当检查后但在读取值之前删除集合中的值时,异常不会随机发生。
抓住异常并处理它是可能的,但我更愿意将它发现。
有什么想法吗?
答案 0 :(得分:3)
有一个专门为此设计的锁,ReaderWriterLockSlim(ReadWriterLock如果使用的是低于.NET 4.0)
此锁允许并发读取,但在发生写入时会锁定读取(和其他写入)。
[ServiceBehavior(ConcurrencyMode = ConcurrencyMode.Multiple)]
public sealed class TestServerConfig : ConfigBase, ITestInterface
{
private ReaderWriterLockSlim updateLock = new ReaderWriterLockSlim();
private SortedList<string, DateTime> dates = new SortedList<string, DateTime>();
public DateTime GetDate(string key)
{
try
{
this.updateLock.EnterReadLock();
if (this.dates.ContainsKey(key))
{
return this.dates[key];
}
else
{
return DateTime.MinValue;
}
}
finally
{
this.updateLock.ExitReadLock();
}
}
public void SetDate(string key, DateTime expirationDate)
{
try
{
this.updateLock.EnterWriteLock();
if (this.dates.ContainsKey(key))
{
this.dates[key] = expirationDate;
}
else
{
this.dates.Add(key, expirationDate);
}
}
finally
{
this.updateLock.ExitWriteLock();
}
}
}
还有支持超时的“Try”版本的锁,您只需检查返回的bool以查看是否已锁定。
更新:另一个解决方案是使用ConcurrentDictionary,这根本不需要任何锁定。 ConcurrentDictionary在内部使用锁,但它们比你可以使用的更短,也有可能微软可以使用某种形式的不安全方法来进一步优化它,我不知道它们究竟在内部采用什么类型的锁。
你需要做一些重写,以使你的操作成为原子
[ServiceBehavior(ConcurrencyMode = ConcurrencyMode.Multiple)]
public sealed class TestServerConfig : ConfigBase, ITestInterface
{
private ConcurrentDictionary<string, DateTime> dates = new ConcurrentDictionary<string, DateTime>();
public DateTime GetDate(string key)
{
DateTime result;
if (this.dates.TryGetValue(key, out result))
{
return result;
}
else
{
return DateTime.MinValue;
}
}
public void SetDate(string key, DateTime expirationDate)
{
this.dates.AddOrUpdate(key, expirationDate, (usedKey, oldValue) => expirationDate);
}
}
UPDATE2:出于好奇,我在引擎盖下看看ConcurrentDictionary
做了什么,它做的只是锁定元素的一组桶,所以你只能得到如果两个对象也共享相同的哈希桶锁,则会发生锁争用。
通常有Environment.ProcessorCount * 4
个锁定桶,但您可以使用设置concurrencyLevel的构造函数手动设置它。
以下是决定使用哪个锁的方法
private void GetBucketAndLockNo(int hashcode, out int bucketNo, out int lockNo, int bucketCount, int lockCount)
{
bucketNo = (hashcode & 2147483647) % bucketCount;
lockNo = bucketNo % lockCount;
}
lockCount
等于构造函数中设置的concurrencyLevel
。
答案 1 :(得分:1)
我建议您使用ReaderWriterLockSlim
文档提供一个几乎完全符合您要求的示例。 (http://msdn.microsoft.com/en-us/library/system.threading.readerwriterlockslim.aspx)
但是,这样的事情:
public DateTime GetDate(string key)
{
cacheLock.EnterReadLock();
try
{
if (this.dates.ContainsKey(key))
{
return this.dates[key];
}
else
{
return DateTime.MinValue;
}
}
finally
{
cacheLock.ExitReadLock();
}
}
public void SetDate(string key, DateTime expirationDate)
{
cacheLock.EnterWriteLock();
try
{
if (this.dates.ContainsKey(key))
{
this.dates[key] = expirationDate;
}
else
{
this.dates.Add(key, expirationDate);
}
}
finally
{
cacheLock.ExitWriteLock();
}
}
ReaderWriterLockSlim
比使用lock
更高效,并区分读写,因此如果没有写入,则读取变为非阻塞。
答案 2 :(得分:0)
如果您确实无法承受锁定读取,您可以(在锁内)制作列表的副本,相应地更新它,然后替换旧列表。现在可能发生的最糟糕的事情是,有些读取会有点过时,但它们永远不会抛出。
lock (this.updateLock)
{
var temp = <copy list here>
if (temp.ContainsKey(key))
{
temp[key] = expirationDate;
}
else
{
temp.Add(key, expirationDate);
}
this.dates = temp;
}
效率不高,但如果你不经常这样做,那可能无关紧要。
答案 3 :(得分:0)
我遇到了同样的情况,并使用了ReaderWriterLockSlim并处理了这种情况。