Megosztás a következőn keresztül:


Lehetséges buktatók a PLINQ használatával

A PLINQ sok esetben jelentős teljesítménybeli javulást tud nyújtani az objektumlekérdezések szekvenciális LINQ-jában. A lekérdezés végrehajtásának párhuzamosítása azonban összetettséghez vezet, amely olyan problémákhoz vezethet, amelyek szekvenciális kódban nem olyan gyakoriak, vagy egyáltalán nem jelentkeznek. Ez a témakör felsorol néhány olyan eljárást, amely elkerülhető a PLINQ-lekérdezések írásakor.

Ne feltételezzük, hogy a párhuzamosság mindig gyorsabb

A párhuzamosítás néha azt eredményezi, hogy a PLINQ-lekérdezés lassabban fut, mint a LINQ to Objects megfelelője. Az alapszabály az, hogy a kevés forráselemet tartalmazó és felhasználók által gyorsan végzett tevékenységek lekérdezései nem valószínű, hogy sokat gyorsulnak. Mivel azonban a teljesítményben számos tényező játszik szerepet, javasoljuk, hogy mérje meg a tényleges eredményeket, mielőtt eldöntené, hogy használja-e a PLINQ-t. További információ: Understanding Speedup in PLINQ.

Ne írjon megosztott memóriahelyekre

A szekvenciális kódban nem ritka, hogy statikus változókból vagy osztálymezőkből olvasnak vagy írnak. Azonban, ha több szál is egyszerre hozzáfér az ilyen változókhoz, nagy a lehetősége a versenyfeltételeknek. Annak ellenére, hogy zárolásokkal szinkronizálhatja a változóhoz való hozzáférést, a szinkronizálás költsége ronthatja a teljesítményt. Ezért azt javasoljuk, hogy a lehető legnagyobb mértékben kerülje vagy legalábbis korlátozza a megosztott állapothoz való hozzáférést a PLINQ-lekérdezésekben.

Kerülje a túl párhuzamosítást

A metódus használatával AsParallel a forrásgyűjtemény particionálásának és a feldolgozószálak szinkronizálásának többletköltségei merülnek fel. A párhuzamosítás előnyeit tovább korlátozza a számítógépen lévő processzorok száma. Nincs gyorsulás, ha több számítás-intenzív szálat futtat egyetlen processzoron. Ezért óvatosnak kell lennie, hogy ne túlzottan párhuzamosítsa a lekérdezést.

A túl párhuzamosítás leggyakoribb forgatókönyve a beágyazott lekérdezésekben fordul elő, ahogyan az az alábbi kódrészletben is látható.

var q = from cust in customers.AsParallel()
        from order in cust.Orders.AsParallel()
        where order.OrderDate > date
        select new { cust, order };
Dim q = From cust In customers.AsParallel()
        From order In cust.Orders.AsParallel()
        Where order.OrderDate > aDate
        Select New With {cust, order}

Ebben az esetben a legjobb, ha csak a külső adatforrást (ügyfeleket) párhuzamosítja, kivéve, ha az alábbi feltételek közül egy vagy több érvényes:

  • A belső adatforrás (cust.Orders) ismerten nagyon hosszú.

  • Minden egyes megrendeléshez költséges számítást végez. (A példában látható művelet nem költséges.)

  • A célrendszerről ismert, hogy elegendő processzorral rendelkezik a lekérdezés cust.Orderspárhuzamosításával előállított szálak számának kezeléséhez.

Az optimális lekérdezési alakzat meghatározásának legjobb módja minden esetben a tesztelés és a mérés. További információ : A PLINQ-lekérdezés teljesítményének mérése.

A nem szálbiztos metódusokra irányuló hívások elkerülése

PLINQ-lekérdezésből nem szálbiztos példánymetódusokba való írás adatsérüléshez vezethet, amely észlelhető vagy sem a programban. Ez kivételekhez is vezethet. Az alábbi példában több szál is megpróbálja egyszerre meghívni a FileStream.Write metódust, amelyet az osztály nem támogat.

Dim fs As FileStream = File.OpenWrite(…)
a.AsParallel().Where(...).OrderBy(...).Select(...).ForAll(Sub(x) fs.Write(x))
FileStream fs = File.OpenWrite(...);
a.AsParallel().Where(...).OrderBy(...).Select(...).ForAll(x => fs.Write(x));

A szálbiztos metódusokra irányuló hívások korlátozása

A .NET legtöbb statikus metódusa szálbiztos, és egyszerre több szálból is meghívható. Az érintett szinkronizálás azonban még ezekben az esetekben is jelentős lassulást eredményezhet a lekérdezésben.

Megjegyzés:

Ezt saját maga is tesztelheti, ha beszúr néhány hívást WriteLine a lekérdezéseibe. Bár ezt a módszert bemutató célokra használják a dokumentációs példákban, ne használja PLINQ-lekérdezésekben.

A szükségtelen rendelési műveletek elkerülése

Amikor a PLINQ párhuzamosan hajt végre egy lekérdezést, a forrásütemezést partíciókra osztja, amelyek egyszerre több szálon is működtethetők. Alapértelmezés szerint a partíciók feldolgozásának és az eredmények kézbesítésének sorrendje nem kiszámítható (kivéve az operátorokat, például OrderBy). Utasíthatja a PLINQ-t, hogy őrizze meg a forrásütemezések sorrendjét, de ez negatív hatással van a teljesítményre. Az ajánlott eljárás az, hogy a lekérdezéseket, amikor csak lehetséges, úgy strukturáljuk, hogy azok ne támaszkodjanak a sorrend megőrzésére. További információ: Order Preservation in PLINQ.

A ForAll és a ForEach előnyben részesítése, ha lehetséges

Bár a PLINQ több szálon hajt végre lekérdezést, ha egy foreach ciklusban (Visual Basicben For Each) használja fel az eredményeket, akkor a lekérdezési eredményeket vissza kell egyesíteni egy szálba, és az enumerátornak sorban kell elérni őket. Bizonyos esetekben ez elkerülhetetlen; azonban, amikor csak lehetséges, használja a ForAll módszert, hogy minden szál saját eredményeket állítson elő, például egy szálbiztos gyűjteménybe, mint pl. System.Collections.Concurrent.ConcurrentBag<T>.

Ugyanez a probléma a következőre Parallel.ForEachvonatkozik: . Más szóval, source.AsParallel().Where().ForAll(...) határozottan előnyben kell részesíteni .Parallel.ForEach(source.AsParallel().Where(), ...)

Vegye figyelembe a szál affinitási problémáit

Egyes technológiák, például az egyszálas apartman (STA) összetevők COM-interoperabilitása, a Windows Forms és a Windows megjelenítési alaprendszer (WPF) szál affinitási korlátozásokat vezetnek be, amelyek megkövetelik, hogy a kód egy adott szálon fusson. A Windows Formsban és a WPF-ben például egy vezérlő csak azon a szálon érhető el, amelyen létrejött. Ha PLINQ-lekérdezésben próbál hozzáférni egy Windows Forms-vezérlő megosztott állapotához, kivétel keletkezik, ha a hibakeresőben fut. (Ez a beállítás kikapcsolható.) Ha azonban a lekérdezés a felhasználói felületi szálon végrehajtódik, akkor a vezérlőt elérheti a lekérdezés eredményeit enumeráló foreach ciklusban, mert a kód csak egy szálon fut.

Ne feltételezze, hogy a ForEach, a For és a ForAll iterációi mindig párhuzamosan futnak

Fontos szem előtt tartani, hogy a Parallel.For, Parallel.ForEach, vagy ForAll hurok egyes iterációi végrehajtásra kerülhetnek, de nem szükségszerűen párhuzamosan. Ezért kerülje az olyan kód írását, amely a helyes működéshez az iterációk párhuzamos végrehajtásától vagy az iterációk meghatározott sorrendben történő végrehajtásától függ.

Ez a kód például valószínűleg holtpontra kerül:

Dim mre = New ManualResetEventSlim()
Enumerable.Range(0, Environment.ProcessorCount * 100).AsParallel().ForAll(Sub(j)
   If j = Environment.ProcessorCount Then
       Console.WriteLine("Set on {0} with value of {1}", Thread.CurrentThread.ManagedThreadId, j)
       mre.Set()
   Else
       Console.WriteLine("Waiting on {0} with value of {1}", Thread.CurrentThread.ManagedThreadId, j)
       mre.Wait()
   End If
End Sub) ' deadlocks
ManualResetEventSlim mre = new ManualResetEventSlim();
Enumerable.Range(0, Environment.ProcessorCount * 100).AsParallel().ForAll((j) =>
{
    if (j == Environment.ProcessorCount)
    {
        Console.WriteLine("Set on {0} with value of {1}", Thread.CurrentThread.ManagedThreadId, j);
        mre.Set();
    }
    else
    {
        Console.WriteLine("Waiting on {0} with value of {1}", Thread.CurrentThread.ManagedThreadId, j);
        mre.Wait();
    }
}); //deadlocks

Ebben a példában az egyik iteráció beállít egy eseményt, és az összes többi iteráció várakozik az eseményen. A várakozási iterációk egyike sem fejeződhet be, amíg az eseménybeállítási iteráció be nem fejeződik. Előfordulhat azonban, hogy a várakozási iterációk blokkolják a párhuzamos ciklus végrehajtásához használt összes szálat, mielőtt az eseménybeállítási iterációnak lehetősége lett volna végrehajtani. Ez holtpontot eredményez – az eseménybeállítási iteráció soha nem lesz végrehajtva, és a várakozó iterációk soha nem fognak felébredni.

Különösen fontos, hogy a párhuzamos ciklus egy iterációjának sose kelljen várnia egy másik iterációra a haladás érdekében. Ha a párhuzamos hurok úgy dönt, hogy egymás után ütemezi az iterációkat, de ellenkező sorrendben, holtpont jön létre.

Lásd még