Bài 10: Interop

.NET đã được thiết kế rõ ràng để tương tác với các thư viện native với chi phí thấp. Các chương trình và thư viện .NET có thể gọi các API hệ điều hành cấp thấp một cách liền mạch hoặc khai thác hệ sinh thái rộng lớn của các thư viện C/C++. Thời gian chạy .NET hiện đại tập trung vào việc cung cấp các khối xây dựng tương tác cấp thấp, chẳng hạn như khả năng gọi các native method thông qua các con trỏ hàm, xuất các phương thức .NET dưới dạng unmanaged callbacks hoặc customized interface casting. .NET cũng liên tục phát triển trong lĩnh vực này và trong .NET 7 đã phát hành các giải pháp tạo mã nguồn giúp giảm thêm chi phí hoạt động và thân thiện với AOT.

Đoạn mã sau minh họa hiệu quả của các con trỏ hàm C#.

// Using a function pointer avoids a delegate allocation.
// Equivalent to `void (*fptr)(int) = &Callback;` in C
delegate* unmanaged<int, void> fptr = &Callback;
RegisterCallback(fptr);

[UnmanagedCallersOnly]
static void Callback(int a) => Console.WriteLine($"Callback:  {a}");

[LibraryImport("...", EntryPoint = "RegisterCallback")]
static partial void RegisterCallback(delegate* unmanaged<int, void> fptr);

Ví dụ này sử dụng trình tạo mã nguồn LibraryImport được giới thiệu trong .NET 7. Nó dựa trên DllImport hoặc P/Invoke hiện có.

Continue reading “Bài 10: Interop”

Bài 9: Khả năng sinh code

Mã bytecode .NET không phải là định dạng có thể thực thi trực tiếp bởi máy tính, mà nó cần phải được xử lý bằng một số dạng trình tạo code. Điều này có thể đạt được bằng cách biên dịch sẵn(AOT), biên dịch, phiên dịch hoặc biên dịch ngay lúc chạy (JIT). Trên thực tế, hiện nay tất cả cách cách này đều được sử dụng trong các tình huống khác nhau.

.NET được biết đến nhiều nhất với trình biên dịch JIT. Các JIT biên dịch các phương thức (và các thành phần khác) thành native code trong khi ứng dụng đang chạy và chỉ khi chúng cần thiết, do đó có tên “just in time” (đúng lúc). Ví dụ: một chương trình có thể chỉ gọi một trong số các phương thức trên một kiểu khi chạy. Một JIT cũng có thể tận dụng thông tin chỉ có sẵn trong thời gian chạy, như giá trị của các biến tĩnh chỉ đọc đã được khởi tạo hoặc mô hình CPU chính xác mà chương trình đang chạy và có thể biên dịch cùng một phương thức nhiều lần để tối ưu hóa mỗi lần cho các mục đích khác nhau với khả năng tối ưu code dựa trên các bài học từ các lần biên dịch trước đó.

Continue reading “Bài 9: Khả năng sinh code”

Bài 8: Định dạng được biên dịch của mã nhị phân

Các ứng dụng và thư viện được biên dịch thành mã bytecode đa nền tảng được tiêu chuẩn hóa ở định dạng PE/COFF. Bản phân phối ở dạng nhị phân là một tính năng được thiết cho hiệu suất. Nó cho phép các ứng dụng mở rộng quy mô cho số lượng dự án ngày càng lớn hơn. Mỗi thư viện bao gồm một tập dữ liệu gồm danh sách các kiểu được import và export, còn được gọi là metadata, đóng vai trò quan trọng cho cả hoạt động phát triển và chạy ứng dụng.

Các bản dữ liệu nhị phân được biên dịch bao gồm hai phần chính:

  • Mã bytecode — định dạng ngắn gọn và thông thường cho phép bỏ qua nhu cầu phân tích cú pháp từ mã nguồn dạng văn bản sau khi biên dịch bởi một trình biên dịch ngôn ngữ cấp cao (như C#).
  • Metadata — mô tả các kiểu được import và export, bao gồm vị trí của mã bytecode cho một phương thức nhất định.
Continue reading “Bài 8: Định dạng được biên dịch của mã nhị phân”

Bài 7: Reflection

Reflection là một mô hình “chương trình như dữ liệu”, cho phép một phần của chương trình truy vấn và gọi các phần khác một cách “động”, như là các assembly, kiểu dữ liệu hoặc các thành phần của kiểu. Nó đặc biệt hữu dụng với các mô hình lập trình late-bound(1) và các công cụ.

Đoạn code sau sử dụng reflection để tìm và gọi các kiểu:

foreach (Type type in typeof(Program).Assembly.DefinedTypes)
{
    if (type.IsAssignableTo(typeof(IStory)) &&
        !type.IsInterface)
    {
        IStory? story = (IStory?)Activator.CreateInstance(type);
        if (story is not null)
        {
            var text = story.TellMeAStory();
            Console.WriteLine(text);
        }
    }
}

interface IStory
{
    string TellMeAStory();
}

class BedTimeStore : IStory
{
    public string TellMeAStory() => "Once upon a time, there was an orphan learning magic ...";
}

class HorrorStory : IStory
{
    public string TellMeAStory() => "On a dark and stormy night, I heard a strange voice in the cellar ...";
}
Continue reading “Bài 7: Reflection”

Bài 6: Hỗ trợ xử lý đồng thời (Concurrency)

Hỗ trợ để xử lý nhiều việc cùng lúc là nền tảng cho mọi khối lượng công việc trên thực tế, cho dù đó là ứng dụng client đang xử lý nền trong khi vẫn giữ cho giao diện người dùng phản hồi nhanh, các dịch vụ xử lý hàng ngàn yêu cầu đồng thời, các thiết bị phản hồi vô số tác nhân kích hoạt đồng thời hoặc các máy tính cao cấp hỗ trợ song song việc xử lý các hoạt động sử dụng nhiều sức mạnh tính toán. Các hệ điều hành cung cấp hỗ trợ cho sự đồng thời như vậy thông qua các luồng (thread), cho phép nhiều dòng mã lệnh được xử lý độc lập, với hệ điều hành quản lý việc thực thi các luồng đó trên bất kỳ lõi bộ xử lý có sẵn nào trong máy. Các hệ điều hành cũng cung cấp hỗ trợ để thực hiện I/O, với các cơ chế được cung cấp để cho phép I/O được thực hiện theo cách có thể mở rộng với nhiều thao tác I/O “đang hoạt động” tại bất kỳ thời điểm cụ thể nào. Sau đó, các ngôn ngữ lập trình và các framework có thể cung cấp nhiều mức độ trừu tượng khác nhau trên nền tảng hỗ trợ cốt lõi này.

.NET cung cấp hỗ trợ đồng thời và song song hóa như vậy ở nhiều cấp độ trừu tượng, cả thông qua các thư viện và được tích hợp sâu vào C#. Một class Thread nằm ở cuối hệ thống phân cấp và đại diện cho một thread của hệ điều hành, cho phép các nhà phát triển tạo các thread mới và sau đó tham gia với chúng. ThreadPool nằm phía trên đầu các thread, cho phép các nhà phát triển suy nghĩ về các hạng mục công việc được lên lịch không đồng bộ để chạy trên một nhóm thread, với việc quản lý các thread đó (bao gồm cả việc thêm và xóa các thread khỏi nhóm cũng như chỉ định các đầu mục công việc cho các chủ đề đó) còn lại trong thời gian chạy. Task sau đó cung cấp một cách biểu diễn thống nhất cho bất kỳ hoạt động nào được thực hiện không đồng bộ và có thể được tạo và kết hợp theo nhiều cách; ví dụ: Task.Run cho phép lập lịch trình một nhiệm vụ chạy trên ThreadPool và trả về một Task để thể hiện kết quả cuối cùng của công việc đó, trong khi đó Socket.ReceiveAsync trả về một Task (hoặc ValueTask) thể hiện kết quả cuối cùng hoàn thành I/O không đồng bộ để đọc dữ liệu đang chờ xử lý hoặc trong tương lai từ Socket. Một loạt các synchronization primitive được cung cấp để điều phối các hoạt động đồng bộ và không đồng bộ giữa các thread và hoạt động không đồng bộ và vô số API cấp cao hơn được cung cấp để dễ dàng triển khai các mẫu xử lý đồng thời phổ biến, ví dụ: Parallel.ForEach và Parallel.ForEachAsync giúp dễ dàng xử lý song song tất cả các thành phần của một chuỗi dữ liệu.

Hỗ trợ lập trình không đồng bộ cũng là một tính năng first-class của ngôn ngữ lập trình C#, cung cấp các từ khóa async và await giúp dễ dàng viết và soạn thảo các hoạt động không đồng bộ trong khi vẫn tận hưởng đầy đủ lợi ích của tất cả cấu trúc luồng điều khiển mà ngôn ngữ này cung cấp .

Bài 5: Xử lý lỗi trong .NET

Exception (ngoại lệ) là mô hình xử lý lỗi chính trong .NET. Các exception có ưu điểm là thông tin lỗi không cần phải được khai báo trong method signature hoặc phải được trả về trong các phương thức.

Đoạn code sau mô tả một cách sử dụng tiêu biểu:

try
{
    var lines = await File.ReadAllLinesAsync(file);
    Console.WriteLine($"The {file} has {lines.Length} lines.");
}
catch (Exception e) when (e is FileNotFoundException or DirectoryNotFoundException)
{
    Console.WriteLine($"{file} doesn't exist.");
}

Xử lý các exception một cách đúng đắn là điều cần thiết cho độ tin cậy của ứng dụng. Các exception dự kiến ​​có thể được xử lý có chủ ý trong code của người dùng, ngược tại, ứng dụng sẽ bị crash. Một ứng dụng bị crash vẫn đáng tin cậy hơn một ứng dụng có hành vi không xác định. Nó cũng dễ chẩn đoán hơn khi bạn muốn tìm ra nguyên nhân cốt lõi của vấn đề.

Continue reading “Bài 5: Xử lý lỗi trong .NET”

Bài 4: Tính an toàn của .NET

An toàn là một trong những chủ đề quan trọng nhất trong thập kỷ vừa qua. Nó cũng là một thành phần cốt yếu đối với một môi trường được quản lý như .NET.

Các dạng an toàn:

  • An toàn kiểu (type safety) — Một kiểu không thể được dùng ở chỗ của một kiểu khác, nhằm tránh các hành vi không mong muốn.
  • An toàn bộ nhớ (memory safety)— Chỉ bộ nhớ được cấp phát mới được sử dụng, ví dụ: một biến chỉ được tham chiếu đến một đối tượng hợp lệ hoặc là null.
  • Concurrency or thread safety — Dữ liệu được chia sẻ không được truy cập theo cách có thể dẫn đến hành vi không xác định.

Ghi chú: Chính quyền liên bang Mỹ gần đây đã phát hành một hướng dẫn về Sự quan trọng của anh toàn bộ nhớ.

.NET được thiết kế như một nền tảng an toàn ngay từ đầu. Cụ thể, nó nhằm mục đích tạo ra một thế hệ máy chủ web mới vốn cần chấp nhận đầu vào không đáng tin cậy trong môi trường máy tính thù địch (dịch từ chữ hostile) nhất thế giới (Internet). Hiện tại, người ta thường đồng ý rằng các chương trình web nên được viết bằng một ngôn ngữ an toàn.

Continue reading “Bài 4: Tính an toàn của .NET”

Bài 3: Quản lý bộ nhớ tự động

.NET runtime (trình thực thi các ứng dụng .NET) cung cấp khả năng quản lý bộ nhớ tự động thông qua bộ dọn rác (garbage collector – GC). Với bất kỳ ngôn ngữ nào, mô hình quản lý bộ nhớ luôn là một trong những đặc tính quan trọng nhất. Điều này cũng đúng với .NET.

Các lỗi liên quan đến bộ nhớ heap (bộ nhớ nơi chúng ta xin cấp phát và giải phóng – https://daohainam.com/2021/08/14/bo-nho-heap-la-gi/) thường rất khó để debug. Không có gì lạ khi thấy các kỹ sư phải mất hàng tuần, thậm chí hàng tháng trời để có thể dò ra chúng. Nhiều ngôn ngữ dùng một bộ dọn rác (GC) như một cách thân thiện để loại bỏ các bug đó vì GC sẽ đảm bảo quản lý vòng đời cái đối tượng một cách chính xác. Thường thì GC sẽ giải phóng bộ nhớ hàng loạt để hiệu quả hơn. Việc tạm dừng để dọn rác có thể sẽ không phù hợp với các chương trình có yêu cầu rất cao về độ trễ, và bản thân việc sử dụng bộ nhớ có thể sẽ cao hơn. GC có memory locality (1) tốt hơn và một số có khả năng dồn các vùng nhớ giúp nó ít bị phân mảnh (2) hơn.

.NET có một bộ GC có khả năng tự điều chỉnh, hoạt động theo kiểu tracing (3). Nó nhằm mục đích mang lại khả năng vận hành “không cần thao tác” trong những trường hợp thông thường, đồng thời cung cấp các tùy chọn cấu hình với trường hợp khối lượng công việc lớn. GC là kết quả của nhiều năm đầu tư, cải tiến và học hỏi từ nhiều loại khối lượng công việc.

Sơ đồ bộ nhớ .NET
Continue reading “Bài 3: Quản lý bộ nhớ tự động”

GIẢI THÍCH CÁC KHÁI NIỆM TRONG OOP – TÍNH ĐA HÌNH – phần 2

Trước khi đọc bài này, xin hãy ngẩng đầu lên trời đọc câu sau 3 lần: “Khi tôi học gì thì phải hiểu đến tận gốc rễ!

Trong phần 2, ta sẽ tìm hiểu virtual❗️, là chìa khóa cho sức mạnh của đa hình, chúng ta cũng sẽ phải đọc một chút ngôn ngữ Assembly. Đây là cách học hại não🏴‍☠️, nhưng nó đáng giá từng phút bạn học, và đảm bảo nếu chịu khó bạn sẽ hiểu thêm được rất nhiều, vậy nên hãy cố lên nhé.

Đọc để biết thêm về Ngôn ngữ Assembly: https://daohainam.com/2023/03/07/the-nao-la-ngon-ngu-lap-trinh-bac-thap-bac-cao/

Trước tiên xin giới thiệu với bạn một công cụ cho phép ta dịch các chương trình sang assembly (hợp ngữ), ta sẽ dùng công cụ này để khảo sát những gì trình biên dịch tạo ra từ chương trình animal.

Bạn truy cập Godbolt tại https://godbolt.org/z/KiMvdD.

Trở lại với chương trình animal, với 2 lớp Dog và Fish. Ta sẽ dịch nó sang mã assembly và xem những gì thực sự xảy ra.

Continue reading “GIẢI THÍCH CÁC KHÁI NIỆM TRONG OOP – TÍNH ĐA HÌNH – phần 2”

Bài 2: Hệ thống kiểu trong .NET

.NET cung cấp một hệ thống kiểu rất rộng, phục vụ gần như ngang nhau cho safety (tính an toàn), descriptiveness (khả năng tự mô tả), dynamism (kiểu động), and native interop (tương tác với các thành phần native).

Trước hết, hệ thống kiểu cho phép một mô hình lập trình hướng đối tượng. Nó bao gồm các kiểu, kế thừa (đơn thừa kế), interface (bao gồm default method implementation (phương thức mặc định)) và virtual method để cung cấp một hành vi phù hợp cho tất cả các phân lớp kiểu mà hướng đối tượng cho phép.

Generics là một tính năng phổ biến cho phép các lớp chuyên biệt hóa thành một hoặc nhiều kiểu. Ví dụ: List<T> là một lớp chung mở, nhờ đó ta có thể viết List<string> và List<int> ta không cần phải tạo thêm các lớp ListOfString và ListOfInt riêng biệt, hoặc phải dựa vào object và truyền như trường hợp của ArrayList. Generics cũng cho phép tạo nhiều hệ thống hữu ích trên các kiểu khác nhau (và giảm nhu cầu sử dụng nhiều code), như với Generic Math.

Continue reading “Bài 2: Hệ thống kiểu trong .NET”