web-dev-qa-db-fra.com

Parallel ne fonctionne pas avec Entity Framework

J'ai une liste d'ID et j'ai besoin d'exécuter plusieurs procédures stockées sur chaque ID.

Lorsque j'utilise une boucle foreach standard, cela fonctionne bien, mais lorsque j'ai de nombreux enregistrements, cela fonctionne assez lentement.

Je voulais convertir le code pour qu'il fonctionne avec EF, mais je reçois une exception: "Le fournisseur sous-jacent a échoué sur Open".

J'utilise ce code, à l'intérieur de Parallel.ForEach:

using (XmlEntities osContext = new XmlEntities())
{
    //The code
}

Mais cela lève toujours l'exception.

Une idée comment puis-je utiliser Parallel avec EF? dois-je créer un nouveau contexte pour chaque procédure que j'exécute? J'ai environ 10 procédures, donc je pense qu'il est très mauvais de créer 10 contextes, un pour chacun.

21
m0fo

Les connexions de base de données sous-jacentes qu'Entity Framework utilise ne sont pas pas thread-safe . Vous devrez créer un nouveau contexte pour chaque opération sur un autre thread que vous allez effectuer.

Votre préoccupation sur la façon de paralléliser l'opération est valable; que de nombreux contextes vont coûter cher à ouvrir et à fermer.

Au lieu de cela, vous voudrez peut-être inverser la façon dont vous pensez à paralléliser le code. Il semble que vous parcouriez un certain nombre d'éléments, puis que vous appeliez les procédures stockées en série pour chaque élément.

Si vous le pouvez, créez un nouveau Task<TResult> (ou Task , si vous n'avez pas besoin de résultat) pour chaque procédure puis dans ce Task<TResult>, ouvrez un seul contexte, parcourez tous les éléments, puis exécutez la procédure stockée. De cette façon, vous disposez uniquement d'un nombre de contextes égal au nombre de procédures stockées que vous exécutez en parallèle.

Supposons que vous ayez un MyDbContext avec deux procédures stockées, DoSomething1 et DoSomething2, qui prennent tous deux une instance d'une classe, MyItem.

La mise en œuvre de ce qui précède ressemblerait à quelque chose comme:

// You'd probably want to materialize this into an IList<T> to avoid
// warnings about multiple iterations of an IEnumerable<T>.
// You definitely *don't* want this to be an IQueryable<T>
// returned from a context.
IEnumerable<MyItem> items = ...;

// The first stored procedure is called here.
Task t1 = Task.Run(() => { 
    // Create the context.
    using (var ctx = new MyDbContext())
    // Cycle through each item.
    foreach (MyItem item in items)
    {
        // Call the first stored procedure.
        // You'd of course, have to do something with item here.
        ctx.DoSomething1(item);
    }
});

// The second stored procedure is called here.
Task t2 = Task.Run(() => { 
    // Create the context.
    using (var ctx = new MyDbContext())
    // Cycle through each item.
    foreach (MyItem item in items)
    {
        // Call the first stored procedure.
        // You'd of course, have to do something with item here.
        ctx.DoSomething2(item);
    }
});

// Do something when both of the tasks are done.

Si vous ne pouvez pas exécuter les procédures stockées en parallèle (chacune dépend de leur exécution dans un certain ordre), vous pouvez toujours paralléliser vos opérations , c'est juste un peu plus complexe.

Vous devriez regarder créer des partitions personnalisées sur vos éléments (en utilisant la méthode statique Create sur la classe Partitioner =). Cela vous donnera les moyens d'obtenir IEnumerator<T> implémentations (notez qu'il s'agit de pas IEnumerable<T> donc vous ne pouvez pas foreach dessus).

Pour chaque IEnumerator<T> si vous revenez, vous créez un nouveau Task<TResult> (si vous avez besoin d'un résultat), et dans le Task<TResult> body, vous créez le contexte, puis parcourez les éléments renvoyés par le IEnumerator<T>, en appelant les procédures stockées dans l'ordre.

Cela ressemblerait à ceci:

// Get the partitioner.
OrdinalPartitioner<MyItem> partitioner = Partitioner.Create(items);

// Get the partitions.
// You'll have to set the parameter for the number of partitions here.
// See the link for creating custom partitions for more
// creation strategies.
IList<IEnumerator<MyItem>> paritions = partitioner.GetPartitions(
    Environment.ProcessorCount);

// Create a task for each partition.
Task[] tasks = partitions.Select(p => Task.Run(() => { 
        // Create the context.
        using (var ctx = new MyDbContext())
        // Remember, the IEnumerator<T> implementation
        // might implement IDisposable.
        using (p)
        // While there are items in p.
        while (p.MoveNext())
        {
            // Get the current item.
            MyItem current = p.Current;

            // Call the stored procedures.  Process the item
            ctx.DoSomething1(current);
            ctx.DoSomething2(current);
        }
    })).
    // ToArray is needed (or something to materialize the list) to
    // avoid deferred execution.
    ToArray();
38
casperOne

EF n'est pas thread-safe, vous ne pouvez donc pas utiliser Parallel.

Jetez un oeil à Entity Framework et Multi threading

et cela article .

6
kevin_fitz

C'est ce que j'utilise et fonctionne très bien. Il prend également en charge la gestion des exceptions d'erreur et dispose d'un mode de débogage qui facilite considérablement la traçabilité.

public static ConcurrentQueue<Exception> Parallel<T>(this IEnumerable<T> items, Action<T> action, int? parallelCount = null, bool debugMode = false)
{
    var exceptions = new ConcurrentQueue<Exception>();
    if (debugMode)
    {
        foreach (var item in items)
        {
            try
            {
                action(item);
            }
            // Store the exception and continue with the loop.                     
            catch (Exception e)
            {
                exceptions.Enqueue(e);
            }
        }
    }
    else
    {
        var partitions = Partitioner.Create(items).GetPartitions(parallelCount ?? Environment.ProcessorCount).Select(partition => Task.Factory.StartNew(() =>
        {
            while (partition.MoveNext())
            {
                try
                {
                    action(partition.Current);
                }
                // Store the exception and continue with the loop.                     
                catch (Exception e)
                {
                    exceptions.Enqueue(e);
                }
            }
        }));
        Task.WaitAll(partitions.ToArray());
    }
    return exceptions;
}

Vous l'utilisez comme suit où db est le DbContext d'origine et db.CreateInstance () crée une nouvelle instance en utilisant la même chaîne de connexion.

        var batch = db.Set<SomeListToIterate>().ToList();
        var exceptions = batch.Parallel((item) =>
        {
            using (var batchDb = db.CreateInstance())
            {
                var batchTime = batchDb.GetDBTime();
                var someData = batchDb.Set<Permission>().Where(x=>x.ID = item.ID).ToList();
                //do stuff to someData
                item.WasMigrated = true; //note that this record is attached to db not batchDb and will only be saved when db.SaveChanges() is called
                batchDb.SaveChanges();        
            }                
        });
        if (exceptions.Count > 0)
        {
            logger.Error("ContactRecordMigration : Content: Error processing one or more records", new AggregateException(exceptions));
            throw new AggregateException(exceptions); //optionally throw an exception
        }
        db.SaveChanges(); //save the item modifications
3
realstrategos

Il est un peu difficile de dépanner celui-ci sans savoir quel est le résultat de l'exception interne, le cas échéant. Cela pourrait très simplement être un problème avec la façon dont la chaîne de connexion ou la configuration du fournisseur est installée.

En général, vous devez être prudent avec le code parallèle et EF. Ce que vous faites -devrait- fonctionner, cependant. Une question dans mon esprit; Y a-t-il du travail sur une autre instance de ce contexte avant le parallèle? Selon votre article, vous créez un contexte distinct dans chaque thread. C'est bon. Une partie de moi se demande cependant s'il y a un conflit constructif intéressant entre les multiples contextes. Si vous n'utilisez ce contexte nulle part avant cet appel parallèle, je suggère d'essayer d'exécuter même une simple requête sur le contexte pour l'ouvrir et de vous assurer que tous les bits EF sont activés avant d'exécuter la méthode parallèle. J'avoue, je n'ai pas essayé exactement ce que vous avez fait ici, mais je l'ai fait de près et cela a fonctionné.

0
to11mtm