web-dev-qa-db-fra.com

Un serveur Web asynchrone non bloquant, à un seul thread (comme Node.js) est-il possible dans .NET?

Je regardais cette question , cherchant un moyen de créer un serveur Web asynchrone non bloquant à un seul thread , basé sur des événements dans .NET.

Cette réponse avait l'air prometteur au début, en affirmant que le corps du code s'exécute dans un seul thread.

Cependant, j'ai testé cela en C #:

using System;
using System.IO;
using System.Threading;

class Program
{
    static void Main()
    {
        Console.WriteLine(Thread.CurrentThread.ManagedThreadId);

        var sc = new SynchronizationContext();
        SynchronizationContext.SetSynchronizationContext(sc);
        {
            var path = Environment.ExpandEnvironmentVariables(
                @"%SystemRoot%\Notepad.exe");
            var fs = new FileStream(path, FileMode.Open,
                FileAccess.Read, FileShare.ReadWrite, 1024 * 4, true);
            var bytes = new byte[1024];
            fs.BeginRead(bytes, 0, bytes.Length, ar =>
            {
                sc.Post(dummy =>
                {
                    var res = fs.EndRead(ar);

                    // Are we in the same thread?
                    Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
                }, null);
            }, null);
        }
        Thread.Sleep(100);
    }
}

Et le résultat a été:

1
5

Il semble donc que, contrairement à la réponse, le thread initiant la lecture et le thread terminant la lecture ne sont pas différents .

Alors maintenant, ma question est de savoir comment obtenir un serveur Web asynchrone non bloquant à un seul thread , basé sur des événements dans .NET?

57
user541686

L'ensemble SetSynchronizationContext est un hareng rouge, c'est juste un mécanisme de marshaling, le travail se passe toujours dans le IO Thread Pool.

Ce que vous demandez, c'est un moyen de mettre en file d'attente et de récolter appels de procédure asynchrone pour tous vos IO travail à partir du thread principal. De nombreux frameworks de niveau supérieur encapsulent ce type de fonctionnalité, le plus célèbre étant libevent .

Il y a un grand récapitulatif sur les différentes options ici: Quelle est la différence entre epoll, poll, threadpool? .

.NET prend déjà en charge la mise à l'échelle pour vous en disposant d'un "pool de threads IO" spécial qui gère l'accès IO lorsque vous appelez les méthodes BeginXYZ. Ce IO Thread Pool doit avoir au moins 1 thread par processeur sur la boîte. Voir: ThreadPool.SetMaxThreads .

Si une application à thread unique est une exigence critique (pour une raison folle), vous pouvez bien sûr interopérer tout cela en utilisant DllImport (voir un exemple ici )

Mais ce serait une très tâche complexe et risquée :

Pourquoi ne prenons-nous pas en charge les APC comme mécanisme d'achèvement? Les APC ne sont vraiment pas un bon mécanisme de complétion à usage général pour le code utilisateur. La gestion de la réentrance introduite par les APC est presque impossible; à chaque fois que vous bloquez un verrou, par exemple, une exécution arbitraire des E/S peut prendre le dessus sur votre thread. Il peut essayer d'acquérir ses propres verrous, ce qui peut entraîner des problèmes d'ordre de verrouillage et donc un blocage. Pour éviter cela, il faut une conception méticuleuse et la possibilité de s'assurer que le code de quelqu'un d'autre ne s'exécutera jamais pendant votre attente d'alerte, et vice-versa. Cela limite considérablement l'utilité des APC.

Donc, pour récapituler. Si vous voulez un processus géré à fil unique qui fasse tout son travail en utilisant APC et les ports d'achèvement, vous devrez le coder manuellement. La construire serait risqué et délicat.

Si vous voulez simplement un réseau à grande échelle , vous pouvez continuer à utiliser BeginXYZ et famille et soyez assuré qu'il fonctionnera bien, car il utilise APC. Vous payez un petit prix de marshalling entre les threads et l'implémentation particulière .NET.

De: http://msdn.Microsoft.com/en-us/magazine/cc300760.aspx

L'étape suivante de l'extension du serveur consiste à utiliser des E/S asynchrones. Les E/S asynchrones réduisent le besoin de créer et de gérer des threads. Cela conduit à un code beaucoup plus simple et constitue également un modèle d'E/S plus efficace. Les E/S asynchrones utilisent des rappels pour gérer les données et les connexions entrantes, ce qui signifie qu'il n'y a pas de listes à configurer et à analyser et qu'il n'est pas nécessaire de créer de nouveaux threads de travail pour gérer les E/S en attente.

Un fait intéressant, côté, est que le thread unique n'est pas le moyen le plus rapide de faire des sockets asynchrones sur Windows en utilisant des ports de complétion voir: http://doc.sch130.nsc.ru/www.sysinternals.com/ntw2k/ info/comport.shtml

Le but d'un serveur est de générer le moins de changements de contexte possible en faisant en sorte que ses threads évitent les blocages inutiles, tout en maximisant le parallélisme en utilisant plusieurs threads. L'idéal est qu'il y ait un thread qui traite activement une requête client sur chaque processeur et que ces threads ne se bloquent pas s'il y a des requêtes supplémentaires en attente lorsqu'elles terminent une requête. Cependant, pour que cela fonctionne correctement, l'application doit pouvoir activer un autre thread lors du traitement d'une requête cliente bloquée sur les E/S (comme lorsqu'elle lit un fichier dans le cadre du traitement).

40
Sam Saffron

Ce dont vous avez besoin est une "boucle de message" qui prend la prochaine tâche dans une file d'attente et l'exécute. En outre, chaque tâche doit être codée afin qu'elle termine autant de travail que possible sans bloquer, puis met en file d'attente des tâches supplémentaires pour reprendre une tâche qui a besoin de temps plus tard. Il n'y a rien de magique à cela: ne jamais utiliser un appel de blocage et ne jamais générer de threads supplémentaires.

Par exemple, lors du traitement d'un HTTP GET, le serveur peut lire autant de données que celles actuellement disponibles sur le socket. Si ces données ne sont pas suffisantes pour gérer la demande, mettez à nouveau en file d'attente une nouvelle tâche à lire dans le socket à l'avenir. Dans le cas d'un FileStream, vous souhaitez définir le ReadTimeout sur l'instance à une valeur faible et être prêt à lire moins d'octets que le fichier entier.

C # 5 rend ce modèle beaucoup plus trivial. Beaucoup de gens pensent que la fonctionnalité asynchrone implique le multithreading, mais ce n'est pas le cas . En utilisant async, vous pouvez essentiellement obtenir la file d'attente des tâches que j'ai mentionnée plus tôt sans jamais l'expliciter en la gérant.

17
Chris Pitman

Oui, ça s'appelle Manos de mono

Sérieusement, toute l'idée derrière manos est un serveur Web piloté par un événement asynchrone à un seul thread.

Haute performance et évolutif. Inspiré de tornadoweb, la technologie qui alimente le flux d'amis, Manos est capable de milliers de connexions simultanées, idéal pour les applications qui créent des connexions persistantes avec le serveur.

Le projet semble nécessiter peu d'entretien et ne serait probablement pas prêt pour la production, mais il constitue une bonne étude de cas pour démontrer que cela est possible.

10
Raynos

Voici une excellente série d'articles expliquant ce qu'est IO Ports d'achèvement et comment ils sont accessibles via C # (c'est-à-dire que vous devez PInvoke dans les appels d'API Win32 à partir de Kernel32.dll).

Remarque: Le libuv la plateforme multiplateforme IO framework derrière node.js utilise IOCP sur Windows et libev sur les systèmes d'exploitation Unix.

http://www.theukwebdesigncompany.com/articles/iocp-thread- Covoiturage.php

7
mythz

je me demande si personne n'a mentionné kayak c'est fondamentalement la réponse de C # à Pythons twisted , JavaScripts node.js ou Rubys eventmachine =

2
martyglaubitz

J'ai tripoté ma propre implémentation simple d'une telle architecture et je l'ai mise en place sur github . Je le fais plus comme une chose d'apprentissage. Mais ça a été très amusant et je pense que je vais débusquer davantage.

C'est très alpha, donc il est susceptible de changer, mais le code ressemble un peu à ceci:

   //Start the event loop.
   EventLoop.Start(() => {

      //Create a Hello World server on port 1337.
      Server.Create((req, res) => {
         res.Write("<h1>Hello World</h1>");
      }).Listen("http://*:1337");

   });

Plus d'informations à ce sujet peuvent être trouvées ici .

1
Ben Lesh

J'ai développé un serveur basé sur HttpListener et une boucle d'événements, prenant en charge MVC, WebApi et le routage. Pour ce que j'ai vu, les performances sont bien meilleures que IIS + MVC standard, pour le MVCMusicStore, je suis passé de 100 requêtes par seconde et 100% CPU à 350 avec 30% CPU. Si quelqu'un veut l'essayer, j'ai du mal à obtenir des commentaires! En fait, est présent un modèle pour créer des sites Web basés sur cette structure.

Notez que JE N'UTILISE PAS ASYNC/AWAIT jusqu'à ce que cela soit absolument nécessaire. Les seules tâches que j'utilise sont celles pour les opérations liées aux E/S comme l'écriture sur le socket ou la lecture de fichiers.

PS toute suggestion ou correction est la bienvenue!

1
Kendar

vous pouvez ce cadre SignalR et ce Blog à ce sujet

0
iomar

Tout d'abord sur SynchronizationContext. C'est comme Sam l'a écrit. La classe de base ne vous donnera pas de fonctionnalité à un seul thread. Vous avez probablement eu cette idée de WindowsFormsSynchronizationContext qui fournit des fonctionnalités pour exécuter du code sur le thread d'interface utilisateur.

Vous pouvez en savoir plus ici

J'ai écrit un morceau de code qui fonctionne avec les paramètres ThreadPool. (Encore quelque chose que Sam a déjà souligné).

Ce code enregistre 3 actions asynchrones à exécuter sur le thread libre. Ils s'exécutent en parallèle jusqu'à ce que l'un d'eux modifie les paramètres de ThreadPool. Ensuite, chaque action est exécutée sur le même thread.

Cela prouve seulement que vous pouvez forcer l'application .net à utiliser un seul thread. La véritable implémentation d'un serveur Web qui recevrait et traiterait les appels sur un seul thread est quelque chose de complètement différent :).

Voici le code:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.IO;

namespace SingleThreadTest
{
    class Program
    {
        class TestState
        {
            internal string ID { get; set; }
            internal int Count { get; set; }
            internal int ChangeCount { get; set; }
        }

        static ManualResetEvent s_event = new ManualResetEvent(false);

        static void Main(string[] args)
        {
            Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
            int nWorkerThreads;
            int nCompletionPortThreads;
            ThreadPool.GetMaxThreads(out nWorkerThreads, out nCompletionPortThreads);
            Console.WriteLine(String.Format("Max Workers: {0} Ports: {1}",nWorkerThreads,nCompletionPortThreads));
            ThreadPool.GetMinThreads(out nWorkerThreads, out nCompletionPortThreads);
            Console.WriteLine(String.Format("Min Workers: {0} Ports: {1}",nWorkerThreads,nCompletionPortThreads));
            ThreadPool.QueueUserWorkItem(new WaitCallback(LetsRunLikeCrazy), new TestState() { ID = "A  ", Count = 10, ChangeCount = 0 });
            ThreadPool.QueueUserWorkItem(new WaitCallback(LetsRunLikeCrazy), new TestState() { ID = " B ", Count = 10, ChangeCount = 5 });
            ThreadPool.QueueUserWorkItem(new WaitCallback(LetsRunLikeCrazy), new TestState() { ID = "  C", Count = 10, ChangeCount = 0 });
            s_event.WaitOne();
            Console.WriteLine("Press enter...");
            Console.In.ReadLine();
        }

        static void LetsRunLikeCrazy(object o)
        {
            if (s_event.WaitOne(0))
            {
                return;
            }
            TestState oState = o as TestState;
            if (oState != null)
            {
                // Are we in the same thread?
                Console.WriteLine(String.Format("Hello. Start id: {0} in thread: {1}",oState.ID, Thread.CurrentThread.ManagedThreadId));
                Thread.Sleep(1000);
                oState.Count -= 1;
                if (oState.ChangeCount == oState.Count)
                {
                    int nWorkerThreads = 1;
                    int nCompletionPortThreads = 1;
                    ThreadPool.SetMinThreads(nWorkerThreads, nCompletionPortThreads);
                    ThreadPool.SetMaxThreads(nWorkerThreads, nCompletionPortThreads);

                    ThreadPool.GetMaxThreads(out nWorkerThreads, out nCompletionPortThreads);
                    Console.WriteLine(String.Format("New Max Workers: {0} Ports: {1}", nWorkerThreads, nCompletionPortThreads));
                    ThreadPool.GetMinThreads(out nWorkerThreads, out nCompletionPortThreads);
                    Console.WriteLine(String.Format("New Min Workers: {0} Ports: {1}", nWorkerThreads, nCompletionPortThreads));
                }
                if (oState.Count > 0)
                {
                    Console.WriteLine(String.Format("Hello. End   id: {0} in thread: {1}", oState.ID, Thread.CurrentThread.ManagedThreadId));
                    ThreadPool.QueueUserWorkItem(new WaitCallback(LetsRunLikeCrazy), oState);
                }
                else
                {
                    Console.WriteLine(String.Format("Hello. End   id: {0} in thread: {1}", oState.ID, Thread.CurrentThread.ManagedThreadId));
                    s_event.Set();
                }
            }
            else
            {
                Console.WriteLine("Error !!!");
                s_event.Set();
            }
        }
    }
}
0
Grzegorz W

LibuvSharp est un wrapper pour libuv, qui est utilisé dans le projet node.js pour async IO. Mais il ne contient que des fonctionnalités TCP/UDP/Pipe/Timer de bas niveau. Et cela restera comme ça, écrire un serveur Web dessus est une toute autre histoire. Il ne prend même pas en charge la résolution DNS, car il s'agit simplement d'un protocole au-dessus d'udp.

0
Andrius Bentkus

Je pense que c'est possible, voici un exemple open-source écrit en VB.NET et C #:

https://github.com/perrybutler/dotnetsockets/

Il utilise le modèle asynchrone basé sur les événements (EAP), le modèle IAsyncResult et le pool de threads (IOCP). Il va sérialiser/marshaler les messages (les messages peuvent être n'importe quel objet natif tel qu'une instance de classe) en paquets binaires, transférer les paquets via TCP, puis désérialiser/démarsaler les paquets à la fin de la réception afin que votre objet natif fonctionne avec . Cette partie est un peu comme Protobuf ou RPC.

Il a été initialement développé comme un "netcode" pour les jeux multijoueurs en temps réel, mais il peut servir à de nombreuses fins. Malheureusement, je n'ai jamais réussi à l'utiliser. Peut-être que quelqu'un d'autre le fera.

Le code source a beaucoup de commentaires donc il devrait être facile à suivre. Prendre plaisir!

0
perry

Une sorte de support du système d'exploitation est essentielle ici. Par exemple, Mono utilise epoll sous Linux avec des E/S asynchrones, il devrait donc très bien évoluer (toujours un pool de threads). Si vous recherchez et les performances et l'évolutivité, essayez-le.

D'un autre côté, l'exemple de serveur Web C # (avec des bibliothèques natives) basé sur l'idée que vous avez mentionnée peut être Manos de Mono. Le projet n'a pas été actif récemment; cependant, l'idée et le code sont généralement disponibles. Lire ceci (en particulier la partie "Regardons de plus près Manos").

Modifier:

Si vous voulez juste que le rappel soit déclenché sur votre thread principal, vous pouvez faire un peu d'abus des contextes de synchronisation existants comme le répartiteur WPF. Votre code, traduit dans cette approche:

using System;
using System.IO;
using System.Threading;
using System.Windows;

namespace Node
{
    class Program
    {
        public static void Main()
        {
            var app = new Application();
            app.Startup += ServerStart;
            app.Run();
        }

        private static void ServerStart(object sender, StartupEventArgs e)
        {
            var dispatcher = ((Application) sender).Dispatcher;
            Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
            var path = Environment.ExpandEnvironmentVariables(
                @"%SystemRoot%\Notepad.exe");
            var fs = new FileStream(path, FileMode.Open,
                FileAccess.Read, FileShare.ReadWrite, 1024 * 4, true);
            var bytes = new byte[1024];
            fs.BeginRead(bytes, 0, bytes.Length, ar =>
            {
                dispatcher.BeginInvoke(new Action(() =>
                {
                    var res = fs.EndRead(ar);

                    // Are we in the same thread?
                    Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
                }));
            }, null);
        }
    }
}

imprime ce que vous souhaitez. De plus, vous pouvez définir des priorités avec le répartiteur. Mais d'accord, c'est moche, hacky et je ne sais pas pourquoi je le ferais de cette façon pour une autre raison que de répondre à votre demande de démo;)

0
konrad.kruczynski

Voici un autre implémentation du serveur Web de boucle d'événements appelé SingleSand . Il exécute toute la logique personnalisée à l'intérieur de la boucle d'événements à un seul thread, mais le serveur Web est hébergé dans asp.net. En réponse à la question, il n'est généralement pas possible d'exécuter une application à un seul thread pur en raison de la nature multi-thread .NET. Il y a certaines activités qui s'exécutent dans des threads séparés et le développeur ne peut pas changer leur comportement.

0
neleus