none
Thread Safe DbContext RRS feed

  • Pregunta

  • Hola, tengo una duda respecto al funcionamiento de Entity Framework. Actualmente estoy trabajando con:

    • visual studio 2015
    • entity framework 6.1.3
    • MySql.Data 6.9.7
    • MySql.Data.Entity 6.9.7

    Se que DbContext no es thread safe, pero me surge una duda, según el último párrafo de este documento https://msdn.microsoft.com/en-us/data/jj729737.aspx.

    Dice así: 

    Multithreading

    The context is not thread safe. You can still create a multithreaded application as long as an instance of the same entity class is not tracked by multiple contexts at the same time.

    Yo interpretaría que si realizo todas las llamadas con context.AsNoTracking(), sería thread safe, pero me gustaría saber la opinión de los demás.

    Ahora, ¿por que pregunto esto? Hice una aplicación de prueba que genera varias consultas de forma paralela a una base de datos dummy (mysql 5.6) y cuando la aplicación se ejecuta, veo que abre tantas conexiones como llamadas realizo.

    El código invocado es más o menos así

    using (var dbCon = new LibraryConnection())
                {
                    Console.WriteLine("\t-Inicio obtener autores key: " + key);
                    var author = dbCon.Author.FirstOrDefault();
                    Console.WriteLine("\t-Fin obtener autores key: " + key);
    
    
                    //Espera 5 segundos
                    System.Threading.Thread.Sleep(5000);
    
                    Console.WriteLine("\t-Inicio obtener libros key: " + key);
                    var book = dbCon.Book.FirstOrDefault();
                    Console.WriteLine("\t-Fin obtener libros key: " + key);
    
                    dbCon.Database.Connection.Close();
                }           

    Y la parte que orquesta esta llamada es la siguiente:

    private static void Do()
            {
                var tester = new Tester();
    
                var tasks = new List<Task>();
                tasks.Add(Task.Factory.StartNew(() => tester.DoLongWorkWithUsing(Guid.NewGuid().ToString())));
                tasks.Add(Task.Factory.StartNew(() => tester.DoLongWorkWithUsing(Guid.NewGuid().ToString())));
                tasks.Add(Task.Factory.StartNew(() => tester.DoLongWorkWithUsing(Guid.NewGuid().ToString())));
                tasks.Add(Task.Factory.StartNew(() => tester.DoLongWorkWithUsing(Guid.NewGuid().ToString())));
                tasks.Add(Task.Factory.StartNew(() => tester.DoLongWorkWithUsing(Guid.NewGuid().ToString())));
                tasks.Add(Task.Factory.StartNew(() => tester.DoLongWorkWithUsing(Guid.NewGuid().ToString())));
    
                Task.WaitAll(tasks.ToArray());            
            }


    http://www.amsoft.cl

    viernes, 4 de septiembre de 2015 2:47

Respuestas

  • No es nada sorprendente una vez que entiendes cómo funciona el Pool de conexiones.

    Primer asunto a considerar: La conexión no admite funcionamiento en multihilo. es decir, una vez que un hilo está enviando peticiones al servidor a través de una conexión, y está recibiendo datos del servidor por esa conexión, no se pueden meter "entre medias" otro bytes generados por otro hilo en esa conexión. Por lo tanto se necesita una conexión por hilo mientras el hilo la tenga en uso.

    Para que no se creen 1000 conexiones si hay 1000 hilos (sean o no del mismo usuario o de 1000 usuarios distintos) es para lo que está el pool de conexiones. El pool está siempre activado por defecto, salvo que lo desactives aposta.

    Cuando tu programa cierra una conexión (cosa que ocurre al salir del bloque "using" en el que la abres), la conexión no se cierra de verdad, sino que se devuelve al pool. La siguiente vez que abres una conexión no se abre de verdad, sino que se recupera del pool la conexión que antes se devolvió al mismo. Esta operación de tomar y devolver conexiones del pool es muy rápida, mientras que si la conexión tuviera que abrirse "de verdad" sería muy lenta.

    Esto te permite atender a miles de usuarios con un número comparativamente pequeño de conexiones. El pool solo llega a crecer hasta el máximo número de conexiones que estuvieron en uso a la vez. Por eso el contenido del "using" debería ser breve, y no esperar entre medias a la realización de operaciones lentas (tales como una interacción del usuario). De esta manera, las conexiones se devuelven al pool en seguida y es poco probable que dos usuarios coincidan a la vez en ese proceso, con lo que el tamaño máximo del pool no crecerá mucho. En la prueba que tú has hecho sí que crece por culpa del Sleep que tienes dentro del using.

    Se puede limitar el tamaño máximo del pool poniendo un parámetro MaxPoolSize en la cadena de conexión. Por defecto es ciento y pico. Si por casualidad llegaran a coincidir muchos hilos simultáneos y se agotase el pool, el siguiente proceso que intente abrir una conexión se queda bloqueado y no continúa hasta que uno de los otros hilos termine y devuelva su conexión. Si ninguno termina, al cabo de un timeout (medio minuto por defecto) se devuelve un error al proceso que intentó abrir la conexión.

    Hay un timeout para las conexiones devueltas al pool (configurable desde la cadena de conexión) pasado el cual las conexiones que hayan excedido el tiempo marcado sin haber sido tomadas del pool sí que se cierran de verdad. Creo que es algo así como 3 minutos por defecto; se puede cambiar con un parámetro en la cadena de conexión.

    • Marcado como respuesta Vladimir Venegas viernes, 4 de septiembre de 2015 17:49
    viernes, 4 de septiembre de 2015 16:09

Todas las respuestas

  • Con el ejemplo que has puesto no debería haber ningún problema. Fíjate que haces un "using" del DbContext dentro de la rutina que consulta los datos, y es esa rutina la que llamas en varios hilos (ella no usa los hilos por dentro). En consecuencia, el DbContext se va a crear y destruir dentro de cada hilo, y nunca vas a tener dos hilos atacando a la misma instancia. Eso es lo correcto, y funcionará bien, pero implica que tendrás abiertas tantas conexiones como hilos tengas en marcha simultaneamente.
    viernes, 4 de septiembre de 2015 5:27
  • Hola Alberto, gracias por responder. Lo mismo pensaba yo, grande fue mi sorpresa al percatarme que las conexiones permanecen abiertas D:

    Invito a todos a revisar el ejemplo realizado, dado que lo encuentro muy extraño.

    • Servidor de base de datos: 54.210.155.167
    • Usuario admin: root
    • Clave admin: 123456
    • Usuario query: sqluser
    • Clave query: 123
    • Fuentes: https://github.com/vvenegasv/EFTesting

    Como se puede ver, en las siguientes imágenes, se muestra que las conexiones están cerradas hasta el inicio de los hilos. En ese punto se abren tantas conexiones como hilos existan, los cuales permanecen abiertos aunque el hilo haya finalizado. El único momento en el cual las conexiones son cerradas, es cuando cierro la aplicacion de consola.

    ¿Por que me preocupa esta situación? Si tengo un servidor web, que en algun momento pueda recibir 1000 peticiones, ¿abrirá 1000 conexiones al servidor de base de datos? Y seguirán abiertas hasta que el pool de iis finalice o se haga alguna limpieza de los recursos utilizados. Estas son los resultados:

    Antes de comenzar

    antes-de-comenzar

    En ejecución

    en-ejecucion

    Fin Ejecucion

    fin-ejecucion

    Aplicacion cerrada

    aplicacion-cerrada


    http://www.amsoft.cl

    viernes, 4 de septiembre de 2015 13:05
  • No es nada sorprendente una vez que entiendes cómo funciona el Pool de conexiones.

    Primer asunto a considerar: La conexión no admite funcionamiento en multihilo. es decir, una vez que un hilo está enviando peticiones al servidor a través de una conexión, y está recibiendo datos del servidor por esa conexión, no se pueden meter "entre medias" otro bytes generados por otro hilo en esa conexión. Por lo tanto se necesita una conexión por hilo mientras el hilo la tenga en uso.

    Para que no se creen 1000 conexiones si hay 1000 hilos (sean o no del mismo usuario o de 1000 usuarios distintos) es para lo que está el pool de conexiones. El pool está siempre activado por defecto, salvo que lo desactives aposta.

    Cuando tu programa cierra una conexión (cosa que ocurre al salir del bloque "using" en el que la abres), la conexión no se cierra de verdad, sino que se devuelve al pool. La siguiente vez que abres una conexión no se abre de verdad, sino que se recupera del pool la conexión que antes se devolvió al mismo. Esta operación de tomar y devolver conexiones del pool es muy rápida, mientras que si la conexión tuviera que abrirse "de verdad" sería muy lenta.

    Esto te permite atender a miles de usuarios con un número comparativamente pequeño de conexiones. El pool solo llega a crecer hasta el máximo número de conexiones que estuvieron en uso a la vez. Por eso el contenido del "using" debería ser breve, y no esperar entre medias a la realización de operaciones lentas (tales como una interacción del usuario). De esta manera, las conexiones se devuelven al pool en seguida y es poco probable que dos usuarios coincidan a la vez en ese proceso, con lo que el tamaño máximo del pool no crecerá mucho. En la prueba que tú has hecho sí que crece por culpa del Sleep que tienes dentro del using.

    Se puede limitar el tamaño máximo del pool poniendo un parámetro MaxPoolSize en la cadena de conexión. Por defecto es ciento y pico. Si por casualidad llegaran a coincidir muchos hilos simultáneos y se agotase el pool, el siguiente proceso que intente abrir una conexión se queda bloqueado y no continúa hasta que uno de los otros hilos termine y devuelva su conexión. Si ninguno termina, al cabo de un timeout (medio minuto por defecto) se devuelve un error al proceso que intentó abrir la conexión.

    Hay un timeout para las conexiones devueltas al pool (configurable desde la cadena de conexión) pasado el cual las conexiones que hayan excedido el tiempo marcado sin haber sido tomadas del pool sí que se cierran de verdad. Creo que es algo así como 3 minutos por defecto; se puede cambiar con un parámetro en la cadena de conexión.

    • Marcado como respuesta Vladimir Venegas viernes, 4 de septiembre de 2015 17:49
    viernes, 4 de septiembre de 2015 16:09