Task and async in C#
Let’s explore various aspects of C# programming through different code examples.
1. Parallel Processing with Task Parallel Library (TPL)
The first example demonstrates parallel processing using TPL:
ParallelOptions options = new ParallelOptions { MaxDegreeOfParallelism = 10 };
Parallel.For(0, 10, options, i =>
{
// Parallel processing code here
});
// Resource-intensive computation
static void ResourceIntensiveFunction()
{
double result = 0;
for (int i = 0; i < 1000000000; i++)
{
result += Math.Sqrt(i);
}
}
This code shows how to limit parallel processing to 10 threads and measure performance using Stopwatch
.
2. Delegates in C#
Here’s an example of using delegates for method invocation:
public delegate int Operation(int a, int b);
public static int Add(int a, int b) => a + b;
public static int Multiply(int a, int b) => a * b;
public static void PerformOperation(int a, int b, Operation operation)
{
int result = operation(a, b);
Console.WriteLine("Result: " + result);
}
Delegates provide a way to pass methods as parameters, enabling flexible and reusable code.
3. Unicode Output in Console
Console applications can display Unicode characters:
Console.OutputEncoding = System.Text.Encoding.Unicode;
Console.WriteLine("\u2304"); // ⌄
Console.WriteLine("\U0001F4AF"); // 💯
Console.WriteLine("\U0001F604"); // 😄
Setting proper encoding ensures correct display of Unicode characters.
4. Asynchronous Matrix Operations
This example shows parallel matrix computations:
static async Task Main()
{
const int N = 10000;
Task[] tasks = new Task[4];
for (int i = 0; i < 4; i++)
{
tasks[i] = Task.Run(() => ComputeMatrixInverse(N));
}
await Task.WhenAll(tasks);
}
The code demonstrates parallel matrix operations using Tasks for improved performance.
5. Task and Thread Management
Here’s an example of task and thread management:
Task singleTask = Task.Run(() => {
Console.WriteLine($"Single task on thread {Thread.CurrentThread.ManagedThreadId}");
Thread.Sleep(2000);
});
Parallel.For(0, 4, i => {
Console.WriteLine($"Parallel {i} on thread {Thread.CurrentThread.ManagedThreadId}");
Thread.Sleep(2000);
});
This demonstrates different ways to handle concurrent operations in C#.
6. Timer Implementation
Finally, a timer example:
private static System.Timers.Timer aTimer;
private static void SetTimer()
{
aTimer = new System.Timers.Timer(2000);
aTimer.Elapsed += OnTimedEvent;
aTimer.AutoReset = true;
aTimer.Enabled = true;
}
This shows how to implement recurring events using Timer in C#.
Each of these examples demonstrates different aspects of C# programming, from basic concepts to advanced parallel processing techniques. The code samples provide practical implementations that can be used as building blocks for larger applications.
volatile
Keyword
Why volatile
is Useful
In multi-threaded applications, threads may cache variables in their local memory, leading to inconsistencies when the variable is updated by another thread. The volatile
keyword prevents such caching, ensuring that the variable is always read from the main memory, thus maintaining data consistency across threads.
How volatile
is Used
To use the volatile
keyword, simply declare the field with the volatile
modifier. For example:
private volatile bool keepRunning = true;
In the provided code, keepRunning
is marked as volatile
to ensure that changes made by one thread (e.g., the Stop
method) are immediately visible to other threads (e.g., the Worker
method).
When to Use volatile
- When a field is accessed by multiple threads without using locks.
- When you need to ensure that the most recent value of a field is always read by all threads.
When Not to Use volatile
- When more complex synchronization is required (e.g., atomic operations, multiple fields).
- When you need to perform compound operations (e.g., incrementing a counter) that must be atomic.
- When the field is not accessed by multiple threads.
In such cases, other synchronization mechanisms like lock
, Monitor
, or Interlocked
should be used to ensure thread safety.
using System;
using System.Threading;
public class VolatileExample
{
private volatile bool keepRunning = true;
public void Start()
{
Thread workerThread = new Thread(Worker);
workerThread.Start();
}
private void Worker()
{
while (keepRunning)
{
Console.WriteLine("Working...");
Thread.Sleep(1000); // Simulate work
}
Console.WriteLine("Stopped working.");
}
public void Stop()
{
keepRunning = false;
}
public static void Main()
{
VolatileExample example = new VolatileExample();
example.Start();
// Simulate some work, then stop the worker thread
Thread.Sleep(5000); // Let it run for 5 seconds
example.Stop();
}
}
Time Measurement in C#
Time is a critical aspect of many applications. C# provides several ways to measure, track, and represent time. Let’s explore the most common approaches:
DateTime
DateTime represents a specific point in time, typically expressed as a date and time of day.
// Get current date and time
DateTime now = DateTime.Now; // Local time
DateTime utcNow = DateTime.UtcNow; // UTC time
// Creating specific dates
DateTime specificDate = new DateTime(2023, 12, 31, 23, 59, 59);
// Parsing from string
DateTime parsedDate = DateTime.Parse("2023-01-01 12:30:45");
// Formatting
string formatted = now.ToString("yyyy-MM-dd HH:mm:ss");
// Date calculations
DateTime tomorrow = now.AddDays(1);
DateTime nextMonth = now.AddMonths(1);
TimeSpan difference = tomorrow - now;
TimeSpan
TimeSpan represents a time interval (duration).
// Create from explicit values
TimeSpan duration = new TimeSpan(2, 30, 45); // 2 hours, 30 minutes, 45 seconds
TimeSpan twoHours = TimeSpan.FromHours(2);
TimeSpan tenMinutes = TimeSpan.FromMinutes(10);
// Create from difference between DateTimes
DateTime start = DateTime.Now;
DateTime end = start.AddHours(3);
TimeSpan elapsed = end - start;
// Properties
double totalHours = elapsed.TotalHours;
int minutes = elapsed.Minutes; // Just the minutes component (0-59)
Stopwatch
Stopwatch provides high-precision timing for measuring code execution performance.
using System.Diagnostics;
// Basic usage
Stopwatch sw = new Stopwatch();
sw.Start();
// Code to measure
DoSomethingExpensive();
sw.Stop();
Console.WriteLine($"Execution took {sw.ElapsedMilliseconds} ms");
Console.WriteLine($"Or more precisely: {sw.Elapsed.TotalSeconds} seconds");
// Reusing stopwatch
sw.Reset(); // Resets back to zero
sw.Restart(); // Combines Reset() and Start()
Environment.TickCount
TickCount provides the number of milliseconds since the system started.
// Get system uptime in milliseconds (wraps around after ~25 days)
int tickCount = Environment.TickCount;
// Get system uptime in milliseconds (Int64 version, doesn't wrap)
long tickCount64 = Environment.TickCount64;
// Calculate elapsed time between operations
int start = Environment.TickCount;
DoSomeWork();
int end = Environment.TickCount;
int elapsed = end - start;
DateTimeOffset
DateTimeOffset represents a point in time with timezone awareness.
// Create with timezone information
DateTimeOffset now = DateTimeOffset.Now;
DateTimeOffset utcNow = DateTimeOffset.UtcNow;
// Create with specific offset
DateTimeOffset custom = new DateTimeOffset(2023, 12, 31, 23, 59, 59, TimeSpan.FromHours(-5));
// Get from DateTime
DateTimeOffset fromDateTime = new DateTimeOffset(DateTime.Now);
// Access timezone information
TimeSpan offset = now.Offset;
ValueStopwatch (High-Performance Option)
For extremely performance-sensitive code, a struct-based stopwatch can reduce allocations:
using System.Runtime.CompilerServices;
// Lightweight stopwatch (struct-based to avoid allocations)
public readonly struct ValueStopwatch
{
private readonly long _startTimestamp;
public static ValueStopwatch StartNew() => new ValueStopwatch(Stopwatch.GetTimestamp());
private ValueStopwatch(long startTimestamp)
{
_startTimestamp = startTimestamp;
}
public TimeSpan GetElapsedTime()
{
long end = Stopwatch.GetTimestamp();
long elapsed = end - _startTimestamp;
return TimeSpan.FromTicks(elapsed * TimeSpan.TicksPerSecond / Stopwatch.Frequency);
}
}
// Usage
var watch = ValueStopwatch.StartNew();
DoSomeWork();
TimeSpan elapsed = watch.GetElapsedTime();
Choosing the Right Tool
- DateTime: For calendar dates and times
- DateTimeOffset: When timezone information matters
- TimeSpan: For time intervals and calculations
- Stopwatch: For precise code performance measurement
- Environment.TickCount: For system uptime or simple elapsed time
Each approach has its strengths and is designed for specific scenarios. For performance-critical applications, Stopwatch is usually the best choice for timing code execution.
Observer Pattern in C#
The Observer pattern is a behavioral design pattern where an object (the subject) maintains a list of dependents (observers) and notifies them automatically of state changes. This pattern is fundamental for implementing distributed event handling systems.
Implementation Using Event Handlers
The most common way to implement the Observer pattern in C# is using events and delegates:
public class StockMarket
{
// Event declaration using built-in EventHandler
public event EventHandler<StockChangeEventArgs> StockPriceChanged;
private decimal currentPrice;
public decimal CurrentPrice
{
get { return currentPrice; }
set
{
if (currentPrice != value)
{
currentPrice = value;
// Notify observers about the price change
OnStockPriceChanged(new StockChangeEventArgs(currentPrice));
}
}
}
// Method to raise the event
protected virtual void OnStockPriceChanged(StockChangeEventArgs e)
{
StockPriceChanged?.Invoke(this, e);
}
}
// Custom event args
public class StockChangeEventArgs : EventArgs
{
public decimal NewPrice { get; }
public StockChangeEventArgs(decimal newPrice)
{
NewPrice = newPrice;
}
}
// Observer
public class StockTrader
{
public string Name { get; }
public StockTrader(string name, StockMarket market)
{
Name = name;
// Subscribe to the event
market.StockPriceChanged += OnStockPriceChanged;
}
private void OnStockPriceChanged(object sender, StockChangeEventArgs e)
{
Console.WriteLine($"{Name} noticed stock price changed to {e.NewPrice:C}");
}
}
// Usage
public static void Main()
{
var market = new StockMarket();
var trader1 = new StockTrader("Alice", market);
var trader2 = new StockTrader("Bob", market);
market.CurrentPrice = 100.00m; // Both traders will be notified
market.CurrentPrice = 105.75m; // Both traders will be notified again
}
This implementation is clean and straightforward, leveraging C#’s built-in event system. The StockMarket
acts as the subject, while StockTrader
instances are observers.
Implementation Using Reactive Extensions (Rx)
For more complex scenarios, Reactive Extensions provide a powerful way to implement the Observer pattern:
using System;
using System.Reactive.Subjects;
public class StockMarketRx
{
// Subject acts as both an observable and an observer
private readonly Subject<decimal> priceSubject = new Subject<decimal>();
// Expose the observable interface to prevent direct calls to OnNext
public IObservable<decimal> PriceChanges => priceSubject;
private decimal currentPrice;
public decimal CurrentPrice
{
get { return currentPrice; }
set
{
if (currentPrice != value)
{
currentPrice = value;
// Publish new price to all subscribers
priceSubject.OnNext(currentPrice);
}
}
}
}
public class StockTraderRx : IObserver<decimal>
{
public string Name { get; }
private IDisposable subscription;
public StockTraderRx(string name, StockMarketRx market)
{
Name = name;
// Subscribe to price changes
subscription = market.PriceChanges.Subscribe(this);
}
public void OnCompleted()
{
Console.WriteLine($"{Name}: Market closed for the day");
}
public void OnError(Exception error)
{
Console.WriteLine($"{Name}: Error occurred: {error.Message}");
}
public void OnNext(decimal price)
{
Console.WriteLine($"{Name} noticed stock price changed to {price:C}");
}
public void Unsubscribe()
{
subscription?.Dispose();
}
}
// Usage
public static void RxExample()
{
var market = new StockMarketRx();
var trader1 = new StockTraderRx("Alice", market);
var trader2 = new StockTraderRx("Bob", market);
market.CurrentPrice = 100.00m;
market.CurrentPrice = 105.75m;
// Unsubscribe one trader
trader1.Unsubscribe();
// Only trader2 gets this update
market.CurrentPrice = 110.50m;
}
The Rx implementation provides additional benefits:
- Built-in support for error handling
- Explicit completion notification
- Easy subscription management with IDisposable
- Ability to transform and filter events using LINQ operators
Which Approach to Choose?
Use C# Events when:
- You need a simple, lightweight implementation
- Your application has straightforward publisher-subscriber relationships
- You want to leverage familiar C# language features
Use Reactive Extensions when:
- You need advanced event processing capabilities (filtering, throttling, etc.)
- Your application deals with complex event streams
- You want better error handling and resource cleanup
- You need to compose multiple event sources together
Both approaches effectively implement the Observer pattern, but Reactive Extensions scales better for complex scenarios while adding a small learning curve and dependency.
Enjoy Reading This Article?
Here are some more articles you might like to read next: