DotnetMinorNotes

Threading

Voordelen

Let op

Performance merk je alleen als je machine meerdere cores heeft

Nadelen

Wat is een thread?

Soort van proces, maar dan lightweight

Een thread krijgt werk mee, namelijk een stukje code. Als het werk klaar is, is de thread klaar. Werk wordt onafhankelijk van elkaar uitgevoerd

Thread switches

Als er 2 threads op 1 core draaien, vindt er thread switching plaats

Voor elke CPU (core) wordt er bijgehouden:

Als er een thread switch plaatsvindt

  1. Data van de huidige thread wordt opgeslagen
  2. Data van de nieuwe thread wordt geladen
  3. CPU (core) krijgt de program counter van de nieuwe thread
  4. CPU (core) krijgt de stack pointer van de nieuwe thread

Thread switching

Scheduler van het OS regelt thread switching

In dotnet kan je een thread aanmaken, maar het OS is degene die de scheduling voor je doet

Race conditions

We hebben de volgende statements:

public class BankAccount
{
    public string Name { get; set; }
    public decimal Balance { get; set; }
}
BankAccount account = new BankAccount()
{
    Name = "Marco",
    Balance = 12.90m
};

In de applicatie is er een thread van tante Truus die eens per jaar 10 euro op de rekening stort:

  1. Zet het huidige balans van 12,90 op de stack
  2. Zet de 10 euro op de stack
  3. Voer instructie uit om de 10 euro bij het balans van 12,90 op te tellen op de stack.
    • Totaal is 22,90
  4. POEF: Er vindt een thread switch plaats
    • Staatsloterij thread zet 1.000.000 bij mij op de rekening. Het balans is nu 1.000.012,90
  5. Thread van tante truus maakt zijn werk af en zet de berekende 22,90 terug op de rekening
  6. Weg miljoen :(

Soms wel, soms niet

Soms gebeurt dit wel, soms gebeurt dit niet. Alleen wanneer de scheduler toevallig op het verkeerde moment de threadswitch laat plaatsvinden.

Het feit dat de 1.000.000 is overschreven door de 22,90 noemen we een ‘race condition’.

Thread soorten

Foreground thread

Deze threads houden de applicatie in leven.

Background thread

Deze threads houden de applicatie niet in leven.

Synchronisation

Locks

private static object lockDing = new object();

lock (lockDing)
{
    // Do work...
}
  1. Als de applicatie bij de lock aankomt, zet die het lock-bitje van ‘lockDing’ op 1.
  2. Wanneer de code in de lock uitgevoerd is, wordt het lock-bitje weer op 0 gezet.

Een lock wordt onderwater Monitor.Enter(...) met daaronder een Monitor.Exit(...).

Het is verstandig om een lock object private voor de class te houden, zodat er niet vanuit andere plekken op hetzelfde object gelockt kan worden.

lock (this)
{
    // Dont lock on this!
}

Lock nooit op this. Andere objecten hebben een referentie naar jou en zouden dus op jouw bitje kunnen locken

Wait handles

AutoResetEvent

private static EventWaitHandle vlaggetje = new AutoResetEvent(false); // Non-signalled

private static void DoSomething()
{
    for(int i = 0; i < 1000; i++)
    {
        if(i == 200)
        {
            vlaggetje.WaitOne();
        }
    }
}

private static void Main(string[] args)
{
    for(int i = 0; i < 1000; i++)
    {
        vlaggetje.Set();
    }
}
  1. vlaggetje wordt op unsignalled gezet
  2. DoSomething arriveert bij de WaitOne() en moet wachten totdat het vlaggetje op signalled komt te staan
  3. Main zet het vlaggetje op signalled door de Set() aan te roepen
  4. DoSomething mag door
    • Het vlaggetje wordt weer op unsignalled gezet, doordat het vlaggetje een AutoResetEvent is (De naam zegt het al)
FileSystemWatcher voorbeeld
AutoResetEvent handle = new AutoResetEvent(false);

FileSystemWatcher watcher = new FileSystemWatcher();
watcher.Path = ".";
watcher.Created += (object sender, FileSystemEventArgs e) =>
{
    handle.Set();
};

File.WriteAllText(".\\test.txt", "Hello, world");

bool signalReceived = handle.WaitOne(1000);
Assert.IsTrue(signalReceived);

In het bovenstaande stukje code is een unit test te zien die controleert of het Created event aangeroepen wordt. Hier wordt een WaitHandle gebruikt om te kijken of de file al geschreven is. Gebeurt dit niet binnen de time-out van 1000 ms, wordt signalReceived de waarde false. Anders wordt deze true. Vervolgens kan hier gemakkelijk op ge-assert worden.

ManualResetEvent

De state moet handmatig op unsignalled gezet worden.

Verschillende threads worden dus doorgelaten bij een WaitOne() tot dat de ManualResetEvent op unsignalled gezet wordt.

Synchronization / Interlocked

i++ is geen ondeelbare actie. Deze operatie gaat als volgt:

  1. Waarde van i wordt opgehaald en naar de stack gekopieerd
  2. Waarde wordt met 1 opgehoogd op de stack
    • Een andere thread kan tijdens deze stap de waarde van i zetten
  3. Nieuwe waarde wordt weer van de stack gehaald en in i gezet
    • Als een andere thread i in de tussentijd gezet heeft, wordt dit overschreven door deze stap

Locks kunnen dit probleem verhelpen. Echter kan hiervoor ook de Interlocked class gebruikt worden.

Enkele voorbeelden van Interlocked methodes:

Deze methodes zijn er voor onder andere ints, longs, etc. Deze zijn er NIET voor onder andere decimals

Asynchronous programming

FileStream heeft de methode Read(...) voor het uitlezen van files. Echter is er ook een BeginRead(...) beschikbaar. Deze doet het volgende:

  1. Start een background thread
  2. Read de file
  3. Roep een callback aan
public void DoSomething()
{
    FileStream fs = ...;
    IAsyncResult result = fs.BeginRead(byte[] buffer, null, null);

    int aantalBytesGelezen = fs.EndRead(result);
    Console.WriteLine(aantalBytesGelezen);
}

Bij de BeginRead(...) hoort een EndRead(...). Deze methode is blokkeert de thread totdat het werk van de BeginRead(...) voltooid is. In het bovenstaande stukje wordt dit geillustreerd:

  1. Er wordt een FileStream geopend
  2. De BeginRead(...) wordt aangeroepen
    • De callback en de state worden op null gezet, omdat deze niet nodig zijn voor dit voorbeeld
      • De callback wordt aangeroepen als de operatie voltooid is. De IAsyncResult wordt vervolgens aan deze callback meegegeven. In de IAsyncResult zit het state object.
  3. De EndRead(...) wordt aangeroepen met als parameter de IAsyncResult van de BeginRead(...)
  4. De aantal gelezen bytes wordt teruggegeven door de EndRead(...) en wordt weergegeven op het scherm

Thread safety

Als iets thread safe is, kan het veilig gebruikt worden vantuit meerdere threads

Concurrent collections

Het .NET framework komt met thread safe collections. Voorbeelden hiervan zijn:

Concurrent collections zorgen ervoor dat je iets trager bent, omdat er bij elke operatie een lock op de collection wordt.

Syncrhonization primitives —–

CountDownEvent

System.Threading.CountdownEvent

Barrier

System.Threading.Barrier

Hek waar threads aankomen. Als alle threads er zijn, gaat de barrier weg en gaat alles tegelijk door.

SpinLock

System.Threading.SpinLock

Lightweight versie van Monitor (lock(ding) { ... })

Hoe?

Threads

Threadpool

Wanneer de Thread class gebruikt wordt, wordt het werk dat gedaan moet worden in de threadpool gezet. Dit gebeurt zodra het thread object aangemaakt wordt.

Als alle threads bezig zijn en er komt meer werk, maakt de threadpool er threads bij (er zit wel een max op). Hier zal dus threadswitching plaatsvinden.

Tasks

Een task is onder water zo’n ‘administratie object’ als IAsyncDing

Task task = Task.Factory.StartNew(() =>
{
    // This would be work which takes a long time
    Console.WriteLine("Working");
});

task.Start();

Een task zonder resultaat

Task<int> task = Task.Factory.StartNew(() =>
{
    // This would be work which takes a long time
    return 42;
});

Console.WriteLine(task.Result);

Een task met resultaat

Thread is duur om aan te maken tegenover een Task. Het volgende moet gebeuren om een Thread aan te maken:

  1. Naar OS
  2. 1MB heap reserveren
  3. OS maakt de thread aan
  4. Thread wordt geregistreerd bij de scheduler
  5. De thread moet geschedulet worden
  6. Er wordt een priorieit aan gehangen

Task functie in delegate stoppen en klaar Als de task gestart wordt, komt die in de queue

Task<double>[] tasks = new Task<double>[]
{
    Task<double>.Factory.StartNew(() => DoSomething()),
    Task<double>.Factory.StartNew(() => DoSomething2())
};

// Wait for all tasks with a time-out of 5000 ms
bool isDone = Task.WaitAll(tasks, 5000);

// Wait for 10000 ms and count how many tasks are done
int numberOfFinishedTasks = Task.WaitAny(tasks, 10000);

Taskpool

Wanneer de Task<T> class gebruikt wordt, wordt het werk dat gedaan moet worden in de taskpool gezet.

Nieuwe Task objecten vanuit een andere Task aanmaken

Deze tasks:

Heeft een andere core geen werk meer, dan ‘steelt’ die werk uit een andere core waar meerdere tasks uitgevoerd worden.

Parallel

Parallel.For

Kan gezien worden als een paralelle versie van een for loop

// from 0 to 1000
Parallel.For(0, 1000, (i) =>
{
    Console.Write(i + " - ");
});
ParalellLoopState loopState = new ParallelLoopState();

Parallel.For(0, 5, (index, loopstate) => Process(index, loopState));

loopState.Break(); stop all iterations beyond current iteration loopState.Stop(); stop all iterations

Parallel.Invoke

Voer methodes tegelijkertijd uit

Parallel.Invoke(() => DoWork1(), () => DoWork2());

Token cancelation

CancellationTokenSource cts = new CancellationTokenSource();
CencellationToken token = cts.Token;

Task myTask = Task.Factory.StartNew(() =>
{
    // Infinite loop
    for (; ; )
    {
        token.ThrowIfCancellationRequested();
        ...
    }
}, token);

// More code...

cts.Cancel();

PLINQ

Voert queries parallel uit

Gebruik de methdoe AsParallel(...) om een query in parallel uit te voeren

var evenNumbers = from number in numbers.AsParallel()
                  where number % 2 == 0
                  select number;

De AsParallel(...) methode doet helemaal niets. Deze methode heeft de volgende parameter AsParallel(this IEnumerable<T> collection). Wat deze methode zo speciaal maakt, is het feit dat de returntype IParallelEnumerable<T>. Dit zorgt ervoor dat je niet de gewone where krijgt, maar een where uit de parallel enumerable universe. Dit geldt ook voor de andere extension methodes. Deze extension methodes doen alles parallel.

AsSequential(...) zorgt ervoor dat alles weer non-parallel/sequentieel gebeurt.

Ordening
var evenNumbers = from number in numbers
                    .AsParallel()
                    .AsOrdered()
                  where number % 2 == 0
                  select number;

Kost wel tijd, maar dan heb je ook wat. Een andere optie is om later een orderby te gebruiken:

var evenNumbers = from number in numbers.AsParallel()
                  where number % 2 == 0
                  orderby number
                  select number;
Exceptions
try 
{
    var evenNumbers = from number in numbers.AsParallel()
        where number % 2 == 0
        orderby number
        select number;
}
catch(AggregateException e)
{
    foreach(var exception in e.InnerExceptions)
    {
        Console.WriteLine(exception.Message);
    }
}

Async / await

Vroeger hadden we de Begin en End methodes. Zie file.BeginRead en file.EndRead (Klikker de klik)

async Task<int> AccessTheWebAsync()
{ 
    HttpClient client = new HttpClient();
    
    Task<string> getStringTask = client.GetStringAsync("http://msdn.microsoft.com");
    
    DoIndependentWork();

    string urlContents = await getStringTask;

    return urlContents.Length;
}
  1. De task getStringTask begint al met het ophalen van http://msdn.microsoft.com in een andere thread.
  2. Op de main thread wordt onafhankelijk werk gedaan.
  3. De content van http://msdn.microsoft.com is nodig na de DoIndependentWork(). await wordt gebruikt om te wachten tot de getStringTask voltooid is.