Parallelizzare in Silverlight 2.0

di Cristian Civera, in .NET,

Durante lo sviluppo di MetadataDiffViewer ho dovuto fare i conti con la grande quantità di informazioni da dover processare e dover trovare un modo di rendere l'interfaccia la più utilizzabile possibile. Per prima cosa ho optato per preparare una struttura di assembly/module/type/member tutta in lazy loading, in questo modo solo nell'espandere un assembly vengono caricati i moduli, solo espandendo un modulo vengono caricati i tipi ecc. LINQ a questo scopo è fantastico perché lavorando con IEnumerable si può parserizzare, incrociare, ordinare, ma elaborare il tutto solo quando viene effettivamente consumato.

Questo comporta però un difetto perché non mi consente di mostrare preventivamente quali assembly sono cambiati. Ho optato quindi per eseguire in un thread in background e ciclare per la struttura ad oggetti; appena trova un elemento cambiato, l'intero assembly è cambiato e perciò evito di proseguire. Questa operazione era comunque lunga e la cosa meno piacevole era vedere la cpu che lavorava solo al 50%, essendo un dual core.

Mi son scritto quindi un piccolo extension method, specifico per la mia esigenza, per parallelizzare un'operazione basata su sorgenti LINQ. Il primo problema è determinare quanti core abbiamo a disposizione, ma non potendo accedere all'OS ho ripiegato sul fatto che il thread pool in SL 2.0 ha 250 thread per core e questo numero non si può cambiare. Perciò ho creato una semplice proprietà:

private const int DefaultThreadsPerCpu = 250;

public static int CpuNumber {
    get {
        int wt; int cpt;
        ThreadPool.GetMaxThreads(out wt, out cpt);
        return Math.Max(1, wt / DefaultThreadsPerCpu);
    }
}

L'extension method che ho poi definito accetta una sorgente dati sul quale ciclare e un delegate ad una funzione che elabora ogni item ed eventualmente restituisce un risultato:

public static IEnumerable<tresult> ForEach<IEnumerable<T>, TItem, TResult>
                                   (this IEnumerable<T> source, Func<TItem, TResult> process)
{
{

Per prima cosa ho ripartito la sorgente in n parti, quante sono le cpu, incrociando gli item. Ad esempio su due core al primo vanno l'item 1,3,5.., al secondo vanno 2,4,6.

int cpuNumber = CpuNumber;

// Elementi da processare per ogni cpu
IEnumerable<TItem>[] items = new IEnumerable<TItem>[cpuNumber];
// Semafori di notifica per ogni cpu
ManualResetEvent[] events = new ManualResetEvent[cpuNumber];
// Risultati ottenuti per ogni cpu
IList<TResult>[] results = new IList<TResult>[cpuNumber];

int itemsPerCpu = (int)Math.Ceiling(source.Count() / (double)cpuNumber);

for (int x = 0; x < items.Length; x++)
{
    items[x] = new TItem[itemsPerCpu];
    int cpu = x;
    // Ripartizione incrociata
    items[x] = source.Where((item, index) => (index + cpu) % cpuNumber == 0);
}

L'uso della variabile interna cpu che semplicemente ricopia x è fondamentale perché all'atto del consumo di items[] x sarebbe sempre all'ultimo valore. Usando LINQ occorre infatti ricordarsi che l'elaborazione avviene a posteriori, a meno non si chiami un ToList/ToArray.

A questo punto avvio i thread e aspetto tramite un WaitHandle che questi abbiamo finito di lavorare:

for (int x = 0; x < items.Length; x++)
{
    // Semaforo di notifica al thread che ha lanciato ForEach
    events[x] = new ManualResetEvent(false);
    results[x] = new List<TResult>(itemsPerCpu);
    Thread t = new Thread(delegate(object i)
    {
        int index = (int)i;

        foreach (TItem item in items[index])
            results[index].Add(process(item));

        events[index].Set();
    });
    t.Start(x);
}

WaitHandle.WaitAll(events);

I risultati sono un List che poi vengono restituiti, in questo caso non nello stesso ordine della sorgente. Tutto questo per la mia esigenza, ma le variabili possono essere tante, in funzione della sorgente (può non essere possibile dividere il carico in quel modo), o del risultato (certi risultati dipendono dai risultati degli altri thread), ma è un inizio. L'uso poi è molto semplice:

var r = from n in Enumerable.Range(1, 1000)
     select "Ciao " + n;
int l = r.ForEach(s => s.Length).Sum();
Commenti

Visualizza/aggiungi commenti

| Condividi su: Twitter, Facebook, LinkedIn

Per inserire un commento, devi avere un account.

Fai il login e torna a questa pagina, oppure registrati alla nostra community.

Nella stessa categoria
I più letti del mese