Bài 12: Tổng kết

Chúng tôi có một số phiên bản trong kỷ nguyên .NET hiện đại, gần đây đã phát hành .NET 7. Chúng tôi nghĩ rằng sẽ hữu ích nếu chúng tôi tóm tắt những gì đang cố gắng xây dựng — ở mức thấp nhất của nền tảng — kể từ .NET Core 1.0 . Mặc dù rõ ràng chúng tôi đã giữ nguyên tinh thần của .NET gốc, nhưng kết quả là một nền tảng mới tạo ra một con đường mới và mang lại nhiều giá trị mới hơn đáng kể cho các nhà phát triển.

Hãy kết thúc nơi chúng ta bắt đầu. .NET là đại diện cho bốn giá trị: Năng suất, Hiệu suất, Bảo mật và Độ tin cậy. Chúng tôi rất tin tưởng rằng các nhà phát triển sẽ được phục vụ tốt nhất khi các nền tảng ngôn ngữ khác nhau cung cấp các cách tiếp cận khác nhau. Với tư cách là một nhóm, chúng tôi tìm cách mang lại năng suất cao nhất cho các nhà phát triển .NET đồng thời cung cấp một nền tảng dẫn đầu về hiệu suất, bảo mật và độ tin cậy.

Chúng tôi dự định thêm nhiều bài viết trong loạt bài này. Những chủ đề nào bạn muốn xem giải quyết đầu tiên? Hãy cho chúng tôi biết trong các ý kiến. Bạn có muốn xem thêm nội dung về “bức tranh lớn” này không?

Nếu muốn biết thêm nội dung này, bạn có thể xem phần Giới thiệu về Common Language Runtime (CLR).

Bài đăng này được viết bởi Jan Kotas, Rich Lander, Maoni Stephens và Stephen Toub, với thông tin chi tiết và đánh giá của các đồng nghiệp trong nhóm .NET.

(Kết thúc bài viết)

Bài viết gốc tại địa chỉ: https://devblogs.microsoft.com/dotnet/why-dotnet/

Bài 11: Các bản phân phối

Nhóm .NET tại Microsoft duy trì một số bản phân phối, gần đây nhất đã hỗ trợ Android, iOS, và WebAssembly. Nhóm này dùng một số kỹ thuật khác nhau để duy trì hỗ trợ cho các yêu cầu riêng biệt của từng môi trường. Hầu hết các nền tảng được viết bằng C#, cho phép khi chuyển đổi chỉ cần làm việc trên một tập tương đối nhỏ các thành phần.

Cộng đồng cũng duy trì một tập các bản phân phối khác, chủ yếu tập trung cho Linux. Ví dụ, .NET được bao gồm trong Alpine LinuxFedoraRed Hat Enterprise Linux, và Ubuntu.

Cộng đồng cũng đã mở rộng .NET để chạy trên các nền tảng khác. Samsung đã chuyển .NET lên nền tảng Tizen cho ArmRed Hat và IBM cũng chuyển .NET lên LinuxONE/s390xLoongson Technology chuyển .NET lên LoongArch. Chúng tôi hi vọng và mong muốn sẽ có thêm nhiều đối tác nữa chuyển .NET lên các môi trường khác.

Continue reading “Bài 11: Các bản phân phối”

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”