developpement:dotnet:csharp:introduction
Différences
Ci-dessous, les différences entre deux révisions de la page.
Les deux révisions précédentesRévision précédenteProchaine révision | Révision précédente | ||
developpement:dotnet:csharp:introduction [2023/10/04 01:37] – [Reactive] sgariepy | developpement:dotnet:csharp:introduction [2023/10/06 05:06] (Version actuelle) – [Reactive] sgariepy | ||
---|---|---|---|
Ligne 1003: | Ligne 1003: | ||
* Not all static methods are thread safe | * Not all static methods are thread safe | ||
+ | <code csharp> | ||
+ | void Main() | ||
+ | { | ||
+ | var task1 = Task.Factory.StartNew(AddItem); | ||
+ | var task2 = Task.Factory.StartNew(AddItem); | ||
+ | var task3 = Task.Factory.StartNew(AddItem); | ||
+ | var task4 = Task.Factory.StartNew(AddItem); | ||
+ | var task5 = Task.Factory.StartNew(AddItem); | ||
+ | Task.WaitAll(task1, | ||
+ | |||
+ | foreach (var item in items) | ||
+ | { | ||
+ | Console.WriteLine(item.Key + ": " + item.Value); | ||
+ | } | ||
+ | } | ||
+ | |||
+ | private static void AddItem() | ||
+ | { | ||
+ | lock (items) | ||
+ | { | ||
+ | Console.WriteLine(" | ||
+ | items.Add(items.Count, | ||
+ | } | ||
+ | |||
+ | Dictionary< | ||
+ | lock (items) | ||
+ | { | ||
+ | Console.WriteLine(" | ||
+ | dictionary = items; | ||
+ | } | ||
+ | } | ||
+ | |||
+ | private static void AddItemNotSafe() | ||
+ | { | ||
+ | if (items.ContainsKey(1)) { // Ceci devrait être " | ||
+ | // do something | ||
+ | } | ||
+ | else | ||
+ | { | ||
+ | items.Add(1, | ||
+ | } | ||
+ | } | ||
+ | </ | ||
===== Thread Affinity ===== | ===== Thread Affinity ===== | ||
Ligne 1014: | Ligne 1057: | ||
* Cannot call members on a thread that is thread safe from a different thread | * Cannot call members on a thread that is thread safe from a different thread | ||
* Requires request to be marshaled to the thread-safe thread that created the object | * Requires request to be marshaled to the thread-safe thread that created the object | ||
+ | |||
+ | |||
+ | Ceci peut mener à une erreur '' | ||
+ | |||
+ | ===== Signaling ===== | ||
+ | |||
+ | Permet de signaler à d' | ||
+ | |||
+ | EventWaitHandle permet le signalement | ||
+ | |||
+ | * AutoResetEvent | ||
+ | * Utilisé quand un thread a besoin d'un accès exclusif à une ressource | ||
+ | * Une seule ressource peut accéder la ressource à la fois | ||
+ | * Se ferme automatiquement | ||
+ | * Un thread attend pour un signal en appelant '' | ||
+ | * Calling '' | ||
+ | * Si plusieurs threads appellent WaitOne, une file d' | ||
+ | * ManualResetEvent | ||
+ | * CountdownEvent | ||
+ | |||
+ | |||
+ | <code csharp> | ||
+ | static EventWaitHandle eventWaitHandle = new EventWaitHandle(false, | ||
+ | // Equivalent: static EventWaitHandle eventWaitHandle = new AutoResetEvent(false); | ||
+ | |||
+ | void Main() | ||
+ | { | ||
+ | Task.Factory.StartNew(WorkerThread); | ||
+ | Thread.Sleep(2500); | ||
+ | eventWaitHandle.Set(); | ||
+ | } | ||
+ | |||
+ | void WorkerThread() | ||
+ | { | ||
+ | Console.WriteLine(" | ||
+ | eventWaitHandle.WaitOne(); | ||
+ | // Logic | ||
+ | Console.WriteLine(" | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | |||
+ | |||
+ | ===== Two-way signaling ===== | ||
+ | |||
+ | Le two-way signaling utilise deux EventWaitHandle. | ||
+ | |||
+ | |||
+ | <code csharp> | ||
+ | static EventWaitHandle first = new AutoResetEvent(false); | ||
+ | static EventWaitHandle second = new AutoResetEvent(false); | ||
+ | |||
+ | static object customLock = new object(); | ||
+ | static string value = String.Empty; | ||
+ | |||
+ | void Main() | ||
+ | { | ||
+ | Task.Factory.StartNew(WorkerThread); | ||
+ | Console.WriteLine(" | ||
+ | first.WaitOne(); | ||
+ | | ||
+ | lock (customLock) | ||
+ | { | ||
+ | value = " | ||
+ | Console.WriteLine(value); | ||
+ | } | ||
+ | | ||
+ | Thread.Sleep(1000); | ||
+ | second.Set(); | ||
+ | Console.WriteLine(" | ||
+ | } | ||
+ | |||
+ | void WorkerThread() | ||
+ | { | ||
+ | Thread.Sleep(1000); | ||
+ | | ||
+ | lock (customLock) | ||
+ | { | ||
+ | value = " | ||
+ | Console.WriteLine(value); | ||
+ | } | ||
+ | | ||
+ | first.Set(); | ||
+ | Console.WriteLine(" | ||
+ | | ||
+ | Console.WriteLine(" | ||
+ | second.WaitOne(); | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | |||
+ | ===== Manual Event Reset ===== | ||
+ | |||
+ | |||
+ | <code csharp> | ||
+ | static ManualResetEvent customEvent = new ManualResetEvent(false); | ||
+ | // Equivalent: static EventWaitHandle eventWaitHandle = new EventWaitHandle(false, | ||
+ | |||
+ | void Main() | ||
+ | { | ||
+ | Task.Factory.StartNew(CallWaitOne); | ||
+ | Task.Factory.StartNew(CallWaitOne); | ||
+ | Task.Factory.StartNew(CallWaitOne); | ||
+ | Task.Factory.StartNew(CallWaitOne); | ||
+ | Task.Factory.StartNew(CallWaitOne); | ||
+ | | ||
+ | Thread.Sleep(1000); | ||
+ | Console.WriteLine(" | ||
+ | Console.Read(); | ||
+ | | ||
+ | customEvent.Set(); | ||
+ | | ||
+ | Thread.Sleep(1000); | ||
+ | Console.WriteLine(" | ||
+ | Console.Read(); | ||
+ | Task.Factory.StartNew(CallWaitOne); | ||
+ | Task.Factory.StartNew(CallWaitOne); | ||
+ | Task.Factory.StartNew(CallWaitOne); | ||
+ | Task.Factory.StartNew(CallWaitOne); | ||
+ | Task.Factory.StartNew(CallWaitOne); | ||
+ | Thread.Sleep(1000); | ||
+ | |||
+ | Console.WriteLine(" | ||
+ | Console.Read(); | ||
+ | customEvent.Reset(); | ||
+ | Task.Factory.StartNew(CallWaitOne); | ||
+ | Task.Factory.StartNew(CallWaitOne); | ||
+ | Task.Factory.StartNew(CallWaitOne); | ||
+ | Task.Factory.StartNew(CallWaitOne); | ||
+ | Task.Factory.StartNew(CallWaitOne); | ||
+ | Thread.Sleep(1000); | ||
+ | |||
+ | Console.WriteLine(" | ||
+ | Console.Read(); | ||
+ | customEvent.Set(); | ||
+ | | ||
+ | Console.ReadLine(); | ||
+ | } | ||
+ | |||
+ | void CallWaitOne() | ||
+ | { | ||
+ | Console.WriteLine(" | ||
+ | customEvent.WaitOne(); | ||
+ | Console.WriteLine(" | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | |||
+ | ===== Countdown ===== | ||
+ | |||
+ | <code csharp> | ||
+ | static CountdownEvent customCountdown = new CountdownEvent(5); | ||
+ | |||
+ | void Main() | ||
+ | { | ||
+ | Task.Factory.StartNew(DoSomething); | ||
+ | Task.Factory.StartNew(DoSomething); | ||
+ | Task.Factory.StartNew(DoSomething); | ||
+ | Task.Factory.StartNew(DoSomething); | ||
+ | Task.Factory.StartNew(DoSomething); | ||
+ | | ||
+ | customCountdown.Wait(); | ||
+ | Console.WriteLine(" | ||
+ | } | ||
+ | |||
+ | void DoSomething() | ||
+ | { | ||
+ | Thread.Sleep(250); | ||
+ | Console.WriteLine(" | ||
+ | customCountdown.Signal(); | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | ====== Task Parallel Library (TPL) ====== | ||
+ | |||
+ | |||
+ | * Set of public types and APIs that can be found in two namespaces: | ||
+ | * System.Threading | ||
+ | * System.Threading.Tasks | ||
+ | * Simplifies process of adding parallelism and concurrency to applications | ||
+ | * Value is ability to scale degree of concurrency dynamically | ||
+ | * Handles partitioning of work | ||
+ | * Schedules threads on ThreadPool | ||
+ | * Allows for task cancellation | ||
+ | * Handles state management | ||
+ | * Not all code is suitable for parallelization | ||
+ | * Threading of any type has an associated overhead | ||
+ | * In some cases, multithreading may be slower than sequential code | ||
+ | |||
+ | Exemple simple: | ||
+ | |||
+ | <code csharp> | ||
+ | void Main() | ||
+ | { | ||
+ | Stopwatch stopwatch = new Stopwatch(); | ||
+ | stopwatch.Start(); | ||
+ | for (int i = 0; i < 10; i++) | ||
+ | { | ||
+ | Console.WriteLine(i); | ||
+ | } | ||
+ | stopwatch.Stop(); | ||
+ | Console.WriteLine(" | ||
+ | stopwatch.Start(); | ||
+ | | ||
+ | Parallel.For(0, | ||
+ | Console.WriteLine(i); | ||
+ | }); | ||
+ | |||
+ | stopwatch.Stop(); | ||
+ | Console.WriteLine(" | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | Le cas en parallèle prend plus de temps dû au overhead. | ||
+ | |||
+ | |||
+ | <code csharp> | ||
+ | // Générer des images: https:// | ||
+ | |||
+ | void Main() | ||
+ | { | ||
+ | var path = Directory.GetCurrentDirectory(); | ||
+ | var files = Directory.GetFiles(path + @" | ||
+ | | ||
+ | var normalAlteredPath = path + @" | ||
+ | var parallelAlteredPath = path + @" | ||
+ | Directory.CreateDirectory(normalAlteredPath); | ||
+ | Directory.CreateDirectory(parallelAlteredPath); | ||
+ | | ||
+ | ParallelExecutionMode(files, | ||
+ | NormalExecutionMode(files, | ||
+ | } | ||
+ | |||
+ | void NormalExecutionMode(string[] files, string alteredPath) | ||
+ | { | ||
+ | Stopwatch stopwatch = Stopwatch.StartNew(); | ||
+ | foreach (var currentFile in files) | ||
+ | { | ||
+ | var file = Path.GetFileName(currentFile); | ||
+ | using (var fileBitmap = new Bitmap(currentFile)) | ||
+ | { | ||
+ | fileBitmap.RotateFlip(RotateFlipType.Rotate270FlipX); | ||
+ | fileBitmap.Save(Path.Combine(alteredPath, | ||
+ | Console.WriteLine(" | ||
+ | } | ||
+ | } | ||
+ | Console.WriteLine(" | ||
+ | stopwatch.Stop(); | ||
+ | } | ||
+ | |||
+ | void ParallelExecutionMode(string[] files, string alteredPath) | ||
+ | { | ||
+ | Stopwatch stopwatch = Stopwatch.StartNew(); | ||
+ | |||
+ | Parallel.ForEach(files, | ||
+ | var file = Path.GetFileName(currentFile); | ||
+ | using (var fileBitmap = new Bitmap(currentFile)) | ||
+ | { | ||
+ | fileBitmap.RotateFlip(RotateFlipType.Rotate270FlipX); | ||
+ | fileBitmap.Save(Path.Combine(alteredPath, | ||
+ | Console.WriteLine(" | ||
+ | } | ||
+ | }); | ||
+ | |||
+ | Console.WriteLine(" | ||
+ | stopwatch.Stop(); | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | |||
+ | |||
+ | <code csharp> | ||
+ | void Main() | ||
+ | { | ||
+ | var list = Enumerable.Range(0, | ||
+ | CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(); | ||
+ | | ||
+ | ParallelOptions parallelOptions = new ParallelOptions(); | ||
+ | parallelOptions.CancellationToken = cancellationTokenSource.Token; | ||
+ | parallelOptions.MaxDegreeOfParallelism = System.Environment.ProcessorCount; | ||
+ | | ||
+ | Console.WriteLine(" | ||
+ | | ||
+ | Task.Factory.StartNew(() => | ||
+ | { | ||
+ | if (Console.ReadLine() == " | ||
+ | { | ||
+ | cancellationTokenSource.Cancel(); | ||
+ | } | ||
+ | | ||
+ | long total = 0; | ||
+ | | ||
+ | try | ||
+ | { | ||
+ | Parallel.For< | ||
+ | { | ||
+ | Thread.Sleep(200); | ||
+ | parallelOptions.CancellationToken.ThrowIfCancellationRequested(); | ||
+ | subtotal += list[count]; | ||
+ | return subtotal; | ||
+ | }, | ||
+ | (x) => | ||
+ | { | ||
+ | Interlocked.Add(ref total, x); | ||
+ | }); | ||
+ | } | ||
+ | catch (OperationCanceledException ex) | ||
+ | { | ||
+ | Console.WriteLine(" | ||
+ | } | ||
+ | finally | ||
+ | { | ||
+ | cancellationTokenSource.Dispose(); | ||
+ | } | ||
+ | Console.WriteLine(" | ||
+ | }); | ||
+ | | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | ===== Continuation with state ===== | ||
+ | |||
+ | <code csharp> | ||
+ | void Main() | ||
+ | { | ||
+ | Task< | ||
+ | List< | ||
+ | | ||
+ | for (int i = 0; i < 3; i++) | ||
+ | { | ||
+ | task = task.ContinueWith((x, | ||
+ | continuationTasks.Add(task); | ||
+ | } | ||
+ | | ||
+ | task.Wait(); | ||
+ | | ||
+ | foreach (var continuation in continuationTasks) | ||
+ | { | ||
+ | Person person = continuation.AsyncState as Person; | ||
+ | Console.WriteLine(" | ||
+ | } | ||
+ | } | ||
+ | |||
+ | static DateTime DoSomething() | ||
+ | { | ||
+ | return DateTime.Now; | ||
+ | } | ||
+ | |||
+ | internal class Person | ||
+ | { | ||
+ | public int Id { get; set; } | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | ===== TaskCompletionSource ===== | ||
+ | |||
+ | <code csharp> | ||
+ | void Main() | ||
+ | { | ||
+ | TaskCompletionSource< | ||
+ | Task< | ||
+ | | ||
+ | Task.Factory.StartNew(() => { | ||
+ | Thread.Sleep(2000); | ||
+ | taskCompletionSource.SetResult(new Product { Id = 1, Name = "Some name" }); | ||
+ | }); | ||
+ | | ||
+ | Task.Factory.StartNew(() => | ||
+ | { | ||
+ | if (Console.ReadLine() == " | ||
+ | { | ||
+ | Product result = lazyTask.Result; | ||
+ | Console.WriteLine(" | ||
+ | } | ||
+ | }); | ||
+ | | ||
+ | Thread.Sleep(5000); | ||
+ | } | ||
+ | |||
+ | class Product | ||
+ | { | ||
+ | public int Id { get; set; } | ||
+ | public string Name { get; set; } | ||
+ | } | ||
+ | |||
+ | </ | ||
+ | |||
+ | ===== PLINQ ===== | ||
+ | |||
+ | Parallel LINQ: | ||
+ | |||
+ | * Automates parallelization | ||
+ | * Considéré déclaratif plutôt qu' | ||
+ | * Opérateurs qui font en sorte que ce n'est pas parallélisé: | ||
+ | * Take, Select, SelectMany, Skip, TakeWhile, SkipWhile, ElementAt | ||
+ | * Anomalies | ||
+ | * Join, GroupBy, GroupJoin, Distinct, Union, Intersect, Except | ||
+ | * Force parallelism: | ||
+ | * .AsParallel().withExecutionMode(ParallelExecution.ForceParallelism) | ||
+ | |||
+ | |||
+ | <code csharp> | ||
+ | void Main() | ||
+ | { | ||
+ | var list = Enumerable.Range(1, | ||
+ | var primeNumbers = list | ||
+ | .AsParallel() | ||
+ | .Where(IsPrime); | ||
+ | Console.WriteLine(" | ||
+ | } | ||
+ | |||
+ | bool IsPrime(int x) | ||
+ | { | ||
+ | if (x == 1) return false; | ||
+ | if (x == 2) return true; | ||
+ | if (x % 2 == 0) return false; | ||
+ | var boundary = (int)Math.Floor(Math.Sqrt(x)); | ||
+ | |||
+ | for (int i = 3; i <= boundary; i += 2) | ||
+ | { | ||
+ | if (x % i == 0) | ||
+ | { | ||
+ | return false; | ||
+ | } | ||
+ | } | ||
+ | return true; | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | ==== Degree of Parallelism ==== | ||
+ | |||
+ | <code csharp> | ||
+ | void Main() | ||
+ | { | ||
+ | List< | ||
+ | websites.Add(" | ||
+ | websites.Add(" | ||
+ | websites.Add(" | ||
+ | | ||
+ | List< | ||
+ | .AsParallel() | ||
+ | .WithDegreeOfParallelism(websites.Count()) | ||
+ | .Select(PingSites) | ||
+ | .ToList(); | ||
+ | | ||
+ | foreach (var response in responses) | ||
+ | { | ||
+ | Console.WriteLine(response.Address + " " + response.Status + " " + response.RoundtripTime); | ||
+ | } | ||
+ | | ||
+ | Console.ReadLine(); | ||
+ | } | ||
+ | |||
+ | private static PingReply PingSites(string websiteName) | ||
+ | { | ||
+ | Ping ping = new Ping(); | ||
+ | return ping.Send(websiteName); | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | |||
+ | |||
+ | |||
+ | ====== Thread Marshalling ====== | ||
+ | |||
+ | * [[http:// | ||
+ | * [[http:// | ||
+ | |||
+ | |||
+ | ====== Pattern Matching ====== | ||
+ | |||
+ | <code csharp> | ||
+ | void Main() | ||
+ | { | ||
+ | var circle = new Circle(5); | ||
+ | var circleRadius100 = new Circle(250); | ||
+ | var rectangle = new Rectangle(420, | ||
+ | var square = new Rectangle(70, | ||
+ | |||
+ | var shapes = new List< | ||
+ | |||
+ | var randomShape = shapes[new Random().Next(shapes.Count)]; | ||
+ | |||
+ | CSharp6Feature(randomShape); | ||
+ | CSharp7Feature(randomShape); | ||
+ | CSharp8Feature(randomShape); | ||
+ | CSharp9Feature(randomShape); | ||
+ | } | ||
+ | |||
+ | private void CSharp6Feature(Shape shape) | ||
+ | { | ||
+ | Console.WriteLine(" | ||
+ | if (shape is Circle) // ' | ||
+ | { | ||
+ | var circle = (Circle)shape; | ||
+ | Console.WriteLine($" | ||
+ | } | ||
+ | else | ||
+ | { | ||
+ | Console.WriteLine($" | ||
+ | } | ||
+ | } | ||
+ | |||
+ | private void CSharp7Feature(Shape shape) | ||
+ | { | ||
+ | Console.WriteLine(" | ||
+ | |||
+ | if (shape is Circle circle) | ||
+ | { | ||
+ | Console.WriteLine($" | ||
+ | } | ||
+ | else | ||
+ | { | ||
+ | Console.WriteLine($" | ||
+ | } | ||
+ | | ||
+ | // using switch | ||
+ | | ||
+ | switch (shape) | ||
+ | { | ||
+ | case Circle c: | ||
+ | Console.WriteLine($" | ||
+ | break; | ||
+ | case Rectangle r when r.Height == r.Width: | ||
+ | Console.WriteLine($" | ||
+ | break; | ||
+ | default: | ||
+ | Console.WriteLine($" | ||
+ | break; | ||
+ | } | ||
+ | } | ||
+ | |||
+ | private void CSharp8Feature(Shape shape) | ||
+ | { | ||
+ | Console.WriteLine(" | ||
+ | |||
+ | if (shape is Circle { Radius: 10 }) | ||
+ | { | ||
+ | Console.WriteLine($" | ||
+ | } | ||
+ | | ||
+ | var shapeDetails = shape switch | ||
+ | { | ||
+ | Circle => "This is a circle", | ||
+ | Rectangle rec when rec.Height == rec.Width => "This is a square", | ||
+ | _ => "Shape is something else" | ||
+ | }; | ||
+ | } | ||
+ | |||
+ | private void CSharp9Feature(Shape shape) | ||
+ | { | ||
+ | Console.WriteLine(" | ||
+ | |||
+ | if (shape is not Rectangle) // ' | ||
+ | { | ||
+ | Console.WriteLine($" | ||
+ | } | ||
+ | |||
+ | if (shape is Circle { Radius: > 100 and < 200, Area: >= 1000 }) | ||
+ | { | ||
+ | Console.WriteLine($" | ||
+ | } | ||
+ | |||
+ | |||
+ | // that can be used like so: if (shape is not null) {...} | ||
+ | |||
+ | var shapeDetails = shape switch | ||
+ | { | ||
+ | Circle => "This is a circle", | ||
+ | Rectangle rec when rec.Height == rec.Width => "This is a square", | ||
+ | { Area: 100 } => "Area is 100", | ||
+ | _ => "Shape is something else" | ||
+ | }; | ||
+ | |||
+ | var areaDetails = shape.Area switch | ||
+ | { | ||
+ | >= 100 and <= 200 => "Area is between 100 and 200", | ||
+ | _ => "" | ||
+ | }; | ||
+ | | ||
+ | } | ||
+ | |||
+ | public static class Extensions | ||
+ | { | ||
+ | public static bool IsLetter(this char c) => | ||
+ | c is >= ' | ||
+ | } | ||
+ | |||
+ | public abstract class Shape | ||
+ | { | ||
+ | public abstract double Area { get; } | ||
+ | } | ||
+ | |||
+ | public class Rectangle : Shape, ISquare | ||
+ | { | ||
+ | public Rectangle(int height, int width) | ||
+ | { | ||
+ | Height = height; | ||
+ | Width = width; | ||
+ | } | ||
+ | | ||
+ | public override double Area => Height * Width; | ||
+ | | ||
+ | public int Height { get; set; } | ||
+ | public int Width { get; set; } | ||
+ | } | ||
+ | |||
+ | public class Circle : Shape | ||
+ | { | ||
+ | private const double PI = Math.PI; | ||
+ | | ||
+ | | ||
+ | public Circle(int diameter) | ||
+ | { | ||
+ | Diameter = diameter; | ||
+ | } | ||
+ | |||
+ | public int Diameter { get; set; } | ||
+ | public int Radius => Diameter / 2; | ||
+ | |||
+ | public override double Area => PI * Radius * Radius; | ||
+ | } | ||
+ | |||
+ | public interface ISquare | ||
+ | { | ||
+ | int Height { get; set; } | ||
+ | int Width { get; set; } | ||
+ | } | ||
+ | |||
+ | static decimal GetGroupTicketPriceDiscount(int groupSize, DateTime visitDate) | ||
+ | => (groupSize, visitDate.DayOfWeek) switch | ||
+ | { | ||
+ | (<= 0, _) => throw new ArgumentException(" | ||
+ | (_, DayOfWeek.Saturday or DayOfWeek.Sunday) => 0.0m, | ||
+ | (>= 5 and < 10, DayOfWeek.Monday) => 20.0m, | ||
+ | (>= 10, DayOfWeek.Monday) => 30.0m, | ||
+ | (>= 5 and < 10, _) => 12.0m, | ||
+ | (>= 10, _) => 15.0m, | ||
+ | _ => 0.0m | ||
+ | }; | ||
+ | |||
+ | </ | ||
+ | |||
+ | |||
Ligne 1055: | Ligne 1742: | ||
- | ====== Thread Marshalling ====== | ||
- | * [[http:// | ||
- | * [[http:// | ||
developpement/dotnet/csharp/introduction.1696376276.txt.gz · Dernière modification : 2023/10/04 01:37 de sgariepy