7

I want to implement a HashTable (or mabybe a HashSet or Dictionary) which has unique members which expire after a while. For example:

// Items expire automatically after 10 seconds (Expiration period = 10 sec)
bool result = false;
// Starting from second 0
result = MyHashSet.Add("Bob");   // second 0 => true
result = MyHashSet.Add("Alice"); // second 5 => true
result = MyHashSet.Add("Bob");   // second 8 => false (item already exist)
result = MyHashSet.Add("Bob");   // second 12 => true (Bob has expired)

How to do that in a thread-safe manner with lowest costs?

BartoszKP
  • 34,786
  • 15
  • 102
  • 130
Xaqron
  • 29,931
  • 42
  • 140
  • 205

3 Answers3

8

You could create you own Hash Table where each item contains a creation time and a timespan. In the indexer where you try to return the value return null if the lifetime of the item has expired. And remove the item. A background thread that removes items from the table will not ensure you that you will never return an expired item without this. Then you can create a thread that does this just to remove expired items altogether to minimize memory consumption if a lot of items are never acessed.

Marino Šimić
  • 7,318
  • 1
  • 31
  • 61
  • That's good idea (first part of your answer). My problem is at the inserting side but the idea could be applied. I'm not agree with using any extra thread. – Xaqron May 14 '11 at 19:31
  • you are probably right, a hastable takes no time to find an item in the hastable, so removing items wont help in speed. but the extra thread will use the cpu for nothing. the only thing the thread will help is if a lot of added items are never acessed, it will remove them and minimize memory consumption – Marino Šimić May 14 '11 at 19:53
  • yes but take into account that a hastable will have much more reads than writes because that serves her purpose, so just see what read vs write ratio you will tipically have and you can trigger the thread at later insertion times, when you statistically should have more "garbage", except in some initialization time when you have many writes only. In that case you dont need a timer either. But probably you don't need this level of optimization on todays computers :D – Marino Šimić May 15 '11 at 01:33
7

Have you considered using System.Web.Caching instead of having to roll your own ?

http://www.hanselman.com/blog/UsingTheASPNETCacheOutsideOfASPNET.aspx

EDIT

Well the above should not add THAT much of an overhead to the system but have a look at this.

A few health warnings on the code below.

  • It's incomplete... see the throw new NotImplementedException()s at the bottom. I'll try and come back to it in a while as it's an interesting puzzle.
  • You may want to cange the way expiration is done & have overrides on the Add Methods to supply different values to the constructed value
  • I've only tested it the bare minimum in a console app. see test code
  • It also needs a bit of work around the TKey & TValue Collections as they'll blindly return the entirety of the inner dictionary's collections without any expiration checking... if you don't need particularly granular expiration. You could add a system.timer to the class which periodically walked the entire collection and removed expired entries.
  • If you look at the Definition for the BCL Dictionary you'll see it implements a hell of a lot of other interfaces to so depending on your requirements you may want to implement these as well. IDictionary<TKey, TValue>, ICollection<KeyValuePair<TKey, TValue>>, IEnumerable<KeyValuePair<TKey, TValue>>, IDictionary, ICollection, IEnumerable, ISerializable, IDeserializationCallback

Test Code

TimeSpan t = new TimeSpan(0,0,5); //5 Second Expiry
ExpiringDictionary<int, string> dictionary 
    = new ExpiringDictionary<int,string>(t);

dictionary.Add(1, "Alice");
dictionary.Add(2, "Bob");
dictionary.Add(3, "Charlie");
//dictionary.Add(1, "Alice"); //<<this will throw a exception as normal... 

System.Threading.Thread.Sleep(6000);
dictionary.Add(1, "Alice"); //<< this however should work fine as 6 seconds have passed

Implementation

public class ExpiringDictionary<TKey, TValue> : IDictionary<TKey, TValue>
{
    private class ExpiringValueHolder<T> {
        public T Value { get; set; }
        public DateTime Expiry { get; private set; }
        public ExpiringValueHolder(T value, TimeSpan expiresAfter)
        {
            Value = value;
            Expiry = DateTime.Now.Add(expiresAfter);
        }

        public override string ToString() { return Value.ToString(); }

        public override int GetHashCode() { return Value.GetHashCode(); }
    };
    private Dictionary<TKey, ExpiringValueHolder<TValue>> innerDictionary;
    private TimeSpan expiryTimeSpan;

    private void DestoryExpiredItems(TKey key)
    {
        if (innerDictionary.ContainsKey(key))
        {
            var value = innerDictionary[key];

            if (value.Expiry < System.DateTime.Now)
            { 
                //Expired, nuke it in the background and continue
                innerDictionary.Remove(key);
            }
        }
    }

    public ExpiringDictionary(TimeSpan expiresAfter)
    {
        expiryTimeSpan = expiresAfter;
        innerDictionary = new Dictionary<TKey, ExpiringValueHolder<TValue>>();
    }

    public void Add(TKey key, TValue value)
    {
        DestoryExpiredItems(key);

        innerDictionary.Add(key, new ExpiringValueHolder<TValue>(value, expiryTimeSpan));
    }

    public bool ContainsKey(TKey key)
    {
        DestoryExpiredItems(key);

        return innerDictionary.ContainsKey(key);
    }

    public bool Remove(TKey key)
    {
        DestoryExpiredItems(key);

        return innerDictionary.Remove(key);
    }

    public ICollection<TKey> Keys
    {
        get { return innerDictionary.Keys; }
    }

    public bool TryGetValue(TKey key, out TValue value)
    {
        bool returnval = false;
        DestoryExpiredItems(key);

        if (innerDictionary.ContainsKey(key))
        {
            value = innerDictionary[key].Value;
            returnval = true;
        } else { value = default(TValue);}

        return returnval;
    }

    public ICollection<TValue> Values
    {
        get { return innerDictionary.Values.Select(vals => vals.Value).ToList(); }
    }

    public TValue this[TKey key]
    {
        get
        {
            DestoryExpiredItems(key);
            return innerDictionary[key].Value;
        }
        set
        {
            DestoryExpiredItems(key);
            innerDictionary[key] = new ExpiringValueHolder<TValue>(value, expiryTimeSpan);
        }
    }

    public void Add(KeyValuePair<TKey, TValue> item)
    {
        DestoryExpiredItems(item.Key);

        innerDictionary.Add(item.Key, new ExpiringValueHolder<TValue>(item.Value, expiryTimeSpan));
    }

    public void Clear()
    {
        innerDictionary.Clear();
    }

    public int Count
    {
        get { return innerDictionary.Count; }
    }

    public bool IsReadOnly
    {
        get { return false; }
    }

    public bool Contains(KeyValuePair<TKey, TValue> item)
    {
        throw new NotImplementedException();
    }

    public void CopyTo(KeyValuePair<TKey, TValue>[] array, int arrayIndex)
    {
        throw new NotImplementedException();
    }

    public bool Remove(KeyValuePair<TKey, TValue> item)
    {
        throw new NotImplementedException();
    }

    public IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator()
    {
        throw new NotImplementedException();
    }

    System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
    {
        throw new NotImplementedException();
    }
}
Mark Rogers
  • 96,497
  • 18
  • 85
  • 138
Eoin Campbell
  • 43,500
  • 17
  • 101
  • 157
0

try this:

static void Main() {
    ExpirationList<string> list = new ExpirationList<string>(new List<string>());
    bool r1 = list.Add("Bob", 3000); // true
    Thread.Sleep(2000);
    bool r2 = list.Add("Bob", 3000); // false
    Thread.Sleep(2000);
    bool r3 = list.Add("Bob", 3000); // true
}

public class ExpirationList<T> {
    private List<T> _list;

    public ExpirationList(List<T> list) {
        if (list == null) throw new ArgumentException();
        _list = list;
    }

    public bool Add(T item, int lifetime) {

        lock (_list) {
            if (_list.Contains(item))
                return false;
            _list.Add(item);
        }

        new Action<int>(time => Thread.Sleep(time))
            .BeginInvoke(lifetime, new AsyncCallback(result => {
                T obj = (T)result.AsyncState;
                lock (_list) {
                    _list.Remove(obj);
                }
            }), item);

        return true;

    }

    // add other proxy code here

}

of course, List can be replaced with Hashtable and (it would be even more correct) async delegates can be replaced with Timers, but I hope that common approach is clear

Mikant
  • 299
  • 3
  • 18
  • This will end up a huge number of sleeping threads in my case (thousands of inserts per minute). – Xaqron May 14 '11 at 19:21
  • I think this is unsustainable for large collections – Marino Šimić May 14 '11 at 19:30
  • @Xargon: so set 1 Timer that will tick for example 1 time a second, and an additional List that will keep the lifetimes. decrement all the lifetimes per tick and if one of them becomes zero, then remove it and its linked item (on the same index on the main list) from the lists. to avoid creation of two lists you can wrap you class inside a new one with an extra property : Lifetime and use one list – Mikant May 14 '11 at 19:33
  • the second approach as the first but much more will make able to return expired items because the times takes time to do the operation. while another thread can be acessing the collection. – Marino Šimić May 14 '11 at 19:59