Lựa chọn

 Danh mục

 Chi tiết tài nguyên
Using C# Generics to implement a Cache collection
   Đăng bởi: host | Ngày : 01:43 05/12/09 | Xem: 14407 |        
Using C# Generics to implement a Cache collection
This article really started out of a desire for me to learn the new generics functionality added to the newer versions of .NET and the version 2.0 of the Framework. Whilst at this moment I don't feel that a full fledged tutorial on generics is appropriate, this article shows my use of generics in solving a problem I recently encountered.
In my current job, I recently had the misfortune to discover a memory issue with our main development. We use hundreds of bitmaps at any one time. Long ago, someone thought that in order to improve performance we would cache the bitmaps in memory. Nice idea, except my current project seems to have pushed the technology further than anyone else had, and now we're out of memory because of the hundreds of Megabytes of images we have cached.
The Solution?
Thinking about the problem, it was obvious that we didn't need to cache every image, just the ones we were working with. Trying the system without any caching demonstrated why the caching support was originally added, performance started to get sluggish as the hard-disk is accessed so much. However, if an image hasn't been used for ages, then we can quite easily read that again from disk (as we had first time it was used) keeping the frequently and recently accessed images in the cache.
At work, a couple of hours of coding and I had implemented a very basic C++ cache using the STL and templates. This article however takes over from the point of me wondering how much alike the STL and the new C# generics are.
One of the things I really disliked about coding in C# was that whenever I used a collection (which I did lots) I had to worry about the types I was adding and retrieving, or else I had to create my own type-safe implementations of the collections. Whenever I use the STL in C++, this was never an issue. Constantly checking and casting objects was cluttering up my code.
On the most basic level, generics allow me to specify the types I want to be working with. Take the following two examples, the first in C# 1.x and the second in C# 2.x.
ArrayList items = new ArrayList();
items.Add("a string");
List<string> items = new List<string>();
items.Add("a string");
items.Add(123); // Compile time error!!
items.Add(this); // Compile time error!!
Using generics, I can specify the type that I want to have a list of, in this case strings. The compiler will then ensure that I do not start adding lots of incompatible types.
The implementation of the cache that I have created uses the signature of cache<Key,Value> which looks very similar to the signature of a hashtable or dictionary. What I wanted is to associate the cached data with some form of identifier (e.g., the filename of the bitmap I have cached). The actual class definition is shown below:
   publicclass Cache
      where Key: IComparable
      where Value: ICacheable
Notice that there are some changes to the typical class definition. The new where keyword allows the use of generics to be refined. In the code above, I have specified that I want the Key type to support at least IComparable (this is necessary for me to search for the keys in the collections).
I have also defined a need for the values to implement ICacheable. The ICacheable interface is a very simple interface I created:
 publicinterface ICacheable
  int BytesUsed { get; }
All it really needs to do is supply the property BytesUsed in order to let the cache know how big its represented data is. This can be seen being accessed in the Add method.
 publicvoid Add(Key k, Value v)
    // Check if we're using this yet
    if (ContainsKey(k))
      // Simple replacement by removing and adding again, this
      // will ensure we do the size calculation in only one place.
    // Need to get current total size and see if this will fit.
    int projectedUsage = v.BytesUsed
                       + this.CurrentCacheUsage;
    if (projectedUsage > maxBytes)
    // Store this value now..
    StoreItem(k, v);
As is typical with any container class, there are a number of other methods for getting or changing the members of the collection. Implemented methods are:
  • void Remove(Key k)
  • void Touch(Key k)
  • Value GetValue(Key k)
  • bool ContainsKey(Key k)
  • Value this[Key k]
  • void PurgeAll()
In order to access the stored members of the cache, a number of enumerators are implemented, these allow getting the keys, the values, and both the key and value as a pair. The implementation of enumerators in C# 2.0 has been made very easy.
  • IEnumerator<Value> GetEnumerator()
  • IEnumerable<Key> GetKeys()
  • IEnumerable<Value> GetValues()
  • IEnumerable<KeyValuePair<Key, Value>> GetItems()
 public IEnumerable<Value> GetValues()
  foreach (KeyValuePair<Key, Value> i in cacheStore)
   yield return i.Value;
Note the new yield keyword. This, in effect, allows the control of the program execution to be passed back to (or yielded to) the caller. Next time the enumerator is accessed, the execution continues at the same point in the collection. In the above example, I am accessing the internal store, cacheStore, and enumerating it with the foreach command, however, I am exposing the value of each to the caller of GetValues(). Whilst this might take a little understanding, trust me, it is simpler than what was needed before!
There are only a few properties defined for the cache class, their functionality is largely obvious.
  • int MaxBytes { get; set; }
  • int Count{ get; } // Get the current number of items in cache
  • int CurrentCacheUsage{ get; }
As a condition of using the beta software from Microsoft, it is not possible to quote performance of this class in concrete terms. The compiler and its generated code is likely to get much faster in the future… also, it isn't really possible to directly compare generic code to non-generic code without having two different implementations. I have tested this code with many Megabytes of bitmaps and its caching performance was as good, no noticeable impact on performance. It must be noted that once an item is knocked out of the cache, the run-time system and its garbage collection may not dispose off the object for quite a while. What the cache implementation does do is allow you to control the lifespan of your objects!
The implementation of the cache class used here is very basic; it could be further enhanced in many ways. For my needs, the solution implemented is not too bad and allows me some control over how much memory I want my applications using in order to keep large resources active in memory.
Bình luận

Thêm nhận xét