- Explain why asynchronous code is needed
- Write an
async Task<T>method and wait for it withawait - Wait for several tasks at once with
Task.WhenAll - Combine records, collections, LINQ and validation in a small program
In the morning you make tea. You do not stand by the kettle until it boils — meanwhile you slice bread and set the table. Programs wait a lot too: for a file to be read, for a server to answer, for a database. So that a program does not “freeze” while waiting, C# code is written asynchronously, with the keywords async and await.
Task, async and await
An object that represents work which will finish in the future. Task is work without a result, and Task<T> is work that will eventually produce a result of type T — for example, a Task<string> will give back a string.
An asynchronous method has async in its header, returns Task or Task<T>, and its name traditionally ends with Async. Inside it, await means “wait until this job is done, but do not block anyone else meanwhile”. Task.Delay(500) imitates a 500-millisecond wait — in a real program there would be a file read or an internet request instead. With top-level statements you can write await directly.
Console.WriteLine("Ordering tea...");
string tea = await MakeTeaAsync();
Console.WriteLine(tea);
async Task<string> MakeTeaAsync()
{
Console.WriteLine("The kettle is boiling...");
await Task.Delay(500);
return "Tea is ready!";
}Ordering tea... The kettle is boiling... Tea is ready!
The real power of asynchrony shows when you wait for several jobs at once. If you call a method without await, it starts and hands you a Task. Then Task.WhenAll waits for all the tasks together and gives their results in an array, in the order you started them. Two jobs of half a second each finish in about half a second, not in a whole second.
using System.Diagnostics;
var timer = Stopwatch.StartNew();
Task<string> teaTask = PrepareAsync("tea", 500);
Task<string> toastTask = PrepareAsync("toast", 500);
string[] results = await Task.WhenAll(teaTask, toastTask);
Console.WriteLine(string.Join(" + ", results));
Console.WriteLine($"Faster than one by one: {timer.ElapsedMilliseconds < 1000}");
async Task<string> PrepareAsync(string item, int milliseconds)
{
Console.WriteLine($"Started: {item}");
await Task.Delay(milliseconds);
return $"{item} ready";
}Started: tea Started: toast tea ready + toast ready Faster than one by one: True
Stopwatch measures the time that passed: both jobs together took less than a second.Project: a gradebook report
Now let's combine what you have learned in this course. A teacher keeps students' scores as lines in the format Name;Score. The program must load the data, skip bad lines, give each student a grade and print a short report. We split the work into steps:
- 1Data model
We describe a student with the one-line
record Student(string Name, int Score);. - 2Asynchronous loading
The
LoadLinesAsyncmethod returns the lines. Here we imitate the file withTask.Delay; a real program would useawait File.ReadAllLinesAsync("scores.txt"). - 3Validation
We split each line with
Split(';')and check the score withint.TryParse— a bad line does not stop the program. - 4Analysis and report
We sort and find the average with LINQ, give grades with a switch expression and print a neat table with interpolation.
string[] lines = await LoadLinesAsync();
List<Student> students = [];
foreach (string line in lines)
{
string[] parts = line.Split(';');
if (parts.Length == 2 && int.TryParse(parts[1], out int score))
students.Add(new Student(parts[0], score));
else
Console.WriteLine($"Skipped bad line: '{line}'");
}
Console.WriteLine($"Loaded {students.Count} students");
async Task<string[]> LoadLinesAsync()
{
await Task.Delay(300);
return ["Aysel;95", "Murad;79", "Leyla;abc", "Elvin;45", "Nigar;91"];
}
record Student(string Name, int Score);Skipped bad line: 'Leyla;abc' Loaded 4 students
using System.Globalization;
CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;
List<Student> students = [new("Aysel", 95), new("Murad", 79), new("Elvin", 45), new("Nigar", 91)];
foreach (var s in students.OrderByDescending(s => s.Score))
Console.WriteLine($"{s.Name,-6}{s.Score,4} {Grade(s.Score)}");
Console.WriteLine($"Average: {students.Average(s => s.Score):F1}");
Console.WriteLine($"Passed: {students.Count(s => s.Score >= 50)} of {students.Count}");
string Grade(int score) => score switch
{
>= 90 => "A",
>= 80 => "B",
>= 70 => "C",
>= 50 => "D",
_ => "F"
};
record Student(string Name, int Score);Aysel 95 A Nigar 91 A Murad 79 C Elvin 45 F Average: 77.5 Passed: 3 of 4
Combine both parts in one app.cs file: put the report code after the loading code and keep record Student once at the end of the file. Then grow the project yourself:
- Read the data from a real file:
await File.ReadAllLinesAsync("scores.txt"). - Ask the user to add a new student with
Console.ReadLine(). - Count how many students got each grade with
GroupBy(s => Grade(s.Score)). - Save the report to a file with
await File.WriteAllTextAsync("report.txt", text).
Key points
- Asynchronous code does not freeze the program while it waits.
- An
asyncmethod returnsTaskorTask<T>, and its name ends withAsync. awaitwaits for a task's result; forgetting it gives warningCS4014.Task.WhenAllwaits for several tasks together and saves time.- A real program is built from small parts: a model (record), loading, validation, analysis (LINQ) and a report.
Check yourself
10 questions. Every correct answer earns XP.
Task<int> represent?