Performance

.NET Processes

Any process have three default streams:

  • Standard input (stdin): This is the only input stream for all terminal or console applications. When you call Console.ReadLine or Console.Read the result is taken from this stream.

  • Standard Output (stdout): When you call output-related commands in the console singleton class, all data goes here. For example the WriteLine or the Write methods.

  • Standard error (stderr): This is a dedicated stream to print error-related content to the console. This is dedicated because some applications and scripting solutions want to hide these messages.

using System;

namespace MyBusiness
{
    class Program
    {
        static void Main(string[] args)
        {
            string ?name = "";

            // Check if parameters are passed
            // If no, ask for the name
            if (args.Length == 0)
            {
                Console.WriteLine("Hello, please tell me your name:");
                // standard input
                name = Console.ReadLine();
            }
            else
            {
                name = args[0];
            }

            // standard output
            Console.WriteLine("stdout: Hello {0} ", name);
            // standard error
            Console.Error.WriteLine("stderr: Hello {0} ", name);
        }
    }
}

Type the following commands at the terminal,

$ dotnet run
$ dotnet run Yun
// Redirect standard output to a file called stdout.txt
$ dotnet run > stdout.txt
// Redirect standard error to a file called stderr.txt
$ dotnet run 2> stderr.txt

try-catch-finally statement Doc

The try-catch statement consists of a try block followed by one or more catch clauses, which specify handlers for different exceptions.

When an exception is thrown, the common language runtime (CLR) looks for the catch statement that handles this exception.

using System;
using System.IO;
using static System.IO.Path;
using static System.Environment;

namespace MyBusiness
{
    class Program
    {
        static void Main(string[] args)
        {
            string file = Combine(CurrentDirectory, "mydata.txt");
            Console.WriteLine($"my file = {file}");
            StreamReader? textReader = null;

            // try-catch-finally statement
            try
            {
                textReader = File.OpenText(file);
                // output all the contents of the file
                Console.WriteLine(textReader.ReadToEnd());
            }
            catch (Exception ex)
            {
                Console.WriteLine($"{ex.GetType()} says {ex.Message}");
            }
            finally
            {
                // if textReader is not null, we close it
                if (textReader != null)
                {
                    textReader.Close();
                }
            }
            Console.WriteLine("End of the program!");
        }
    }
}

When you have mydata.txt under the folder

my file = /Users/yun/Dropbox/FH/BCC/C-Sharp/Codes/CRC_CSD-10/MyBusiness/mydata.txt
Hello yun
End of the program!

When you don’t have mydata.txt under the folder

my file = /Users/yun/Dropbox/FH/BCC/C-Sharp/Codes/CRC_CSD-10/MyBusiness/mydata.txt
System.IO.FileNotFoundException says Could not find file '/Users/yun/Dropbox/FH/BCC/C-Sharp/Codes/CRC_CSD-10/MyBusiness/mydata.txt'.
End of the program!

Define Exception for Your Own Class

In MyBusiness/Program.cs,

using System;
using Animals;

// namespace
namespace MyBusiness
{
    // main program
    internal class Program
    {
        static void Main(string[] args)
        {
            // Create a cat
            Cat coffee = new Cat("Coffee", new DateTime(2022, 3, 20));

            try
            {
                coffee.getPregnant(new DateTime(2023, 3, 31));
                coffee.getPregnant(new DateTime(2022, 4, 25));
            }
            catch (CatException ex)
            {
                Console.WriteLine(ex.Message);
            }
        }
    }
}

In PetLibrary/Animals.cs,

/*
The animal namespace
*/
namespace Animals
{
    public class Cat
    {
        /*
        Class field, including different variables
        they can be organized by similar characteristics
        */

        // The name of the cat
        public string Name;
        // The birthday of the cat
        public DateTime DateOfBirth;

        /*
        Class methods, where functions should be implemented
        */

        // Constructors
        // Default constructor. It will be called by default
        public Cat()
        {
            Name = "Unknown";
            DateOfBirth = DateTime.Today;
        }
        // Parameterized Constructor
        public Cat(string name, DateTime dateOfBirth)
        {
            this.Name = name;
            this.DateOfBirth = dateOfBirth;
        }
        // Finalizer
        ~Cat()
        {
        }

        // method
        public void getPregnant(DateTime day)
        {
            if ((day.Year - DateOfBirth.Year) < 1)
            {
                // It is a handler
                throw new CatException("The cat is too young for producing a kitty!");
            }
            else
            {
                Console.WriteLine($"Ok in {day.Year}!");
            }
        }
    }

    public class CatException : Exception
    {
        // Unlike ordinary methods, constructors are not inherited,
        // so we must explicitly declare and explicitly call the base constructor implementations in System.Exception
        public CatException() : base() { }
        public CatException(string message) : base(message) { }
        public CatException(
          string message, Exception innerException)
          : base(message, innerException) {
           }
    }
}
$ Ok in 2023!
$ The cat is too young for producing a kitty!

Nested Exception

// Nested exception
try
{
    try
    {
        var num = int.Parse("abc"); // Throws FormatException               
    }
    catch (FormatException fe)
    {
        try
        {
            coffee.getPregnant(new DateTime(2022, 4, 25));
        }
        catch (CatException ex)
        {
            throw new CatException(ex.Message, fe);
        }
    }
}
catch (CatException oex)
{
    string inMes = "", outMes = "";
    if (oex.InnerException != null)
    {
        // Inner exception (FormatException) message
        inMes = oex.InnerException.Message;
    }
    outMes = oex.Message;
    Console.WriteLine($"Inner Exception:\n\t{inMes}");
    Console.WriteLine($"Outter Exception:\n\t{outMes}");
}
Inner Exception:
        Input string was not in a correct format.
Outter Exception:
        The cat is too young for producing a kitty!

Patterns

Patterns are distilled commonality that you find in programs.

  • Design Patterns: solves reoccurring problems in software construction.

  • Architectural Patterns: fundamental structural organization for software systems. Architectural patterns are seen as commonality at higher level than design patterns.

Event-Driven Architecture Pattern

Event-driven architecture (EDA) The event-driven architecture pattern is a popular distributed asynchronous architecture pattern used to produce highly scalable applications.

Raising and handling events

Built-in EventHandler Delegate

In Program.cs

using System;
using Animals;

// namespace
namespace MyBusiness
{
    // main program
    internal class Program
    {
        static void Main(string[] args)
        {
            // Create a cat and initialize it
            Cat coffee = new Cat("Coffee", new DateTime(2019, 6, 20));

            // coffee.Shout is an event handler
            coffee.Shout += Cat_Shout; // register with an event
            coffee.Poke();
            coffee.Poke();
            coffee.Poke();
            coffee.Poke();
        }

        // This is a method outside of the Main() method
        private static void Cat_Shout(object sender, EventArgs e)
        {
            Cat cat = (Cat)sender;
            Console.WriteLine($"{cat.Name} is at angry level: {cat.AngerLevel}.");
        }
    }
}

In Animals.cs

using System;
using System.Collections.Generic;

/*
The animal namespace
*/
namespace Animals
{
    // Reuse IComparable<Cat>
    public class Cat
    {
        /*
        Class field, including different variables
        they can be organized by similar characteristics
        */

        // The name of the cat
        public string Name { get; set; }
        public DateTime DateOfBirth { get; set; }
        public int AngerLevel { get; set; }

        /*
        Class methods, where functions should be implemented
        */

        // Constructors
        // Default constructor. It will be called by default
        public Cat()
        {
            Name = "";
        }
        // Parameterized Constructor
        public Cat(string name, DateTime dateOfBirth)
        {
            this.Name = name;
        }

        // event delegate field
        public event EventHandler? Shout;

        // method
        public void Poke()
        {
            AngerLevel++;
            if (AngerLevel >= 3)
            {
                // if something is listening...
                if (Shout != null)
                {
                    // ...then call the delegate
                    Shout.Invoke(this, EventArgs.Empty);
                }
            }
        }
    }
}
Coffee is at angry level: 3.
Coffee is at angry level: 4.

Understanding processes, threads, and tasks

  • Process: A process, with one example being each of the console applications we have created has resources like memory and threads allocated to it.

  • Thread: A thread executes your code, statement by statement. By default, each process only has one thread, and this can cause problems when we need to do more than one task at the same time.

  • Task: A task is a set of program instructions that are loaded in memory.

Running multiple actions

In MyBusiness/Program.cs,

// System libraries

namespace MyBusiness;

class Program
{
    static void Main(string[] args)
    {
        // Synchronism.Run();
        // Asynchronism.Run();
        // AsynchronismWithResource.Run();
        AsynchronismWithResourceLocked.Run();
    }
}

Running multiple actions synchronously

In MyBusiness/Method.cs,


namespace MyBusiness;

public class Method
{
    // Consructor
    // The constructor is private protected to prevent instantiation of this class.
    // A private protected member of a base class is accessible from derived types in its containing assembly only if the static type of the variable is the derived class type. 
    private protected Method()
    {
    }

    private protected static void MethodA()
    {
        Console.WriteLine("Starting Method A...");
        // Parameter of Sleep(): The number of milliseconds for which the thread is suspended.
        Thread.Sleep(3000); // simulate three seconds of work
        Console.WriteLine("Finished Method A.");
    }
    private protected static void MethodB()
    {
        Console.WriteLine("Starting Method B...");
        Thread.Sleep(2000); // simulate two seconds of work
        Console.WriteLine("Finished Method B.");
    }
    private protected static void MethodC()
    {
        Console.WriteLine("Starting Method C...");
        Thread.Sleep(1000); // simulate one second of work
        Console.WriteLine("Finished Method C.");
    }
}

In MyBusiness/Synchronism.cs,

using System.Diagnostics; // Stopwatch

namespace MyBusiness;

public class Synchronism : Method
{
    public Synchronism()
    {
    }

    public static void Run()
    {
        Stopwatch timer = Stopwatch.StartNew();
        Console.WriteLine("Running methods synchronously on one thread.");
        MethodA();
        MethodB();
        MethodC();
        Console.WriteLine($"{timer.ElapsedMilliseconds:#,##0}ms elapsed.");
    }
}
$ Running methods synchronously on one thread.
$ Starting Method A...
$ Finished Method A.
$ Starting Method B...
$ Finished Method B.
$ Starting Method C...
$ Finished Method C.
$ 6,033ms elapsed.
  • Stopwatch Class Doc The Stopwatch class assists the manipulation of timing-related performance counters within managed code.

Running tasks asynchronously

In MyBusiness/Asynchronism.cs,

using System.Diagnostics; // Stopwatch

namespace MyBusiness;

public class Asynchronism : Method
{
    public Asynchronism()
    {
    }

    public static void Run()
    {
        Stopwatch timer = Stopwatch.StartNew();
        // WriteLine("Running methods synchronously on one thread.");
        // MethodA();
        // MethodB();
        // MethodC();

        Console.WriteLine("Running methods asynchronously on multiple threads.");
        Task taskA = new Task(MethodA);
        // Task taskB = new Task(MethodB);
        // Task taskC = new Task(MethodC);
        taskA.Start();
        // taskB.Start();
        // taskC.Start();
        
        // StartNew(action), on the other hand, uses the scheduler of the current thread 
        // which may not use thread pool at all! This can be a matter of concern 
        // particularly when we work with the UI thread!
        Task taskB = Task.Factory.StartNew(MethodB);
        // Task.Run(action) internally uses the default TaskScheduler, which means it 
        // always offloads a task to the thread pool. 
        Task taskC = Task.Run(new Action(MethodC));
        // Task taskC = Task.Factory.StartNew(Action, CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default);        
        Console.WriteLine($"{timer.ElapsedMilliseconds:#,##0}ms elapsed.");
    }
}
Running methods asynchronously on multiple threads.
$ Starting Method A...
$ Starting Method C...
$ Starting Method B...
$ 22ms elapsed.

[IMPORTANT] It is even possible that the console app will end before one or more of the tasks have a chance to start and write to the console!

Waiting for tasks

Task[] tasks = { taskA, taskB, taskC };
Task.WaitAll(tasks);
$ Running methods asynchronously on multiple threads.
$ Starting Method C...
$ Starting Method A...
$ Starting Method B...
$ Finished Method C.
$ Finished Method B.
$ Finished Method A.
3,024ms elapsed.

Accessing a resource from multiple threads

In MyBusiness/MethodWithResource.cs,


namespace MyBusiness;

public class MethodWithResource
{
    public static Random r = new Random();
    public static string Message = ""; // a shared resource

    private protected MethodWithResource()
    {
    }

    private protected static void MethodA()
    {
        // add 5 times char A in the message
        for (int i = 0; i < 5; i++)
        {
            // Next() returns a non-negative random integer that is less than the specified maximum.
            Thread.Sleep(r.Next(2000));
            Message += "A";
            Console.Write(".");
        }
    }
    private protected static void MethodB()
    {
        // add 5 times char B in the message
        for (int i = 0; i < 5; i++)
        {
            Thread.Sleep(r.Next(2000));
            Message += "B";
            Console.Write(".");
        }
    }
}

In MyBusiness/AsynchronismWithResource.cs,

using System.Diagnostics; // Stopwatch

namespace MyBusiness;

public class AsynchronismWithResource : MethodWithResource
{
    public AsynchronismWithResource()
    {
    }

    public static void Run()
    {
        // clean the string Messgage
        Message = "";

        Console.WriteLine("Please wait for the tasks to complete.");
        Stopwatch watch = Stopwatch.StartNew();

        Task a = Task.Factory.StartNew(MethodA);
        Task b = Task.Factory.StartNew(MethodB);

        // Wait until all tasks are finished
        Task.WaitAll(new Task[] { a, b });
        Console.WriteLine();

        Console.WriteLine($"Results: {Message}.");
        Console.WriteLine($"{watch.ElapsedMilliseconds:#,##0} elapsed milliseconds.");
    }
}
$ Please wait for the tasks to complete.
$ ..........
$ Results: BABABABAAB.
$ 8,677 elapsed milliseconds.

[IMPORTANT] This shows that both threads were modifying the message concurrently. In an actual application, this could be a problem.

Applying a mutually exclusive lock to a resource

lock statement Doc

The lock statement acquires the mutual-exclusion lock for a given object, executes a statement block, and then releases the lock. While a lock is held, the thread that holds the lock can again acquire and release the lock. Any other thread is blocked from acquiring the lock and waits until the lock is released. The lock statement ensures that a single thread has exclusive access to that object.

A lock statement of the form lock(x)…, where x is an expression of a reference type.

In MyBusiness/AsynchronismWithResourceLocked.cs,

using System.Diagnostics; // Stopwatch

namespace MyBusiness;

public class AsynchronismWithResourceLocked : MethodWithResource
{
    // You can consider conch is a token, and who has it in hand owns the resource.
    private static object conch = new object();

    public AsynchronismWithResourceLocked()
    {
    }

    public static void Run()
    {
        // clean the string Messgage
        Message = "";

        Console.WriteLine("Please wait for the tasks to complete.");
        Stopwatch watch = Stopwatch.StartNew();

        Task a = Task.Factory.StartNew(MethodA);
        Task b = Task.Factory.StartNew(MethodB);

        // Wait until all tasks are finished
        Task.WaitAll(new Task[] { a, b });
        Console.WriteLine();

        Console.WriteLine($"Results: {Message}.");
        Console.WriteLine($"{watch.ElapsedMilliseconds:#,##0} elapsed milliseconds.");
    }

    // Static methods are not associated with the instance of a class, but with the class 
    // itself. Therefore, when a subclass inherits a static method from its parent class, 
    // it cannot modify the behavior of the static method
    new static void MethodA()
    {
        // add 5 times char A in the message
        // The statements of the critical section. A critical section is a piece of code that accesses a shared resource (data structure or device) but the condition is that only one thread can enter in this section in a time.
        lock (conch)
        {
            for (int i = 0; i < 5; i++)
            {
                Thread.Sleep(2000);
                Message += "A";
                Console.Write(".");
            }
        }
    }

    new static void MethodB()
    {
        // add 5 times char B in the message
        lock (conch)
        {
            for (int i = 0; i < 5; i++)
            {
                Thread.Sleep(2000);
                Message += "B";
                Console.Write(".");
            }
        }
    }
}
$ Please wait for the tasks to complete.
$ ..........
$ Results: AAAAABBBBB.
$ 7,701 elapsed milliseconds.

Understanding the lock statement

lock (conch)
{
    // work with shared resource
}
try
{
    Monitor.Enter(conch);
    // work with shared resource
}
finally
{
    Monitor.Exit(conch);
}

Avoiding deadlocks

Replace the MethodA() to the following content by giving a time to avoid potential deadlock.

static void MethodA()
{
    // add 5 times char A in the message
    try
    {
        // Add a timer to avoid potential deadlock
        if (Monitor.TryEnter(conch, TimeSpan.FromSeconds(15)))
        {
            for (int i = 0; i < 5; i++)
            {
                Thread.Sleep(r.Next(2000));
                Message += "A";
                Console.Write(".");
            }
        }
        else
        {
            Console.WriteLine("Method A failed to enter a monitor lock.");
        }
    }
    finally
    {
        Monitor.Exit(conch);
    }
}
$ Please wait for the tasks to complete.
$ ..........
$ Results: BBBBBAAAAA.
$ 9,263 elapsed milliseconds.

Magic Weapon Example:

MagicWeapon.cs:

// MagicWeapons.cs
using System;
using System.Collections.Generic;
using System.IO;

namespace MagicWeaponsSim
{
    // A delegate that modifies base damage values
    public delegate int DamageModifier(int baseDamage);

    // Class representing a base weapon with damage and attack speed
    public class BaseWeapon
    {
        public int BaseDamage { get; set; }
        public double AttackSpeed { get; set; }

        public double CalculateDPS(DamageModifier damageModifier, double durationInSeconds)
        {
            int attackCount = (int)(durationInSeconds / AttackSpeed);
            double totalDamage = 0;

            for (int i = 0; i < attackCount; i++)
            {
                totalDamage += damageModifier(BaseDamage);
            }

            return totalDamage / durationInSeconds;
        }

        public static Dictionary<string, BaseWeapon> LoadWeaponsFromCSV(string filePath)
        {
            var weapons = new Dictionary<string, BaseWeapon>();
            string[] lines = File.ReadAllLines(filePath);

            foreach (var line in lines[1..]) // Skip header
            {
                var parts = line.Split(',');
                weapons[parts[0]] = new BaseWeapon
                {
                    BaseDamage = int.Parse(parts[1]),
                    AttackSpeed = double.Parse(parts[2])
                };
            }

            return weapons;
        }
    }

    // Result structure for DPS entries
    public class WeaponDPS
    {
        public string? Name { get; set; }
        public double DPS { get; set; }
    }

    public static class Utils
    {
        public static IEnumerable<List<T>> GetCombinations<T>(List<T> list, int k)
        {
            if (k == 0) yield return new List<T>();
            else
            {
                for (int i = 0; i < list.Count; i++)
                {
                    var head = list[i];
                    var rest = list.Skip(i + 1).ToList();
                    foreach (var tailCombo in GetCombinations(rest, k - 1))
                    {
                        tailCombo.Insert(0, head);
                        yield return tailCombo;
                    }
                }
            }
        }

        public static List<List<T>> GetPermutations<T>(List<T> list)
        {
            if (list.Count == 1) return new List<List<T>> { new(list) };
            var result = new List<List<T>>();

            for (int i = 0; i < list.Count; i++)
            {
                var item = list[i];
                var remaining = list.Where((_, index) => index != i).ToList();

                foreach (var perm in GetPermutations(remaining))
                {
                    perm.Insert(0, item);
                    result.Add(perm);
                }
            }

            return result;
        }

        public static void QuickSort(List<WeaponDPS> list, int left, int right)
        {
            if (left >= right) return;

            double pivot = list[(left + right) / 2].DPS;
            int index = Partition(list, left, right, pivot);
            QuickSort(list, left, index - 1);
            QuickSort(list, index, right);
        }

        private static int Partition(List<WeaponDPS> list, int left, int right, double pivot)
        {
            while (left <= right)
            {
                while (list[left].DPS < pivot) left++;
                while (list[right].DPS > pivot) right--;

                if (left <= right)
                {
                    (list[left], list[right]) = (list[right], list[left]);
                    left++;
                    right--;
                }
            }

            return left;
        }

        public static void BubbleSort(List<WeaponDPS> list)
        {
            int n = list.Count;
            bool swapped;

            do
            {
                swapped = false;
                for (int i = 1; i < n; i++)
                {
                    if (list[i - 1].DPS > list[i].DPS)
                    {
                        (list[i - 1], list[i]) = (list[i], list[i - 1]);
                        swapped = true;
                    }
                }
            } while (swapped);
        }
    }
}

Program.cs

// Program.cs
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using MagicWeaponsSim;

class Program
{
    static void Main()
    {
        var weapons = BaseWeapon.LoadWeaponsFromCSV("weapons.csv");
        int strength = 10;
        int dexterity = 20;
        Random rng = new();

        // Our dictionary of damage modifiers
        Dictionary<string, DamageModifier> modifiers = new()
        {
            ["giant"] = dmg => dmg + strength,
            ["random"] = dmg => dmg * rng.Next(0, 3) * 2,
            ["brutal"] = dmg => (dmg * dmg) / 5,
            ["broken"] = dmg => (int)(dmg * 0.7),
            //["spicy"] = dmg => dmg + rng.Next(0, 4) * dexterity,
            //["savage"] = dmg => (int)(dmg * 1.5) - 10,
        };

        var modifierKeys = modifiers.Keys.ToList();
        var allModifierCombos = new List<List<string>>();

        // Generate all combinations of modifiers
        for (int length = 1; length <= modifierKeys.Count; length++)
        {
            foreach (var combo in Utils.GetCombinations(modifierKeys, length))
            {
                allModifierCombos.AddRange(Utils.GetPermutations(combo));
            }
        }

        //Calculate DPS for each weapon and modifier combination
        var results = new List<WeaponDPS>();

        foreach (var weaponEntry in weapons)
        {
            string weaponName = weaponEntry.Key;
            var weapon = weaponEntry.Value;

            foreach (var combo in allModifierCombos)
            {
                DamageModifier chain = dmg => dmg;
                foreach (var mod in combo)
                {
                    DamageModifier current = modifiers[mod];
                    DamageModifier prev = chain;
                    chain = dmg => current(prev(dmg));
                }

                double dps = weapon.CalculateDPS(chain, 10000);

                results.Add(new WeaponDPS
                {
                    Name = $"{string.Join("+", combo)} {weaponName}",
                    DPS = dps
                });
            }
        }

        // Sort results using both algorithms and measure time
        var quickSorted = new List<WeaponDPS>(results);
        var bubbleSorted = new List<WeaponDPS>(results);

        Stopwatch sw = Stopwatch.StartNew();
        Utils.QuickSort(quickSorted, 0, quickSorted.Count - 1);
        sw.Stop();
        double quickTime = sw.Elapsed.TotalMilliseconds;

        sw.Restart();
        Utils.BubbleSort(bubbleSorted);
        sw.Stop();
        double bubbleTime = sw.Elapsed.TotalMilliseconds;

        Console.WriteLine($"QuickSort Time: {quickTime:F6} ms");
        Console.WriteLine($"BubbleSort Time: {bubbleTime:F6} ms");

        // Write results to CSV
        using var writer = new StreamWriter("weapon_dps_results.csv");
        writer.WriteLine("Weapon+DPS_Modifiers,DPS");
        foreach (var entry in quickSorted)
        {
            writer.WriteLine($"{entry.Name},{entry.DPS:F2}");
        }

        writer.WriteLine();
        writer.WriteLine("Sorting Algorithm,Time(ms)");
        writer.WriteLine($"QuickSort,{quickTime:F6}");
        writer.WriteLine($"BubbleSort,{bubbleTime:F6}");

        Console.WriteLine("Results written to weapon_dps_results.csv");
    }
}

Selected Theory

Deadlock