我有一个要求,我需要存储一个项目列表的简单缓存。我正在使用List< T>为此目的,我们现在已经改变了设计以适应多个线程。
系统的体系结构由事件驱动,因此读写操作很可能会发生冲突。由于绝大多数访问都是只读的,我认为ReaderWriterLockSlim可能是一个很好的候选者。缓存只需要在该时刻的读取点准确。
我已经编写了下面的代码,似乎工作正常,但是有一些潜在的痛点吗?
更新:虽然下面的代码确实解决了一些同步问题,但并非100%完美。我已经决定实现一个更简单的类,它不暴露所有的IList< T>操作因此使重复使用更“安全”。
public class SynchronisedList<T> : IList<T>
{
private ReaderWriterLockSlim cacheLock = new ReaderWriterLockSlim();
private IList<T> innerCache = new List<T>();
private U ReadReturn<U>(Func<U> function)
{
cacheLock.EnterReadLock();
try { return function(); }
finally { cacheLock.ExitReadLock(); }
}
private void Read(Action action)
{
cacheLock.EnterReadLock();
try { action(); }
finally { cacheLock.ExitReadLock(); }
}
private U WriteReturn<U>(Func<U> function)
{
cacheLock.EnterWriteLock();
try { return function(); }
finally { cacheLock.ExitWriteLock(); }
}
private void Write(Action action)
{
cacheLock.EnterWriteLock();
try { action(); }
finally { cacheLock.ExitWriteLock(); }
}
public T this[int index]
{
get { return ReadReturn(() => innerCache[index]); }
set { Write(() => innerCache[index] = value); }
}
public int IndexOf(T item) { return ReadReturn(() => innerCache.IndexOf(item)); }
public void Insert(int index, T item) { Write(() => innerCache.Insert(index, item)); }
public void RemoveAt(int index) { Write(() => innerCache.RemoveAt(index)); }
public void Add(T item) { Write(() => innerCache.Add(item)); }
public void Clear() { Write(() => innerCache.Clear()); }
public bool Contains(T item) { return ReadReturn(() => innerCache.Contains(item)); }
public int Count { get { return ReadReturn(() => innerCache.Count); } }
public bool IsReadOnly { get { return ReadReturn(() => innerCache.IsReadOnly); } }
public void CopyTo(T[] array, int arrayIndex) { Read(() => innerCache.CopyTo(array, arrayIndex)); }
public bool Remove(T item) { return WriteReturn(() => innerCache.Remove(item)); }
public IEnumerator<T> GetEnumerator() { return ReadReturn(() => innerCache.GetEnumerator()); }
IEnumerator IEnumerable.GetEnumerator() { return ReadReturn(() => (innerCache as IEnumerable).GetEnumerator()); }
}
internal class Program
{
private static SynchronisedList<int> list = new SynchronisedList<int>();
private static void Main()
{
for (int i = 0; i < 500000; i++)
{
var index = i;
ThreadPool.QueueUserWorkItem((state) =>
{
var threadNum = (int)state;
if (threadNum % 10 == 0)
{
list.Add(threadNum);
}
else
{
Console.WriteLine(list.Count);
}
}, index);
}
Console.ReadKey();
}
}
答案 0 :(得分:7)
您是否了解内置的SynchronizedCollection<T>
类?
它使用基于标准Monitor
的锁定而不是ReaderWriterLockSlim
。您需要进行配置以确定这是否会在您的特定使用方案中产生显着的性能差异。
答案 1 :(得分:3)
这里有几个线程问题。
1。 我认为GetEnumerator函数在这里公开了一个线程问题。它们提供了一个不受锁定控制的innerCache的引用。
它可能崩溃的示例是,如果您有一个线程在列表上执行foreach而另一个线程正在删除或插入元素。
解决方案是复制列表并返回新克隆列表上的枚举器。如果列表很长,那么退回将是内存问题。
2。 除非在同步列表之外有另一个锁定方法,否则Contains()和IndexOf()函数或多或少是无用的。
示例:线程A获取对象的索引,线程B插入/删除/更新该对象,线程A索引现在是陈旧的。
我认为这对于完全同步的列表并不是一个好主意。编写自定义版本而不是功能有限。
如果您只需要一个队列或堆栈,请使用完全同步的两个或三个必要方法实现该队列或堆栈。如果您需要更多功能,请使用List并让不同的线程进行同步。
答案 2 :(得分:2)
此类解决所有问题并使您的列表100%线程安全。
通过使用与数据库中的事务一样工作的范围来避免竞争条件。
客户端代码
List<T> unsafeList = ...
var threadSafeList = new SyncronisedList(unsafeList);
using (threadSafeList.EnterReadScope()) {
// all your sequential read operations are thread-safe
}
using (threadSafeList.EnterWriteScope()) {
// all sequential read/write operations are thread-safe
}
班级代码
public class SyncronisedList<T> : IList<T> {
private readonly ReaderWriterLockSlim _threadLock;
private readonly IList<T> _internalList;
public SyncronisedList() : this(new List<T>()) {
}
public SyncronisedList(IList<T> internalList) {
_internalList = internalList;
_threadLock = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion);
}
private U Read<U>(Func<U> function) {
using (EnterReadScope())
return function();
}
private void Read(Action action) {
using (EnterReadScope())
action();
}
private U Write<U>(Func<U> function) {
using (EnterWriteScope())
return function();
}
private void Write(Action action) {
using (EnterWriteScope())
action();
}
public IDisposable EnterReadScope() {
return new Scope<T>(this, false);
}
public IDisposable EnterWriteScope() {
return new Scope<T>(this, true);
}
public T this[int index] {
get { return Read(() => _internalList[index]); }
set { Write(() => _internalList[index] = value); }
}
public int IndexOf(T item) { return Read(() => _internalList.IndexOf(item)); }
public void Insert(int index, T item) { Write(() => _internalList.Insert(index, item)); }
public void RemoveAt(int index) { Write(() => _internalList.RemoveAt(index)); }
public void Add(T item) { Write(() => _internalList.Add(item)); }
public void Clear() { Write(() => _internalList.Clear()); }
public bool Contains(T item) { return Read(() => _internalList.Contains(item)); }
public int Count { get { return Read(() => _internalList.Count); } }
public bool IsReadOnly { get { return Read(() => _internalList.IsReadOnly); } }
public void CopyTo(T[] array, int arrayIndex) { Read(() => _internalList.CopyTo(array, arrayIndex)); }
public bool Remove(T item) { return Write(() => _internalList.Remove(item)); }
public IEnumerator<T> GetEnumerator() { return Read(() => _internalList.GetEnumerator()); }
IEnumerator IEnumerable.GetEnumerator() { return Read(() => (_internalList as IEnumerable).GetEnumerator()); }
private class Scope<U> : IDisposable {
private readonly SyncronisedList<U> _owner;
private readonly bool _write;
internal Scope(SyncronisedList<U> owner, bool write) {
_owner = owner;
_write = write;
if (_write)
_owner._threadLock.EnterWriteLock();
else
_owner._threadLock.EnterReadLock();
}
public void Dispose() {
if (_write)
_owner._threadLock.ExitWriteLock();
else
_owner._threadLock.ExitReadLock();
}
}
}
答案 3 :(得分:1)
您的实施没问题,但您仍然需要关心同步问题:
给出一个列表{“foo”}
int index = list.IndexOf("foo");
Console.WriteLine(list[index]);
现在,如果另一个线程在两行之间执行list.Clear()怎么办? 您应该可以公开访问读者编写器锁以处理这些情况。 当然,对于Enumerator来说,......
答案 4 :(得分:0)
尝试列出所有内容 所有人都是线程安全的 非常努力。
我认为您应该查看您的应用程序需要的操作,然后设计一个以线程安全的方式公开它们的类。 (列表级别太低)
您的设计在现实生活中不太可能是线程安全的,因为调用列表的代码可能会以不安全的方式组合操作。
只需暴露一个枚举器即可打开一个 很多问题 - 它意味着什么 访问列表中的所有项目 另一个线程正在改变列表?
答案 5 :(得分:0)
列表中的某些操作对于其他操作而言不能有效地保证线程安全。例如,一个线程即将写入元素5,另一个线程删除元素3,使得过去为元素5的内容被移动到4,而过去为6的内容将被移动到5,第一个线程将最终被覆盖除了预期之外的元素。
当然有可能有一个有用的线程安全集合,它通过索引公开访问,如果对其行为施加某些限制,并且它实现与数组相同的IList<T>
子集。
我可以设想的最有用的ThreadSafeList样式除了支持数组实现的IList<T>
上的操作外,还支持以下操作:
该列表支持添加项目,但不支持插入或删除项目。由于Add
方法将返回新创建的项的索引,因此添加项的任何线程都将对其进行独占控制。此外,AccessItem
原语允许两个或多个线程以他们认为合适的任何方式在列表项上使用原子基元(如果T
是具有暴露字段的结构类型,也可以使用原子基元在那些领域)。
此类型不应构建在List<T>
上,而应构建在T[32][] arr
上。首先将arr[0]
设为T[16]
;如果添加了第17个项目,请将arr[1]
初始化为T[32]
;如果它变满,则将arr[2]
初始化为T[64]
等。任何给定的索引将始终由相同的数组元素表示,即使列表扩展,因此对现有列表元素的访问将不会受到名单扩大的影响。
仅附加列表可能是一个有用的线程安全类型,但我还没有发现任何版本的.net中的任何此类类型。