Voilà la dernière partie de cette aventure au combien trépidante. Il ne reste plus qu'à vider régulièrement le contenu du cache pour que ce dernier soit complet.
Pour faire cela, nous allons utiliser un Timer (namespace System.Timers). Pour ceux qui ne connaîtraient pas cette classe, peu nombreux j'imagine, elle permet de déclencher un événement à intervalle régulier.
Voici très brièvement sa définition au sein de notre classe :
1: private Timer _timer;
2: 3: public Cache()
4: { 5: _timer = new Timer();
6: _timer.Elapsed += timer_Elapsed; 7: _timer.Interval = 5000; 8: _timer.Start(); 9: 10: ... 11: } Le principe est super simple : lorsque l'événement se déclenche, on regarde tous les éléments présents et on efface ceux qui sont trop vieux. Cependant, avant de rentrer plus en détails dans le vif du sujet, je me dois d'exposer une dernière petite chose.
Dans ce cache, nous allons offrir la possibilité d'être notifié lorsqu'un élément est supprimé du cache et parce qu'on est sympa, on va aussi offrir la possibilité d'empêcher que l'élément soit supprimé. Là d'un coup tu te dis : c'est con ce que tu dis, le cache n'a plus aucun intérêt. Hé bien tu n'as qu'à moitié tord ! Je dis beaucoup de conneries, mais je dis aussi parfois des trucs qui peuvent servir.
Par exemple, tu es un site marchand et tu souhaites mettre en cache les paniers de tes utilisateurs. Ces paniers ne doivent subsister que 20 min (histoire de leur mettre la pression pour qu'ils achètent vite :p) mais attention tu es tombé sur un mec un peu lent qui a un panier qui vient d'atteindre les 20 min et il est en train de payer, donc ce serait dommage de lui effacer son panier. Il faut donc pouvoir offrir la possibilité de garder le panier au-delà de 20 min qui est pourtant sa durée de vie... Ok, c'est pas terrible comme exemple, mais ça illustre un peu le concept.
Pour faire tout ça nous allons créer nos propres EventArgs, delegate et event. En voici les définitions :
1: public class ItemRemovedEventArgs<K, V> : EventArgs
2: { 3: #region Private Members
4: 5: private V _value;
6: private K _key;
7: private bool _cancel;
8: 9: #endregion
10: 11: #region Constructors
12: 13: public ItemRemovedEventArgs(K key, V value)
14: { 15: _key = key; 16: _value = value;
17: _cancel = false;
18: } 19: 20: #endregion
21: 22: #region Properties
23: 24: public K Key
25: { 26: get { return _key; }
27: } 28: 29: public V Value
30: { 31: get { return _value; }
32: } 33: 34: public bool Cancel
35: { 36: get { return _cancel; }
37: set { _cancel = value; }
38: } 39: 40: #endregion
41: } et
1: public delegate void ItemRemovedHandler(object sender, ItemRemovedEventArgs<K, V> e);
2: 3: public event ItemRemovedHandler ItemRemoved;
Bon, fini les plaisanteries, maintenant on veut voir du code ! Voici le code qui s'occupe de la suppression tel qu'il est présent dans mon projet :
1: private void timer_Elapsed(object sender, ElapsedEventArgs e)
2: { 3: // If a scavenging is currently being processed,
4: // don't try to do one too to avoid a longer lock on the dictionnary
5: if (!_isScavenging)
6: { 7: _isScavenging = true;
8: 9: // Check for old entries
10: _lock.EnterUpgradeableReadLock(); 11: try
12: { 13: List<K> keysToDelete = new List<K>();
14: DateTime currentDatetime = DateTime.Now; 15: 16: // TODO: Change the foreach to a while for better performance
17: foreach (KeyValuePair<K, CacheItem> pair in _items)
18: { 19: if ((pair.Value.LastRefresh + pair.Value.Duration) < currentDatetime)
20: { 21: keysToDelete.Add(pair.Key); 22: } 23: } 24: if (keysToDelete.Count > 0)
25: { 26: _lock.EnterWriteLock(); 27: try
28: { 29: for (int j = 0; j < keysToDelete.Count; j++)
30: { 31: V value = _items[keysToDelete[j]].Item;
32: if (ItemRemoved != null)
33: { 34: ItemRemovedEventArgs<K, V> ea = new ItemRemovedEventArgs<K, V>(keysToDelete[j], value);
35: ItemRemoved(this, ea);
36: if (!ea.Cancel)
37: { 38: _items.Remove(keysToDelete[j]); 39: } 40: } 41: else
42: { 43: _items.Remove(keysToDelete[j]); 44: } 45: } 46: } 47: finally
48: { 49: _lock.ExitWriteLock(); 50: } 51: } 52: } 53: finally
54: { 55: _lock.ExitUpgradeableReadLock(); 56: } 57: 58: _isScavenging = false;
59: } 60: } Petite remarque qui a je trouve son importance, on fait toute cette manip en deux temps :
- Je recherche tous les éléments que je dois supprimer
- Je supprime les éléments
Pourquoi faire la chose en deux fois ? Tout d'abord parce qu'un Dictionary ne permet pas de supprimer un élément lorsque l'on est en train de parcourir sa collection (avec un foreach), mais surtout aussi, afin de ne locker la collection qu'en lecture au début, et n'acquérir le lock en écriture que si il y a besoin de faire des modifications. Grâce à cela on permet des locks plus brefs.
Et voilà, si je ne me suis pas vautré dans mes explications, tout cela devrait permettre d'avoir un cache qui fonctionne plutôt pas mal. Personnellement j'ai effectué quelques tests afin de vérifier le bon fonctionnement de ce code (oui je vais éviter de raconter des conneries si possible) et mon code a mis sur mon pc environ 13 secondes pour faire 12 500 000 accès (moitié en lecture, moitié en écriture).
Le protocole étant grossièrement 500 threads qui en parallèle font 25 000 tentatives chacun sur le cache (les lectures et écritures étant réparties aléatoirement sur l'ensemble du test). Si on considère qu'une lecture prends deux fois moins de temps qu'un ajout, et que mes process sont répartis également sur mes dual core, on obtient qu'une lecture prends 0,025 ms et une écriture 0,050 ms. Et tout ça sans aucune erreur ni inter-blocage :-)
Aucun commentaire:
Enregistrer un commentaire