C# d'Événements et de la Sécurité des Threads

mise à JOUR

à partir de C # 6, la réponse à cette question Est:

SomeEvent?.Invoke(this, e);

j'entends/je lis souvent le conseil suivant:

toujours faire une copie d'un événement avant de le vérifier pour null et le feu. Cela permettra d'éliminer un problème potentiel avec filetage où l'événement devient null à l'endroit juste entre où vous vérifiez pour nulle et où vous le feu de l'événement:

// Copy the event delegate before checking/calling
EventHandler copy = TheEvent;

if (copy != null)
    copy(this, EventArgs.Empty); // Call any handlers on the copied list

Updated : j'ai pensé à la lecture sur les optimisations que cela pourrait également exiger le membre de l'événement d'être volatile, mais Jon Skeet déclare dans sa réponse que le CLR ne pas optimiser la copie.

mais pendant ce temps, pour que cette question se produise même, un autre fil doit avoir fait quelque chose comme ceci:

// Better delist from event - don't want our handler called from now on:
otherObject.TheEvent -= OnTheEvent;
// Good, now we can be certain that OnTheEvent will not run...

la séquence actuelle peut-être ce mélange:

// Copy the event delegate before checking/calling
EventHandler copy = TheEvent;

// Better delist from event - don't want our handler called from now on:
otherObject.TheEvent -= OnTheEvent;    
// Good, now we can be certain that OnTheEvent will not run...

if (copy != null)
    copy(this, EventArgs.Empty); // Call any handlers on the copied list

Le point OnTheEvent s'exécute après que l'auteur a désabonné, et pourtant ils ont juste désabonné en particulier pour éviter que cela se produise. Ce dont nous avons vraiment besoin, c'est d'une implémentation d'événement personnalisée avec synchronisation appropriée dans les accesseurs add et remove . Et il y a aussi le problème des blocages possibles si une serrure est maintenue pendant qu'un événement est déclenché.

ainsi est ce Programmation Cargo Cult ? Il semble que de cette façon - beaucoup de gens doivent prendre cette mesure pour protéger leur code de fils multiples, alors qu'en réalité, il me semble que les événements nécessitent beaucoup plus de soin que cela avant qu'ils ne puissent être utilisés comme partie d'un design multi-threaded. Par conséquent, les gens qui ne prennent pas ce soin supplémentaire pourraient tout aussi bien ignorer ce conseil - ce n'est tout simplement pas un problème pour les programmes monofiltres, et en fait, compte tenu de l'absence de volatile en la plupart du code exemple en ligne, Le Conseil peut être sans effet du tout.

(et n'est-il pas beaucoup plus simple de simplement attribuer le delegate { } vide sur la déclaration du membre de sorte que vous n'avez jamais besoin de vérifier pour null en premier lieu?)

mise à Jour: Dans le cas où c'était pas clair, je l'ai fait saisir l'intention du conseil - d'éviter une référence nulle exception, dans toutes les circonstances. Mon point c'est que cette null l'exception de référence ne peut se produire que si un autre thread est retiré de l'événement, et la seule raison de le faire est de s'assurer qu'aucun autre appel ne sera reçu via cet événement, ce qui n'est manifestement pas le cas avec cette technique. Vous dissimuleriez une condition de race - il vaudrait mieux la révéler! Cette exception nulle aide à détecter un abus de votre composant. Si vous voulez que votre composant soit protégé contre les abus, vous pouvez suivre l'exemple de WPF - stocker l'ID du thread dans votre constructeur et ensuite lancer une exception si un autre thread tente d'interagir directement avec votre composant. Ou bien mettre en œuvre un composant vraiment sûr (ce n'est pas une tâche facile).

donc je soutiens que simplement faire cette copie/vérification idiome est la programmation Cargo cult, ajoutant le désordre et le bruit à votre code. Pour réellement protéger contre d'autres fils nécessite beaucoup plus de travail.

mise à Jour en réponse à Eric Lippert du blog:

donc il y a une chose importante que j'ai manquée chez les gestionnaires d'événements:" les gestionnaires d'événements sont tenus d'être robustes face au fait d'être appelés même après que l'événement a été désabonné", et évidemment donc nous n'avons qu'à nous soucier de la possibilité que le délégué d'événements soit null . cette exigence s'applique-t-elle aux manutentionnaires d'événements documentés quelque part?

Et aussi: "Il y a d'autres façons de résoudre ce problème; par exemple, initialiser le handler pour avoir une action vide qui n'est jamais retirée. Mais faire une vérification nulle est le modèle standard."

donc le dernier fragment de ma question Est, pourquoi est explicite-nul-vérifiez le "modèle standard"? l'alternative, assigner le délégué vide, nécessite seulement = delegate {} pour être ajouté à la déclaration de l'événement, et cela élimine ces petites piles de cérémonie puante de chaque endroit où l'événement est soulevé. Il serait facile de s'assurer que le délégué vide est pas cher à instancier. Ou suis-je manque encore quelque chose?

sûrement que (comme Jon Skeet l'a suggéré) c'est juste .NET 1.x Conseil qui n'est pas mort, comme il aurait dû le faire en 2005?

223
demandé sur Daniel Earwicker 2009-04-24 19:37:35

15 réponses

L'équipe n'est pas autorisé à effectuer l'optimisation dont vous parlez dans la première partie, en raison de l'état. Je sais que cela a été soulevé comme un spectre il y a un certain temps, mais ce n'est pas valide. (J'ai vérifié avec Joe Duffy ou Vance Morrison il y a un moment; je ne me souviens pas lequel.)

sans le modificateur volatile il est possible que la copie locale prise soit périmée, mais c'est tout. Ça ne causera pas un NullReferenceException .

et oui, il y a certainement une condition de course, mais il y en aura toujours une. Supposons que nous changions simplement le code en:

TheEvent(this, EventArgs.Empty);

supposons maintenant que la liste des invocations pour ce délégué comporte 1000 entrées. Il est parfaitement possible que l'action au début de la liste soit exécutée avant qu'un autre thread ne désabonne un handler proche de la fin de la liste. Cependant, ce gestionnaire sera toujours exécuté, car il y aura une nouvelle liste. (Les délégués sont immuables.) Aussi loin que je puisse voir ceci est inévitable.

en utilisant un délégué vide évite certainement le contrôle de nullité, mais ne fixe pas la condition de course. Il ne garantit pas que vous toujours "voir" la dernière valeur de la variable.

97
répondu Jon Skeet 2009-04-24 15:53:30

je vois beaucoup de gens qui vont vers la méthode d'extension de faire ceci ...

public static class Extensions   
{   
  public static void Raise<T>(this EventHandler<T> handler, 
    object sender, T args) where T : EventArgs   
  {   
    if (handler != null) handler(sender, args);   
  }   
}

qui vous donne une meilleure syntaxe pour relancer l'événement ...

MyEvent.Raise( this, new MyEventArgs() );

et supprime également la copie locale puisqu'elle est capturée au moment de l'appel de méthode.

50
répondu JP Alioto 2009-04-24 15:56:33

, "Pourquoi est explicite-null-cochez la case 'normale'?"

je soupçonne que la raison pour cela pourrait être que le nul-check est plus performant.

si vous inscrivez toujours un délégué vide à vos événements quand ils sont créés, il y aura quelques frais généraux:

  • coût de construction du délégué vide.
  • Coût de construction d'un délégué de la chaîne de la contenir.
  • Coût d'invoquer le délégué inutile chaque fois que l'événement est soulevé.

(notez que les contrôles UI ont souvent un grand nombre d'événements, dont la plupart ne sont jamais souscrits. Avoir à créer un abonné fictif à chaque événement et ensuite l'invoquer serait probablement un succès de performance significatif.)

j'ai fait quelques tests rapides de performance pour voir l'impact de l'approche abonnez-vous-vide-délégué, et voici mes résultats:

Executing 50000000 iterations . . .
OnNonThreadSafeEvent took:      432ms
OnClassicNullCheckedEvent took: 490ms
OnPreInitializedEvent took:     614ms <--
Subscribing an empty delegate to each event . . .
Executing 50000000 iterations . . .
OnNonThreadSafeEvent took:      674ms
OnClassicNullCheckedEvent took: 674ms
OnPreInitializedEvent took:     2041ms <--
Subscribing another empty delegate to each event . . .
Executing 50000000 iterations . . .
OnNonThreadSafeEvent took:      2011ms
OnClassicNullCheckedEvent took: 2061ms
OnPreInitializedEvent took:     2246ms <--
Done

notez que pour le cas de zéro ou un abonné (commun pour les contrôles UI, où les événements sont nombreux), l'événement pré-initialisé avec un délégué vide est sensiblement plus lent (plus de 50 millions d'itérations...)

pour plus d'informations et de code source, visitez ce billet de blog sur .net événement invocation thread safety que j'ai publié juste la veille de cette question a été posée (!)

(ma configuration de test peut imparfait alors n'hésitez pas à télécharger le code source et l'inspecter vous-même. Tout commentaire est apprécié.)

31
répondu Daniel Fortunov 2009-05-11 06:15:16

j'ai vraiment apprécié cette lecture ne pas! Même si j'en ai besoin pour travailler avec la fonctionnalité C# appelée events!

pourquoi ne pas corriger cela dans le compilateur? Je sais qu'il y a des personnes atteintes de SP qui lisent ces messages, alors s'il vous plaît ne flame pas cela!

1 - le problème de Null ) Pourquoi ne pas faire les événements .Vide au lieu de null en premier lieu? Combien de lignes de code serait sauvé null vérifier ou d'avoir à coller un = delegate {} sur le déclaration? Laisser le compilateur gérer le caisson Vide, c'est à dire ne rien faire! Si toutes les questions au créateur de l'événement, ils peuvent vérifier .Vide et faire ce qu'ils soins avec elle! Sinon, toutes les vérifications null / add delegate sont des piratages autour du problème!

honnêtement, je suis fatigué d'avoir à faire cela avec chaque événement-alias code de la bobine!

public event Action<thisClass, string> Some;
protected virtual void DoSomeEvent(string someValue)
{
  var e = Some; // avoid race condition here! 
  if(null != e) // avoid null condition here! 
     e(this, someValue);
}

2-The race condition issue ) j'ai lu le blog D'Eric, je suis d'accord que le H (handler) devrait manipuler quand il déréférencie lui-même, mais l'événement ne peut pas être rendu immuable/thread sûr? C'est-à-dire, mettre un drapeau de verrouillage sur sa création, de sorte que chaque fois qu'il est appelé, il verrouille tous les abonnés et non-abonnés à elle pendant son exécution?

Conclusion ,

les langues modernes ne sont-elles pas censées résoudre des problèmes comme ceux-ci pour nous?

10
répondu Chuck Savage 2011-04-27 19:47:35

selon Jeffrey Richter dans le livre CLR via c# , la bonne méthode est:

// Copy a reference to the delegate field now into a temporary field for thread safety
EventHandler<EventArgs> temp =
Interlocked.CompareExchange(ref NewMail, null, null);
// If any methods registered interest with our event, notify them
if (temp != null) temp(this, e);

parce qu'il force une copie de référence. Pour plus d'informations, voir son article dans le livre.

5
répondu alyx 2010-11-11 00:18:03

j'ai utilisé ce modèle de conception pour m'assurer que les gestionnaires d'événements ne sont pas exécutés après qu'ils soient désabonnés. Ça marche plutôt bien jusqu'à présent, même si je n'ai pas essayé le profilage des performances.

private readonly object eventMutex = new object();

private event EventHandler _onEvent = null;

public event EventHandler OnEvent
{
  add
  {
    lock(eventMutex)
    {
      _onEvent += value;
    }
  }

  remove
  {
    lock(eventMutex)
    {
      _onEvent -= value;
    }
  }

}

private void HandleEvent(EventArgs args)
{
  lock(eventMutex)
  {
    if (_onEvent != null)
      _onEvent(args);
  }
}

je travaille principalement avec Mono pour Android ces jours-ci, et Android ne semble pas aimer quand vous essayez de mettre à jour une vue après que son activité a été envoyé à l'arrière-plan.

4
répondu Ash 2013-07-16 21:49:43

cette pratique ne consiste pas à appliquer un certain ordre d'opérations. Il s'agit en fait d'éviter une exception de référence nulle.

le raisonnement derrière les gens qui se soucient de l'exception de la référence nulle et non de la condition raciale exigerait une recherche psychologique approfondie. Je pense que cela a quelque chose à voir avec le fait que la correction du problème de référence nulle est beaucoup plus facile. Une fois que c'est réglé, ils accrochent une grande bannière "Mission accomplie" sur leur code et décompressez leur combinaison de vol.

Note: la fixation de la condition de course implique probablement l'utilisation d'une piste de drapeau synchrone si le handler doit courir

1
répondu dss539 2009-04-24 17:19:43

donc je suis un peu en retard pour la fête ici. :)

pour ce qui est de l'utilisation du motif nul plutôt que nul pour représenter des événements sans abonnés, considérez ce scénario. Vous devez invoquer un événement, mais construire l'objet (EventArgs) n'est pas trivial, et dans le cas commun votre événement n'a pas d'abonnés. Il serait bénéfique pour vous si vous pouviez optimiser votre code à vérifier pour voir si vous aviez des abonnés du tout avant de vous engager traitement effort pour construire les arguments et invoquer l'événement.

dans cet esprit, une solution est de dire" Eh bien, zéro abonnés est représenté par null."Ensuite, il suffit d'effectuer le contrôle nul avant d'effectuer votre opération coûteuse. Je suppose qu'une autre façon de faire cela aurait été d'avoir une propriété de compte sur le type de délégué, donc vous n'effectueriez l'opération coûteuse si mon délégué.Compte > 0. L'utilisation D'une propriété Count est un modèle assez agréable qui résout le problème d'origine de permettre l'optimisation, et il a également la propriété agréable de pouvoir être invoqué sans causer une exception NullReferenceException.

gardez à l'esprit, cependant, que puisque les délégués sont des types de référence, ils sont autorisés à être nuls. Peut-être qu'il n'y avait tout simplement pas de bonne façon de cacher ce fait sous les couvertures et de soutenir seulement le modèle d'objet null pour les événements, de sorte que l'alternative a pu forcer les développeurs à vérifier à la fois pour null et pour les abonnés zéro. Ce serait encore pire que la situation actuelle.

Note: c'est de la pure spéculation. Je ne suis pas impliqué avec les langues .NET ou CLR.

1
répondu Levi 2009-05-01 05:33:16

avec C# 6 et au-dessus, le code pourrait être simplifié en utilisant le nouveau .? operator comme dans TheEvent?.Invoke(this, EventArgs.Empty);

https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/operators/null-conditional-operators

1
répondu Phil1970 2017-08-29 23:47:18

pour les mono-thread applications, vous êtes correc ce n'est pas un problème.

Cependant, si vous faites un composant qui expose des événements, il n'y a aucune garantie qu'un consommateur de votre composant ne va pas aller le multithreading, auquel cas vous devez vous préparer pour le pire.

L'Utilisation du délégué vide permet de résoudre le problème, mais provoque également une performance sur chaque appel à l'événement, et pourrait éventuellement avoir des implications GC.

vous avez raison que le consommateur essaie de se désabonner pour que cela arrive, mais s'ils l'ont fait après la copie temporaire, alors considérer le message déjà en transit.

si vous n'utilisez pas la variable temporaire, et n'utilisez pas le délégué vide, et que quelqu'un se désabonne, vous obtenez une exception de référence nulle, ce qui est fatal, donc je pense que le coût en vaut la peine.

0
répondu Jason Coyne 2009-04-24 15:46:10

Je n'ai jamais vraiment considéré cela comme un problème parce que je ne protège généralement que contre ce genre de mauvais filetage potentiel dans les méthodes statiques (etc) sur mes composants réutilisables, et je ne fais pas d'événements statiques.

est-ce que je le fais mal?

0
répondu Greg D 2009-04-24 17:31:31

envoyez tous vos événements à la construction et laissez-les tranquilles. La conception de la classe de délégué ne peut absolument pas gérer tout autre usage correctement, comme je l'expliquerai dans le dernier paragraphe de ce post.

tout d'abord, il n'y a pas de raison d'essayer de intercepter un événement notification quand votre événement handlers doit déjà prendre une décision synchronisée sur si / Comment répondre à la notification .

tout ce qui peut être notifié doit l'être. Si vos gestionnaires d'événements traitent correctement les notifications (c'est-à-dire qu'ils ont accès à un État d'application faisant autorité et ne répondent que lorsque cela est approprié), alors il sera possible de les notifier à tout moment et de leur faire confiance pour répondre correctement.

la seule fois où un handler ne devrait pas être notifié qu'un événement s'est produit, est si l'événement en fait n'a pas s'est produite! Donc, si vous ne voulez pas qu'un handler soit notifié, arrêtez de générer les événements (c'est-à-dire désactiver le contrôle ou ce qui est responsable de détecter et de faire naître l'événement en premier lieu).

honnêtement, je pense que la classe des délégués est inviolable. La fusion / transition vers un Délégat Multicast a été une énorme erreur, parce qu'elle a effectivement changé la définition (utile) d'un événement de quelque chose qui se produit à un seul instant dans le temps, à quelque chose cela se produit sur une période de temps. Un tel changement nécessite un mécanisme de synchronisation qui peut logiquement l'effondrer en un seul instant, mais le MulticastDelegate ne dispose pas d'un tel mécanisme. La synchronisation devrait englober toute la durée ou l'instant où l'événement a lieu, de sorte qu'une fois qu'une application prend la décision synchronisée de commencer à gérer un événement, elle finit de le gérer complètement (transactionnellement). Avec la boîte noire qui est la classe hybride MulticastDelegate / Delegate, c'est presque impossible, donc adhérer à l'utilisation d'un seul abonné et/ou mettre en œuvre votre propre type de MulticastDelegate qui a une poignée de synchronisation qui peut être retiré pendant que la chaîne de handler est utilisé/modifié . Je recommande ceci, parce que l'alternative serait de mettre en œuvre la synchronisation/intégrité transactionnelle de façon redondante dans tous vos gestionnaires, ce qui serait ridiculement/inutilement complexe.

0
répondu Triynko 2009-05-27 18:49:38

s'il vous Plaît prendre un coup d'oeil ici: http://www.danielfortunov.com/software/%24daniel_fortunovs_adventures_in_software_development/2009/04/23/net_event_invocation_thread_safety Cette solution est la bonne et doit toujours être utilisée à la place de toutes les autres solutions de rechange.

" vous pouvez vous assurer que la liste interne d'invocation a toujours au moins un membre en l'initialisant avec une méthode anonyme. Parce qu'aucune partie externe peut avoir une référence de la méthode anonyme, aucune partie externe peut retirer de la méthode, de sorte que le délégué ne sera jamais nulle" - Programming. net Components, 2e édition, par Juval Löwy

public static event EventHandler<EventArgs> PreInitializedEvent = delegate { };  

public static void OnPreInitializedEvent(EventArgs e)  
{  
    // No check required - event will never be null because  
    // we have subscribed an empty anonymous delegate which  
    // can never be unsubscribed. (But causes some overhead.)  
    PreInitializedEvent(null, e);  
}  
0
répondu Eli 2013-04-26 16:28:30

Je ne crois pas que la question soit confinée au type C#" event". En supprimant cette restriction, pourquoi ne pas réinventer un peu la roue et faire quelque chose dans ce sens?

Élever l'événement de fil en toute sécurité - les meilleures pratiques

  • possibilité de sous-inscrire ou de se désabonner de tout fil pendant une augmentation (race condition supprimée)
  • surcharge de L'opérateur pour += et -= au niveau de la classe.
  • délégué générique défini par l'appelant
0
répondu crokusek 2017-05-23 12:17:39

Merci pour cette discussion utile. Je travaillais sur ce problème récemment et fait la classe suivante qui est un peu plus lent, mais permet d'éviter les appels à des objets disposés.

le point principal ici est que la liste d'invocation peut être modifiée même si l'événement est soulevé.

/// <summary>
/// Thread safe event invoker
/// </summary>
public sealed class ThreadSafeEventInvoker
{
    /// <summary>
    /// Dictionary of delegates
    /// </summary>
    readonly ConcurrentDictionary<Delegate, DelegateHolder> delegates = new ConcurrentDictionary<Delegate, DelegateHolder>();

    /// <summary>
    /// List of delegates to be called, we need it because it is relatevely easy to implement a loop with list
    /// modification inside of it
    /// </summary>
    readonly LinkedList<DelegateHolder> delegatesList = new LinkedList<DelegateHolder>();

    /// <summary>
    /// locker for delegates list
    /// </summary>
    private readonly ReaderWriterLockSlim listLocker = new ReaderWriterLockSlim();

    /// <summary>
    /// Add delegate to list
    /// </summary>
    /// <param name="value"></param>
    public void Add(Delegate value)
    {
        var holder = new DelegateHolder(value);
        if (!delegates.TryAdd(value, holder)) return;

        listLocker.EnterWriteLock();
        delegatesList.AddLast(holder);
        listLocker.ExitWriteLock();
    }

    /// <summary>
    /// Remove delegate from list
    /// </summary>
    /// <param name="value"></param>
    public void Remove(Delegate value)
    {
        DelegateHolder holder;
        if (!delegates.TryRemove(value, out holder)) return;

        Monitor.Enter(holder);
        holder.IsDeleted = true;
        Monitor.Exit(holder);
    }

    /// <summary>
    /// Raise an event
    /// </summary>
    /// <param name="args"></param>
    public void Raise(params object[] args)
    {
        DelegateHolder holder = null;

        try
        {
            // get root element
            listLocker.EnterReadLock();
            var cursor = delegatesList.First;
            listLocker.ExitReadLock();

            while (cursor != null)
            {
                // get its value and a next node
                listLocker.EnterReadLock();
                holder = cursor.Value;
                var next = cursor.Next;
                listLocker.ExitReadLock();

                // lock holder and invoke if it is not removed
                Monitor.Enter(holder);
                if (!holder.IsDeleted)
                    holder.Action.DynamicInvoke(args);
                else if (!holder.IsDeletedFromList)
                {
                    listLocker.EnterWriteLock();
                    delegatesList.Remove(cursor);
                    holder.IsDeletedFromList = true;
                    listLocker.ExitWriteLock();
                }
                Monitor.Exit(holder);

                cursor = next;
            }
        }
        catch
        {
            // clean up
            if (listLocker.IsReadLockHeld)
                listLocker.ExitReadLock();
            if (listLocker.IsWriteLockHeld)
                listLocker.ExitWriteLock();
            if (holder != null && Monitor.IsEntered(holder))
                Monitor.Exit(holder);

            throw;
        }
    }

    /// <summary>
    /// helper class
    /// </summary>
    class DelegateHolder
    {
        /// <summary>
        /// delegate to call
        /// </summary>
        public Delegate Action { get; private set; }

        /// <summary>
        /// flag shows if this delegate removed from list of calls
        /// </summary>
        public bool IsDeleted { get; set; }

        /// <summary>
        /// flag shows if this instance was removed from all lists
        /// </summary>
        public bool IsDeletedFromList { get; set; }

        /// <summary>
        /// Constuctor
        /// </summary>
        /// <param name="d"></param>
        public DelegateHolder(Delegate d)
        {
            Action = d;
        }
    }
}

et l'usage est:

    private readonly ThreadSafeEventInvoker someEventWrapper = new ThreadSafeEventInvoker();
    public event Action SomeEvent
    {
        add { someEventWrapper.Add(value); }
        remove { someEventWrapper.Remove(value); }
    }

    public void RaiseSomeEvent()
    {
        someEventWrapper.Raise();
    }

Test

je l'ai testé de la manière suivante. Je avoir un fil qui crée et détruit des objets comme celui-ci:

var objects = Enumerable.Range(0, 1000).Select(x => new Bar(foo)).ToList();
Thread.Sleep(10);
objects.ForEach(x => x.Dispose());

Dans un Bar (un objet écouteur) constructeur-je m'abonner à SomeEvent (ce qui est mis en œuvre comme indiqué ci-dessus) et de désabonnement dans Dispose :

    public Bar(Foo foo)
    {
        this.foo = foo;
        foo.SomeEvent += Handler;
    }

    public void Handler()
    {
        if (disposed)
            Console.WriteLine("Handler is called after object was disposed!");
    }

    public void Dispose()
    {
        foo.SomeEvent -= Handler;
        disposed = true;
    }

J'ai aussi quelques threads qui soulèvent l'événement dans une boucle.

toutes ces actions sont effectuées simultanément: beaucoup d'auditeurs sont créés et détruits et l'événement est tiré sur même temps.

S'il y avait des conditions de course, je devrais voir un message dans une console, mais il est vide. Mais si j'utilise événements clr comme d'habitude, je vois plein de messages d'avertissement. Donc, je peux conclure qu'il est possible d'implémenter un thread safe events en c#.

Qu'en pensez-vous?

0
répondu Tony 2014-10-18 07:33:31