Написание некоторого кода блокировки было бы довольно легко, за исключением ...
Как вы хотите получить его? Хотите ли вы иметь возможность перечислять (foreach) над списком потокобезопасным способом? Есть несколько способов выполнить эту часть, каждый из которых имеет компромисс.
Вы можете пойти с поведением по умолчанию
Это, вероятно, не сработает - вы получите исключение, если кто-то изменит список, пока вы его перечисляете.
Вы можете заблокировать коллекцию на протяжении всего процесса перечисления. Это означает, что любой поток, пытающийся добавить ваш кеш, будет заблокирован до выхода из цикла foreach.
Вы можете копировать коллекцию изнутри каждый раз, когда перечисляете ее и перечисляете копию.
Это означает, что если кто-то добавит вас в ваш список во время его перечисления, вы не увидите изменения.
Для списка из десяти я бы выбрал последний вариант. (внутренняя копия).
Ваш код будет выглядеть примерно так:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace Enumerator
{
class Program
{
static void Main(string[] args)
{
MyCache<string> cache = new MyCache<string>();
cache.Add("test");
foreach (string item in cache)
Console.WriteLine(item);
Console.ReadLine();
}
}
public class MyCache<T>: System.Collections.IEnumerable
{
private readonly LinkedList<T> InternalCache = new LinkedList<T>();
private readonly object _Lock = new Object();
public void Add(T item)
{
lock (_Lock)
{
if (InternalCache.Count == 10)
InternalCache.RemoveLast();
InternalCache.AddFirst(item);
}
}
#region IEnumerable Members
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
// copy the internal cache to an array. We'll really be enumerating that array
// our enumeration won't block subsequent calls to Add, but this "foreach" instance won't see Adds either
lock (_Lock)
{
T[] enumeration = new T[InternalCache.Count];
InternalCache.CopyTo(enumeration, 0);
return enumeration.GetEnumerator();
}
}
#endregion
}
}
РЕДАКТИРОВАТЬ 1:
После того, как я поделился некоторыми комментариями с Робом Левайном (ниже), я решил добавить пару других альтернатив.
Эта версия позволяет выполнять итерацию коллекции без блокировки. Однако метод Add () немного дороже, так как он должен копировать список (переместил расходы из Enumerate и в add).
public class Cache2<T>: IEnumerable<T>
{
// changes occur to this list, and it is copied to ModifyableList
private LinkedList<T> ModifyableList = new LinkedList<T>();
// This list is the one that is iterated by GetEnumerator
private volatile LinkedList<T> EnumeratedList = new LinkedList<T>();
private readonly object LockObj = new object();
public void Add(T item)
{
// on an add, we swap out the list that is being enumerated
lock (LockObj)
{
if (this.ModifyableList.Count == 10)
this.ModifyableList.RemoveLast();
this.ModifyableList.AddFirst(item);
this.EnumeratedList = this.ModifyableList;
// the copy needs to happen within the lock, so that threaded calls to Add() remain consistent
this.ModifyableList = new LinkedList<T>(this.ModifyableList);
}
}
#region IEnumerable<T> Members
IEnumerator<T> IEnumerable<T>.GetEnumerator()
{
IEnumerable<T> enumerable = this.EnumeratedList;
return enumerable.GetEnumerator();
}
#endregion
#region IEnumerable Members
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
System.Collections.IEnumerable enumerable = this.EnumeratedList;
return enumerable.GetEnumerator();
}
#endregion
}
Редактировать 2:
В последнем примере у нас была действительно недорогая итерация, а компромисс был более дорогим вызовом Add (). Затем я подумал об использовании ReaderWriterLockSlim (это объект .Net 3.5 - старый ReaderWriterLock предлагал довольно низкую производительность)
В этой модели метод Add () дешевле, чем в предыдущей модели (хотя для добавления все равно требуется эксклюзивная блокировка). С этой моделью нам не нужно создавать копии списка. Когда мы перечисляем список, мы вводим блокировку чтения, которая не блокирует других читателей, но блокирует / блокируется авторами (вызовы Add). От того, какая модель лучше - вероятно, зависит от того, как вы используете кеш. Я бы порекомендовал Тестирование и измерение.
public class Cache3<T> : IEnumerable<T>
{
private LinkedList<T> InternalCache = new LinkedList<T>();
private readonly System.Threading.ReaderWriterLockSlim LockObj = new System.Threading.ReaderWriterLockSlim();
public void Add(T item)
{
this.LockObj.EnterWriteLock();
try
{
if(this.InternalCache.Count == 10)
this.InternalCache.RemoveLast();
this.InternalCache.AddFirst(item);
}
finally
{
this.LockObj.ExitWriteLock();
}
}
#region IEnumerable<T> Members
IEnumerator<T> IEnumerable<T>.GetEnumerator()
{
this.LockObj.EnterReadLock();
try
{
foreach(T item in this.InternalCache)
yield return item;
}
finally
{
this.LockObj.ExitReadLock();
}
}
#endregion
#region IEnumerable Members
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
this.LockObj.EnterReadLock();
try
{
foreach (T item in this.InternalCache)
yield return item;
}
finally
{
this.LockObj.ExitReadLock();
}
}
#endregion
}