Arhiiv

Archive for the ‘Uncategorized’ Category

Rööpülesannete katkestamine

Rööpülesanded jooksevad üksteisest sõltumatult. Kui ühes ülesandes visatakse erand, ei takista see teistel ülesannetel edasi jooksmast. Erandid kogutakse kokku ja jõuavad pealõime.

Vaata seda näidet ühest kasutust programmist, kus iga lõim viskab poole täitmise pealt erandi:

static void Do()
{
  for (int i = 0; i < 100; i++)
    if (i >= 50)
      throw new OperationCanceledException();
}

static void Main(string[] args)
{
  try
  {
    Parallel.Invoke(Do, Do, Do);
  }
  catch (AggregateException x)
  {
    Console.WriteLine(x.InnerExceptions.OfType<OperationCanceledException>().Count() + " tasks canceled");
  }
}

Kõigi lõimede erandid kogutakse pealõime erandisse AggregateException. Ja selle InnerExceptions sisaldab meie näites täpselt sama palju erandeid nagu oli lõimesid. See tähendab, et iga lõim tegi oma tööd teistest sõltumatult ja jõudis oma tööga erandini.

Mida teha siis, kui üks lõim peaks saama teiste tegevuse katkestada? Näiteks, kui töös selgub viga, mis teeb mõttetuks kõigi teiste lõimede edasised pingutused. Teine variant on see, et kasutajal on “Loobu” nupp, millel klõpsates ta saab töö katkestada.

Siin tuleb appi CancellationTokenSource, mis on lõimeturvaline, spetsiaalselt selleks otstarbeks loodud objekt.

Meie järgmises näiteks on meil üldkasutatav muutuja

static CancellationTokenSource ts = new CancellationTokenSource();

ja igaüks (suvaline lõim või ka näiteks kasutaja, kes vajutab mingit nuppu) võib välja kutsuda ts.Cancel().

Ülesandeid luues on vaja kaasa anda Token:

Task.WaitAll
(
  Task.Factory.StartNew(Do, ts.Token),
  Task.Factory.StartNew(Do, ts.Token),
  Task.Factory.StartNew(Do, ts.Token)
);

Protseduur Do kasutab spetsiaalset meetodit ThrowIfCancellationRequested, mis kontrollib, kas ülesanded ei ole globaalselt katkestatud ja viskab erandi kui on.

static void Do()
{
  for (int i = 0; i < 100; i++)
  {
    ts.Token.ThrowIfCancellationRequested();
    if (i >= 50)
      ts.Cancel();
  }
}

Ülaltoodud näite tulemuseks on, et kõik ülesanded jõuavad katkestatud seisu, ent need katkestatakse hetkel, kui ükskõik milline lõimedest jõuab poole peale.

Mõned märkused

ThrowIfCancellationRequested vajab ressursse ja reaalelus ei oleks mõttekas tsükli igas sammus seda välja kutsuda. Näiteks kui tegemist on pikkade ülesannetega ja kasutaja vajutab katkestusnuppu, piisaks sellest, kui katkestamiseks kuluks üks sekund. Iga lõim peaks siis sel juhul kontrollima katkestust kord sekundis.

Teine asi on see, et ThrowIfCancellationRequested poolt visatavat erandit ei tohiks töödelda ülesandesiseselt, muidu see info ei jõua pealõimeni. Pealõimel on sellest kasu, sest Task-objekti Status seatakse sel juhul seisu TaskStatus.Canceled, mitte TaskStatus.Faulted nagu muude erandite puhul. Sel eesmärgil edastatakse Token ka Task.Factory.StartNew-meetodile.

Rubriigid:Uncategorized

Erinevate ülesannete rööpkäivitamine

Seni oleme vaadanud, kuidas andmeid rööpselt töödelda. Aga tegelikult saab rööpselt käivitada täiesti erinevaid ülesandeid.

Toome näite: programmi käivitamisel on vaja internetist alla laadida 3 faili. Esimene asi, mis pähe kargab, on teha seda asünkroonselt WebClient.DownloadStringAsync abil. Pole paha mõte. Iga DownloadStringCompleted sündmus kontrollib, kas kõik failid on juba alla tulnud või on mõni veel puudu.

Parallel.Invoke()

Rööplemine on siin palju lihtsam:

string a = "", b = "", c = "";
      
Parallel.Invoke
(
  () => { a = new WebClient().DownloadString("http://1.com&quot;); },
  () => { b = new WebClient().DownloadString("http://2.com&quot;); },
  () => { c = new WebClient().DownloadString("http://3.com&quot;); }
);

Parallel.Invoke() võtab nii palju funktsioone, kui sa tahad anda, ja lõpetab siis, kui kõik need on valmis saanud. Antud juhtumil lõpetatakse siis pärast kõigi kolme stringi allalaadimist. Lihtne, kas pole?

Task.Factory

Teine tore loom on Task.Factory, mis võimaldab luua ülesandeid ühekaupa ja siis nende täitmist ootama jääda.

List<Task> tasks = new List<Task>();
tasks.Add(Task.Factory.StartNew(() => { a = new WebClient().DownloadString("http://1.com&quot;); }));
tasks.Add(Task.Factory.StartNew(() => { b = new WebClient().DownloadString("http://2.com&quot;); }));
tasks.Add(Task.Factory.StartNew(() => { c = new WebClient().DownloadString("http://3.com&quot;); }));
Task.WaitAny(tasks.ToArray(), TimeSpan.FromSeconds(5));

Task.Factory.StartNew() tagastab viivitamatult Task-objekti, käivitades ülesande taustal.

Task.WaitAll() võtab suvalise arvu ülesandeid (Task-objekte) ja ootab, kuni kõik on täidetud. Lisatingimusena võib anda ajalõpu limiidi.

Teine sarnane meetod on Task.WaitAny(), mis ootab, kuni ükskõik milline ülesannetest on täidetud. Selle tagastudes on tavaliselt täitunud ainult üks ülesannetest.

Jep, rööplemine on tõesti mõnus. Üldiselt ei ole see hädavajalik, sest teiste meetoditega võib saavutada samu tulemusi, kuid see on lihtne kasutada ja loogiliselt üles ehitatud. Ja loomulikult on selline kood palju paremini loetav.

Rubriigid:Uncategorized

Rööpülesannete algväärtustamine

Rööplemine toimub eraldi lõimedes, ja juhtumitel, kui on tarvis ligi pääseda objektile, mis ei ole lõimeturvaline, peaks selle objekti lukustama, aga see võib viia olukorda, kus rööplemisest ei ole mingit tolku, kuna kõik lõimed istuvad ikka ühes järjekorras.

Siin tuleb abiks rööplõimede algväärtustamine, kus igale ülesandele saab luua oma objektid.

Kõige lihtsam näide, kus seda võib vaja minna, on juhuarvude generaator, mis ei ole lõimeturvaline.

double[] prices = new double[10000];

Parallel.For<Random>
(
  0,
  prices.Length,
  () => new Random(),
  (i, state, random) => { prices[i] = random.NextDouble(); return random; },
  random => { }
);

Parallel.For() võtab lisaks miinimum- ja maksimumväärtusele ka kolm funktsiooni.

Esimene käivitatakse iga ülesande alguses, teine iga tsükli jaoks ja kolmas ülesannet lõpetades.

Esimeses funktsioonis me loome Random-objekti (tõele au andes tuleks tunnistada, et see näide on liiga lihtsustatud, sest parema tulemuse jaoks peaks arvestama, et iga Random-objekt vajab erinevat seemet, kuna need luuakse peaaegu üheaegselt).

Teine funktsioon saab positsiooni, staatuse objekti ja ülesande jaoks algväärtustamisel loodud Random-objekti. Seda kasutatakse järgmise juhuarvu genereerimiseks ja massiivi salvestamiseks. Funktsioon peab tagastama järgmises tsüklis kasutatava objekti (meie juhtumil ei ole tarvis seda muuta).

Kolmas funktsioon oleks kasulik, kui Random-objekt vajaks lõpetuseks mingit protseduuri. Meie seda antud näites ei vaja.

Aggregate()

See on tore meetod, mida saab kasutada oma koondfunktsioonide loomiseks, kui sisseehitatutest ei piisa. Kujutan ette, et see kulub marjaks ära laenukalkulaatorite ja statistikaülesannete jaoks. Aga meie teeme midagi väga tavalist: võtame kõigi väärtuste keskmise.

double d = prices.AsParallel().Aggregate
(
  () => new double[2],
  (accum, element) => { accum[0] += element; accum[1]++; return accum; },
  (accumA, accumB) => { accumA[0] += accumB[0]; accumA[1] += accumB[1]; return accumA; },
  accum => accum[0] / accum[1]
);

Seda protseduuri ei saa teostada ülesannet algväärtustamata, sest me ei saa kõigile lõimedele anda ühiskasutatavat double[2]-muutujat. Niisiis vajab iga ülesanne oma muutujat.

Esimene funktsioon loob kohaliku muutuja (millele edaspidi viidatakse nimega accum).

Teine funktsioon on tsükli käitamiseks. Masiivi 0-positsioon on väärtuste summa ja 1-positsioon on väärtuste koguarv. Nagu For-meetodis, tuleb ka siin tagastada muutuja ise. Funktsiooni teine parameeter, element, on loomulikult tsüklis töödeldav sisendväärtus.

Kolmas funktsioon on see, mis käivitatakse kõiki erinevaid lõimesid kokku võttes. See koondab kõigi lõimede tulemused, antud juhul summeerib kõik väärtused, võttes kaks sisendparameetrit ja tagastades summa.

Neljas funktsioon käivitatakse üks kord: siis kui kõik lõimed on summeeritud. Siin me jagame summa väärtuste koguarvuga ja saamegi keskmise.

Kolm varianti

Selle ülesande saaks loomulikult teostada palju lihtsamalt: prices.Average() jadana ja prices.AsParallel().Average() rööpes.

Aga ma loodan, et see näide aitas mõista, mida on vaja, et teostada funktsiooni, mida standardvalikus ei leidu.

Muuseas, nagu võib arvata, ei tarvitse jadas ja rööpes teostatud koondfunktsioonide tulemused täielikult ühtida, sest rööpes tehakse kõik tööd kahes astmes (iga lõime jaoks eraldi ja seejärel koondatuna), mis annab erineva vea kui jadaülesandes.

Minu katse puhul tuli minu enda koodijupi ja AsParallel().Average() vastus sama, kuid jadaprogrammi vastus viimase numbrikoha osas pisut erinev:

0.497030457169437
0.497030457169437
0.497030457169439

See ongi oodatud tulemus, mis näitab, et rööpkoondfunktsioonid töötavad sisemiselt kaheastmelistena.

Rubriigid:Uncategorized