Kihagyás

C# alapok III

Párhuzamos programozás

Manapság a legtöbb eszköz támogatja a többszálú programvégrehajtást, azaz a program futása több részben, egyidőben is történhet. A gyakorlaton a feladat alapú párhuzamosításról esik szó (Task-based asynchronous programming), ez a megközelítés magasabb absztrakciós lehetőséget nyújt a programozónak.

Két fő előnye van a párhuzamosításnak: hatékonyabb és jobban skálázható alkalmazások készülhetnek. A keretrendszer foglalkozik a futtató szálak mennyiségével, a lehető legjobb teljesítményt ígérve.

Task

Mint ahogy a nevéből már sejthető, a Task egy feladat absztrakciója. Nagy általánosságban két típusú Task-ot különböztetünk meg: amely rendelkezik visszatérési értékkel és amely nem. Amely nem rendelkezik visszatérési értékkel, az System.Threading.Task.Task típusú, ellenkező esetben pedig ennek leszármazottja: System.Threading.Task.Task<TResult>. A létrehozott Task objektumtól lekérdezhető a státusza, elindult-e, befejeződött-e, meg lett szakítva, dobott-e kivételt, stb.

Egy Task létrehozásánál egy delegate-t kell megadni, amin keresztül a kívánt kódrészlet elérhető.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
Thread.CurrentThread.Name = "main thread";

Task taskA = new Task(() => {
    for(int i = 0; i < 5; i++)
        Console.WriteLine("Hello from taskA.");
});
taskA.Start();

// Thread.Sleep(4000);
Console.WriteLine("Hello from thread '{0}'.",
                    Thread.CurrentThread.Name);
taskA.Wait();

A fenti példában a taskA objektum létrehozásakor az nem indul el, ehhez expliciten meg kell hívni a Start metódust. Ez a két lépés egyben is végrehajtható, ekkor a Task.Run-t kell használni.

1
2
3
4
Task taskB = Task.Run(() => {
    for(int i = 0; i < 5; i++)
        Console.WriteLine("Hello from taskB.");
});

A Wait metódus implikálja, hogy a Main függvényt futtató szál megvárja a taskA-t, hogy befejezze a munkát.

Amennyiben egy Task rendelkezik visszatérési értékkel, akkor valószínűleg meg is szeretnénk kapni azt. Egy random mágiát kiszámoló függvény esetében a hívás a következő formában volt: double magic = DoMagic(3.14);. A függvény lefutott, majd következő utasításnál a magic változóban elérhető a visszaadott (jelen esetben double) érték.

1
2
3
4
5
6
7
8
private static double DoMagic(double start) {
    double sum = 0;
    for (var value = start; value <= start + 10; value += .1) {
        sum += value;
        Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
    }
    return sum;
}

Több Task elindításához használható a Factory osztály statikus StartNew metódusa.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
Task<double>[] taskArray = {
    Task<double>.Factory.StartNew(() => DoMagic(10.0)),
    Task<double>.Factory.StartNew(() => DoMagic(1000.0)),
    Task<double>.Factory.StartNew(() => DoMagic(10000.0))
}; // 3 db Task-ot tartalmazó tömb.

var results = new double[taskArray.Length];
double sum = 0;

for (int i = 0; i < taskArray.Length; i++) {
    results[i] = taskArray[i].Result;
    sum += results[i];
    Console.Write($" + {results[i]}");
}

Console.WriteLine($" = {sum}");

Amikor a vezérlés eléri taskArray[i].Result-t, akkor az eredményt kérő szál (main) megvárja a feladat befejezését. A futás során a konzolra kiírt számok a szálak azonosítószámai, így látható, hogy a három különböző Task párhuzamosan fut. Több Task futtatása során megtörténhet, hogy ugyanazon a szálon futnak, ilyen esetben érdemes a Task.CurrentId-t használni, mely a feladat azonosítójával tér vissza.

Thread

A Task egy absztrakció a szálak felett, viszont érdemes ezek működésével is tisztában lenni.

1
2
3
4
5
6
7
8
9
static void Work() {
    for (int i = 0; i < 20; i++)
        Console.WriteLine($"Work: {i}");
}

static void WorkWork() {
    for (int i = 0; i < 20; i++)
        Console.WriteLine($"WorkWork: {i}");
}

Ha a metódusok az eddig megszokott, szekvenciális módon futnak le, akkor a kimenet nem tartogat semmi meglepőt.

1
2
Work();
WorkWork();

Amennyiben különböző szálakban futtatjuk a két függvényt, más lesz a konzolra kiírt sorok rendje. Ehhez új szálat kell létrehozni, melynek konstruktorában meg kell adni a futtatni kívánt függvény nevét.

1
2
3
4
5
Thread t1 = new Thread(Work);
Thread t2 = new Thread(WorkWork);

t1.Start();
t2.Start();

A t1.Join() metódussal elérhető, hogy az egyik szál megvárja a másikat, míg a Sleep meghatározott idejű várakoztatást tesz lehetővé. Egy szál megszakításához az Abort függvény használható.

async, await

Ez a két kulcsszó lehetővé teszi párhuzamos konstrukciók létrehozását úgy, hogy a kódban szekvenciális program írásának látszata és könnyedsége megmarad.

Ez egy úgynevezett nem-blokkoló futást eredményez, ami grafikus alkalmazások során hasznos. Grafikus alkalmazások esetén nem engedhető meg, hogy a felhasználói felület lefagyjon. Amennyiben a felhasználó elindít egy hosszú folyamatot (pl. letöltés, adatbázisművelet, hosszú számítások elvégzése), a GUI-nak reszponzívnak kell maradnia.

Ez nem csak kényelmi funkció, hanem a UI lefagyás azt is jelentheti a felhasználónak, hogy a teljes munkamenet meghiúsult és kilép.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
static async Task<int> Work() {
    int sum = 0;
    _ = Task.Run(() => {
        for (int i = 0; i < 20; i++) {
            sum += i;
            Console.WriteLine($"Work: {i}");
        }
    });
    Console.WriteLine($"Returning sum {sum}");
    return sum;
}

// Main
Task<int> task = Work();
int sum = task.Result;
Console.WriteLine(sum);

Jelen esetben a Work metódusban elindul a Task futása, viszont nem várjuk meg. A konzolra a Returning sum 0 kerül ki, majd a Main függvény is kiírja ugyanezt az értéket. Ezzel egyidőben a Work: XY is kiírásra kerül (előtte, utána, ahogy sikerül). Ahhoz, hogy a valós összeget adja vissza a függvény, a következő változtatások szükségesek:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
static async Task<int> Work() {
    int sum = 0;
    await Task.Run(() => { // _ = Task => await Task
        for (int i = 0; i < 20; i++) {
            sum += i;
            Console.WriteLine($"Work: {i}");
        }
    });
    Console.WriteLine($"Returning sum {sum}");
    return sum;
}

// Main
Task<int> task = Work();
int sum = task.Result;
Console.WriteLine(sum);

Fájl műveletek

A gyakorlatok során még nem jutottunk el adatbázisok kezeléséhez, viszont valamilyen állományokkal való munka nélkülözhetetlen a kötelező program megvalósításához. Ehhez nyújt segítséget a fejezet, így csak szöveges és bináris állományok olvasására és írására fókuszál.

Állományok beolvasása

Több módszer áll rendelkezésre az állományok beolvasására. Első esetben az a fájlban található szöveg egy db string változóba kerül.

1
2
3
string filePath = @"path\to\file.txt";
string text = File.ReadAllText(filePath);
Console.WriteLine(text);

A @"<path>" szükséges az útvonalban található \,/ karakterek feloldására, máskülönben escape szekvenciaként szeretné feloldani a fordító.

Amennyiben nem egy szöveges változóban kellene a tartalom, hanem pl. soronként, arra a következő kódrészlet nyújt megoldást.

1
2
3
4
5
6
string filePath = @"path\to\file.txt";
string[] lines = File.ReadAllLines(filePath);

foreach (string line in lines) {
    Console.WriteLine(line);
}

Ezek kis fájlok esetén jól működnek, viszont amennyiben a rendelkezésre álló memóriával vetekszik az olvasandó fájl, más megoldást kell alkalmazni.

1
2
3
4
5
using (var reader = new StreamReader(filePath)) {
    string line;
    while ((line = reader.ReadLine()) != null)
        Console.WriteLine(line);
}

A StreamReader egy pufferbe olvas, mely elérhető a program számára. Ez a preferált módszer.

Állományok írása

Állományok írására az olvasáshoz hasonlóan három módszer kerül ismeretésre.

1
2
var lines = { "Line 1", "Line 2", "Line 3" };
File.WriteAllLines(filePath, lines);

Ebben az esetben második paraméterként egy IEnumerable<string> típusú változót kell átadni, amin végig tud a függévny iterálni.

1
2
3
string text = "Lorem Ipsum es simplemente el texto de relleno " +
              "de las imprentas y archivos de texto...";
File.WriteAllText(filePath, text);

A WriteAllText szöveges paramétert vár (opcionálisan kódolást, UTF-8), melyet ki tud írni a megadott helyre.

1
2
3
4
5
using (var writer = new StreamWriter(filePath)) {
    foreach (var item in someIterable) {
            writer.WriteLine(item);
    }
}

Bináris fájlokkal való munkához a {StreamReader, StreamWriter}-t le kell cserélni {BinaryReader, BinaryWriter} típusokra.

Hivatkozások

How to create Threads in C#

C# - Multithreading

C# THREADING AND TYPES OF THREADING STEP BY STEP

Task-based asynchronous programming

How to create Threads in C#

Async and Await In C#

How to read a text file one line at a time

StreamReader Class

How to: Read text from a file

Use Visual C# to read from and write to a text file

How to write to a text file


Utolsó frissítés: 2023-11-06 15:07:25