C# - C Sharp: Kịch bản lập trình không đồng bộ


Khóa học qua video:
Lập trình Python All Lập trình C# All SQL Server All Lập trình C All Java PHP HTML5-CSS3-JavaScript
Đăng ký Hội viên
Tất cả các video dành cho hội viên

Trong bài viết này

  1. Tổng quan về mô hình không đồng bộ
  2. Những phần quan trọng cần hiểu
  3. Nhận biết công việc liên quan đến CPU và liên kết I/O
  4. Thêm ví dụ
  5. Thông tin và lời khuyên quan trọng
  6. Ví dụ hoàn chỉnh
  7. Các nguồn lực khác

Nếu bạn có bất kỳ nhu cầu nào liên quan đến I/O (chẳng hạn như yêu cầu dữ liệu từ mạng, truy cập cơ sở dữ liệu hoặc đọc và ghi vào hệ thống tệp), bạn sẽ muốn sử dụng lập trình không đồng bộ. Bạn cũng có thể có mã giới hạn CPU, chẳng hạn như thực hiện một phép tính tốn kém, đây cũng là một tình huống tốt để viết mã không đồng bộ.

C# có mô hình lập trình không đồng bộ ở cấp độ ngôn ngữ, cho phép dễ dàng viết mã không đồng bộ mà không cần phải thực hiện các cuộc gọi lại hoặc tuân thủ thư viện hỗ trợ tính không đồng bộ. Nó tuân theo cái được gọi là Mẫu không đồng bộ dựa trên nhiệm vụ (TAP).

Tổng quan về mô hình không đồng bộ

Cốt lõi của lập trình bất đồng bộ là các đối tượng Task và Task<T>, mô hình hóa các hoạt động không đồng bộ. Chúng được hỗ trợ bởi các từ khóa async và await. Mô hình này khá đơn giản trong hầu hết các trường hợp:

  • Đối với mã giới hạn I/O, bạn chờ đợi một thao tác trả về một Task hoặc Task<T> bên trong một phương thức async.
  • Đối với mã giới hạn CPU, bạn chờ đợi một thao tác được bắt đầu trên một luồng nền bằng phương thức Task.Run.

Từ khóa await là nơi điều kỳ diệu xảy ra. Nó mang lại quyền kiểm soát cho người gọi phương thức đã thực hiện await và cuối cùng nó cho phép giao diện người dùng phản hồi hoặc dịch vụ có tính linh hoạt. Mặc dù có nhiều cách để tiếp cận mã không đồng bộ ngoài async và await, bài viết này tập trung vào các cấu trúc cấp độ ngôn ngữ.

Ghi chú

Trong một số ví dụ sau, lớp System.Net.Http.HttpClient được sử dụng để tải xuống một số dữ liệu từ một dịch vụ web. Đối tượng s_httpClient được sử dụng trong các ví dụ này là trường tĩnh của lớp Program (vui lòng kiểm tra ví dụ đầy đủ):

private static readonly HttpClient s_httpClient = new();

Ví dụ về giới hạn I/O: Tải xuống dữ liệu từ một dịch vụ web

Bạn có thể cần tải xuống một số dữ liệu từ dịch vụ web khi nhấn nút nhưng không muốn chặn chuỗi giao diện người dùng. Nó có thể được thực hiện như thế này:

s_downloadButton.Clicked += async (o, e) =>
{
    // Dòng này sẽ mang lại quyền kiểm soát UI theo yêu cầu
    // từ dịch vụ web đang diễn ra.
    //
    // Luồng UI hiện có thể tự do thực hiện công việc khác.
    var stringData = await s_httpClient.GetStringAsync(URL);
    DoSomethingWithData(stringData);
};

Mã thể hiện ý định (tải xuống dữ liệu không đồng bộ) mà không bị sa lầy khi tương tác với các đối tượng Task.

Ví dụ về giới hạn CPU: Thực hiện phép tính cho trò chơi

Giả sử bạn đang viết một trò chơi di động trong đó việc nhấn nút có thể gây sát thương lên nhiều kẻ thù trên màn hình. Việc thực hiện tính toán thiệt hại có thể tốn kém và việc thực hiện trên chuỗi giao diện người dùng sẽ khiến trò chơi có vẻ như tạm dừng khi quá trình tính toán được thực hiện!

Cách tốt nhất để xử lý vấn đề này là bắt đầu một chuỗi nền, hoạt động bằng cách sử dụng Task.Run và chờ kết quả của nó bằng cách sử dụng await. Điều này cho phép giao diện người dùng cảm thấy mượt mà khi công việc đang được thực hiện.

static DamageResult CalculateDamageDone()
{
    return new DamageResult()
    {
        // Code bị bỏ qua:
        //
        // Thực hiện một phép tính tốn kém và trả về
        // kết quả của phép tính đó.
    };
}

s_calculateButton.Clicked += async (o, e) =>
{
    // Dòng này sẽ mang lại quyền kiểm soát cho giao diện người dùng trong khi CalculateDamageDone()
    // giải quyết công việc. Luồng UI hiện có thể tự do thực hiện công việc khác.
    var damageResult = await Task.Run(() => CalculateDamageDone());
    DisplayDamage(damageResult);
};

Mã trên thể hiện rõ ràng mục đích của sự kiện nhấp chuột vào nút, nó không yêu cầu quản lý luồng nền theo cách thủ công và thực hiện điều đó theo cách không chặn.

Điều gì xảy ra dưới vỏ bọc

Về phía C#, trình biên dịch sẽ chuyển đổi mã của bạn thành một máy trạng thái theo dõi những thứ như hiệu suất thực thi khi await đạt được và tiếp tục thực thi khi công việc nền hoàn tất.

Đối với những người thiên về mặt lý thuyết, đây là cách triển khai Mô hình hứa hẹn về tính không đồng bộ.

Những phần quan trọng cần hiểu

  • Mã không đồng bộ có thể được sử dụng cho cả mã liên kết I/O và mã liên kết CPU, nhưng khác nhau đối với từng trường hợp.
  • Mã không đồng bộ sử dụng Task<T> và Task, là các cấu trúc được sử dụng để lập mô hình công việc đang được thực hiện ở chế độ nền.
  • Từ khóa async biến một phương thức thành một phương thức không đồng bộ, cho phép bạn sử dụng từ khóa await trong phần nội dung của nó.
  • Khi từ khóa await được áp dụng, nó sẽ tạm dừng phương thức gọi và trả lại quyền kiểm soát cho người gọi cho đến khi tác vụ được chờ đợi hoàn tất.
  • await chỉ có thể được sử dụng bên trong một phương thức không đồng bộ.

Nhận biết công việc liên quan đến CPU và liên kết I/O

Hai ví dụ đầu tiên của hướng dẫn này cho thấy cách bạn có thể sử dụng async và await cho công việc liên quan đến I/O và CPU. Điều quan trọng là bạn có thể xác định khi nào công việc bạn cần thực hiện bị ràng buộc bởi I/O hay bị ràng buộc bởi CPU vì nó có thể ảnh hưởng lớn đến hiệu suất mã của bạn và có khả năng dẫn đến việc sử dụng sai một số cấu trúc nhất định.

Đây là hai câu hỏi bạn nên hỏi trước khi viết bất kỳ mã nào:

  1. Mã của bạn có đang "chờ" thứ gì đó không, chẳng hạn như dữ liệu từ cơ sở dữ liệu?

    Nếu câu trả lời của bạn là "có", thì công việc của bạn bị ràng buộc bởi I/O.

  2. Mã của bạn có thực hiện một phép tính tốn kém không?

    Nếu bạn trả lời "có" thì công việc của bạn bị ràng buộc bởi CPU.

Nếu công việc bạn có là giới hạn I/O, hãy sử dụng async và await không có Task.Run. Bạn không nên sử dụng Thư viện song song tác vụ.

Nếu công việc bạn có bị ràng buộc bởi CPU và bạn quan tâm đến khả năng phản hồi, hãy sử dụng async và await, nhưng lại tạo ra công việc trên một luồng khác bằng Task.Run. Nếu công việc phù hợp với tính đồng thời và song song, hãy cân nhắc sử dụng Thư viện song song tác vụ.

Ngoài ra, bạn phải luôn đo lường việc thực thi mã của mình. Ví dụ: bạn có thể rơi vào tình huống mà công việc liên quan đến CPU của bạn không đủ tốn kém so với chi phí chuyển đổi ngữ cảnh khi đa luồng. Mọi lựa chọn đều có sự đánh đổi của nó và bạn nên chọn sự đánh đổi chính xác cho tình huống của mình.

Thêm ví dụ

Các ví dụ sau đây minh họa nhiều cách khác nhau để bạn có thể viết mã không đồng bộ trong C#. Chúng bao gồm một số tình huống khác nhau mà bạn có thể gặp phải.

Trích xuất dữ lieu từ mạng

Đoạn mã này tải xuống HTML từ URL đã cho và đếm số lần chuỗi ".NET" xuất hiện trong HTML. Nó sử dụng ASP.NET để xác định phương thức điều khiển API Web, phương thức này thực hiện tác vụ này và trả về số.

Ghi chú

Nếu bạn dự định thực hiện phân tích cú pháp HTML trong mã production thì bạn đừng sử dụng biểu thức chính quy. Thay vào đó hãy sử dụng thư viện phân tích cú pháp.

[HttpGet, Route("DotNetCount")]
static public async Task<int> GetDotNetCount(string URL)
{
    // Tạm dừng GetDotNetCount() để cho phép trình gọi (máy chủ web)
    // chấp nhận một yêu cầu khác thay vì chặn yêu cầu này.
    var html = await s_httpClient.GetStringAsync(URL);
    return Regex.Matches(html, @"\.NET").Count;
}

Đây là kịch bản tương tự được viết cho Universal Windows App, thực hiện tác vụ tương tự khi nhấn Button:

private readonly HttpClient _httpClient = new HttpClient();

private async void OnSeeTheDotNetsButtonClick(object sender, RoutedEventArgs e)
{
    // Ghi lại phần xử lý tác vụ tại đây để có thể chờ tác vụ nền sau.
    var getDotNetFoundationHtmlTask = _httpClient.GetStringAsync("https://dotnetfoundation.org");

    // Bất kì công việc nào trên luồng UI đều có thể được thực hiện tại đây, chẳng hạn như bật Progress Bar.
    // Điều quan trọng cần thực hiện trước lời gọi "await" để người dùng
    // thấy thanh tiến trình trước khi thực thi phương thức này.
    NetworkProgressBar.IsEnabled = true;
    NetworkProgressBar.Visibility = Visibility.Visible;

    // Toán tử await tạm dừng OnSeeTheDotNetsButtonClick(), trả điều kiển về trình gọi.
    // Đó là những gì cho phép app phản hồi và không chặn luồng UI
    var html = await getDotNetFoundationHtmlTask;
    int count = Regex.Matches(html, @"\.NET").Count;

    DotNetCountLabel.Text = $"Number of .NETs on dotnetfoundation.org: {count}";

    NetworkProgressBar.IsEnabled = false;
    NetworkProgressBar.Visibility = Visibility.Collapsed;
}

Đợi nhiều nhiệm vụ hoàn thành

Bạn có thể rơi vào tình huống cần truy xuất nhiều phần dữ liệu cùng một lúc. API Taskchứa hai phương thức, Task. WhenAll và Task. WhenAny , cho phép bạn viết mã không đồng bộ thực hiện chờ không chặn trên nhiều tác vụ nền.

Ví dụ này cho thấy cách bạn có thể lấy Userdữ liệu cho một tập hợp userIds.

private static async Task<User> GetUserAsync(int userId)
{
    // Code bị bỏ qua:
    //
    // Cho một Id người dùng {userId}, truy xuất đối tượng người dùng tương ứng
    // vào mục nhập trong cơ sở dữ liệu có {userId} như là Id của nó.

    return await Task.FromResult(new User() { id = userId });
}

private static async Task<IEnumerable<User>> GetUsersAsync(IEnumerable<int> userIds)
{
    var getUserTasks = new List<Task<User>>();
    foreach (int userId in userIds)
    {
        getUserTasks.Add(GetUserAsync(userId));
    }

    return await Task.WhenAll(getUserTasks);
}

Đây là một cách khác để viết ngắn gọn hơn bằng cách sử dung LINQ:

private static async Task<User[]> GetUsersAsyncByLINQ(IEnumerable<int> userIds)
{
    var getUserTasks = userIds.Select(id => GetUserAsync(id)).ToArray();
    return await Task.WhenAll(getUserTasks);
}

Mặc dù nó ít mã hơn nhưng hãy thận trọng khi trộn LINQ với mã không đồng bộ. Bởi vì LINQ sử dụng thực thi trì hoãn (lazy), các lời gọi không đồng bộ sẽ không xảy ra ngay lập tức như chúng thực hiện trong một vòng lặp foreach trừ khi bạn buộc chuỗi được tạo lặp lại bằng lệnh gọi đến .ToList() hoặc .ToArray(). Ví dụ trên sử dụng Enumerable.ToArray để thực hiện truy vấn một cách nhanh chóng và lưu trữ kết quả vào một mảng. Điều đó buộc mã id => GetUserAsync(id) phải chạy và bắt đầu tác vụ.

Thông tin và lời khuyên quan trọng

Với lập trình không đồng bộ, có một số chi tiết cần lưu ý có thể ngăn chặn hành vi không mong muốn.

  • Các phương thức async cần phải có từ khóa await trong nội dung nếu không chúng sẽ không bao giờ mang lại kết quả!

    Điều này rất quan trọng. Nếu await không được sử dụng trong phần nội dung của một phương thức async, thì trình biên dịch C# sẽ tạo ra cảnh báo, nhưng mã sẽ biên dịch và chạy như thể đó là một phương thức bình thường. Điều này cực kỳ kém hiệu quả, vì máy trạng thái do trình biên dịch C# tạo ra cho phương thức async không thực hiện được bất kỳ điều gì.

  • Thêm "Async" làm hậu tố của mỗi tên phương thức không đồng bộ mà bạn viết.

    Đây là quy ước được sử dụng trong .NET để dễ dàng phân biệt các phương thức đồng bộ và không đồng bộ hơn. Một số phương thức nhất định không được mã của bạn gọi một cách rõ ràng (chẳng hạn như trình xử lý sự kiện hoặc phương thức điều khiển web) không nhất thiết phải được áp dụng. Vì mã của bạn không gọi chúng một cách rõ ràng nên việc đặt tên rõ ràng không quan trọng.

  • async void chỉ nên được sử dụng cho xử lý sự kiện.

    async void là cách duy nhất để cho phép các trình xử lý sự kiện không đồng bộ hoạt động vì các sự kiện không có kiểu trả về (do đó không thể sử dụng Task và Task<T>). Bất kỳ cách sử dụng nào khác của async void đều không tuân theo mô hình TAP và có thể khó sử dụng, chẳng hạn như:

    • Các ngoại lệ được đưa vào một phương thức async void không thể được phát hiện bên ngoài phương thức đó.
    • Phương thức async void rất khó kiểm tra.
    • Các phương thức async void có thể gây ra tác dụng phụ xấu nếu người gọi không mong đợi chúng không đồng bộ.
  • Cẩn thận khi sử dụng lambdas không đồng bộ trong biểu thức LINQ

    Biểu thức Lambda trong LINQ sử dụng cơ chế thực thi bị trì hoãn, nghĩa là mã có thể kết thúc thực thi vào thời điểm mà bạn không mong đợi. Việc đưa các tác vụ chặn vào đây dễ dẫn đến bế tắc nếu viết không đúng. Ngoài ra, việc lồng mã không đồng bộ như thế này cũng có thể khiến việc thực thi mã trở nên khó khăn hơn. Async và LINQ rất mạnh nhưng nên được sử dụng cùng nhau một cách cẩn thận và rõ ràng nhất có thể.

  • Viết mã chờ các Task theo hướng không chặn (non-blocking)

    Việc chặn luồng hiện tại như một phương tiện để chờ một Task hoàn thành có thể dẫn đến bế tắc và chặn các luồng ngữ cảnh, đồng thời có thể yêu cầu xử lý lỗi phức tạp hơn. Bảng sau đây cung cấp hướng dẫn về cách xử lý việc chờ tác vụ theo cách không chặn:

    Dùng cái này... Thay vì điều này... Khi muốn làm điều này...
    await Task.Wait hoặc Task.Result Lấy kết quả của một tác vụ nền
    await Task.WhenAny Task.WaitAny Đang chờ hoàn thành bất kỳ nhiệm vụ nào
    await Task.WhenAll Task.WaitAll Đang chờ tất cả nhiệm vụ hoàn thành
    await Task.Delay Thread.Sleep Chờ đợi một khoảng thời gian
  • Hãy cân nhắc sử dụng ValueTask nếu có thể

    Việc trả về một đối tượng Task từ các phương thức không đồng bộ có thể gây ra tắc nghẽn hiệu suất trong một số đường dẫn nhất định. Task là một kiểu tham chiếu, vì vậy sử dụng nó có nghĩa là cấp phát một đối tượng. Trong trường hợp một phương thức được khai báo bằng công cụ sửa đổi async trả về kết quả được lưu trong bộ nhớ đệm hoặc hoàn thành một cách đồng bộ, thì việc phân bổ bổ sung có thể trở thành một khoản chi phí thời gian đáng kể trong các phần quan trọng về hiệu suất của mã. Nó có thể trở nên tốn kém nếu việc phân bổ đó diễn ra theo vòng lặp chặt chẽ. Để biết thêm thông tin, hãy xem các kiểu trả về không đồng bộ tổng quát.

  • Cân nhắc sử dụng ConfigureAwait(false)

    Một câu hỏi phổ biến là "khi nào tôi nên sử dụng phương thức Task.ConfigureAwait(Boolean) ?". Phương thức này cho phép một đối tượng Task cấu hình bộ chờ của nó. Đây là một điều cần cân nhắc quan trọng và việc đặt nó không chính xác có thể gây ảnh hưởng đến hiệu suất và thậm chí gây bế tắc. Để biết thêm thông tin về ConfigureAwait, hãy xem Câu hỏi thường gặp về ConfigureAwait.

  • Viết mã ít trạng thái hơn

    Đừng phụ thuộc vào trạng thái của các đối tượng toàn cục hoặc việc thực thi các phương thức nhất định. Thay vào đó, chỉ phụ thuộc vào giá trị trả về của phương thức. Tại sao?

    • Mã sẽ dễ dàng hơn để reason.
    • Mã sẽ dễ kiểm tra hơn.
    • Việc trộn mã không đồng bộ và mã đồng bộ đơn giản hơn nhiều.
    • Điều kiện cuộc đua thường có thể tránh được hoàn toàn.
    • Tùy thuộc vào giá trị trả về làm cho việc phối hợp mã không đồng bộ trở nên đơn giản.
    • (Bonus) nó hoạt động thực sự tốt với tính năng chèn phụ thuộc (DI).

Mục tiêu được đề xuất là đạt được Tính minh bạch tham chiếu hoàn chỉnh hoặc gần hoàn chỉnh trong mã của bạn. Làm như vậy sẽ tạo ra một cơ sở mã có thể dự đoán, kiểm tra và bảo trì được.

Ví dụ hoàn chỉnh

Dưới đây là ví dụ hoàn chỉnh của file Program.cs.

using System.Text.RegularExpressions;
using System.Windows;
using Microsoft.AspNetCore.Mvc;

class Button
{
    public Func<object, object, Task>? Clicked
    {
        get;
        internal set;
    }
}

class DamageResult
{
    public int Damage
    {
        get { return 0; }
    }
}

class User
{
    public bool isEnabled
    {
        get;
        set;
    }

    public int id
    {
        get;
        set;
    }
}

public class Program
{
    private static readonly Button s_downloadButton = new();
    private static readonly Button s_calculateButton = new();

    private static readonly HttpClient s_httpClient = new();

    private static readonly IEnumerable<string> s_urlList = new string[]
    {
            "https://learn.microsoft.com",
            "https://learn.microsoft.com/aspnet/core",
            "https://learn.microsoft.com/azure",
            "https://learn.microsoft.com/azure/devops",
            "https://learn.microsoft.com/dotnet",
            "https://learn.microsoft.com/dotnet/desktop/wpf/get-started/create-app-visual-studio",
            "https://learn.microsoft.com/education",
            "https://learn.microsoft.com/shows/net-core-101/what-is-net",
            "https://learn.microsoft.com/enterprise-mobility-security",
            "https://learn.microsoft.com/gaming",
            "https://learn.microsoft.com/graph",
            "https://learn.microsoft.com/microsoft-365",
            "https://learn.microsoft.com/office",
            "https://learn.microsoft.com/powershell",
            "https://learn.microsoft.com/sql",
            "https://learn.microsoft.com/surface",
            "https://dotnetfoundation.org",
            "https://learn.microsoft.com/visualstudio",
            "https://learn.microsoft.com/windows",
            "https://learn.microsoft.com/xamarin"
    };

    private static void Calculate()
    {
        // <PerformGameCalculation>
        static DamageResult CalculateDamageDone()
        {
            return new DamageResult()
            {
                // Code omitted:
                //
                // Does an expensive calculation and returns
                // the result of that calculation.
            };
        }

        s_calculateButton.Clicked += async (o, e) =>
        {
            // This line will yield control to the UI while CalculateDamageDone()
            // performs its work. The UI thread is free to perform other work.
            var damageResult = await Task.Run(() => CalculateDamageDone());
            DisplayDamage(damageResult);
        };
        // </PerformGameCalculation>
    }

    private static void DisplayDamage(DamageResult damage)
    {
        Console.WriteLine(damage.Damage);
    }

    private static void Download(string URL)
    {
        // <UnblockingDownload>
        s_downloadButton.Clicked += async (o, e) =>
        {
            // This line will yield control to the UI as the request
            // from the web service is happening.
            //
            // The UI thread is now free to perform other work.
            var stringData = await s_httpClient.GetStringAsync(URL);
            DoSomethingWithData(stringData);
        };
        // </UnblockingDownload>
    }

    private static void DoSomethingWithData(object stringData)
    {
        Console.WriteLine("Displaying data: ", stringData);
    }

    // <GetUsersForDataset>
    private static async Task<User> GetUserAsync(int userId)
    {
        // Code omitted:
        //
        // Given a user Id {userId}, retrieves a User object corresponding
        // to the entry in the database with {userId} as its Id.

        return await Task.FromResult(new User() { id = userId });
    }

    private static async Task<IEnumerable<User>> GetUsersAsync(IEnumerable<int> userIds)
    {
        var getUserTasks = new List<Task<User>>();
        foreach (int userId in userIds)
        {
            getUserTasks.Add(GetUserAsync(userId));
        }

        return await Task.WhenAll(getUserTasks);
    }
    // </GetUsersForDataset>

    // <GetUsersForDatasetByLINQ>
    private static async Task<User[]> GetUsersAsyncByLINQ(IEnumerable<int> userIds)
    {
        var getUserTasks = userIds.Select(id => GetUserAsync(id)).ToArray();
        return await Task.WhenAll(getUserTasks);
    }
    // </GetUsersForDatasetByLINQ>

    // <ExtractDataFromNetwork>
    [HttpGet, Route("DotNetCount")]
    static public async Task<int> GetDotNetCount(string URL)
    {
        // Suspends GetDotNetCount() to allow the caller (the web server)
        // to accept another request, rather than blocking on this one.
        var html = await s_httpClient.GetStringAsync(URL);
        return Regex.Matches(html, @"\.NET").Count;
    }
    // </ExtractDataFromNetwork>

    static async Task Main()
    {
        Console.WriteLine("Application started.");

        Console.WriteLine("Counting '.NET' phrase in websites...");
        int total = 0;
        foreach (string url in s_urlList)
        {
            var result = await GetDotNetCount(url);
            Console.WriteLine($"{url}: {result}");
            total += result;
        }
        Console.WriteLine("Total: " + total);

        Console.WriteLine("Retrieving User objects with list of IDs...");
        IEnumerable<int> ids = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
        var users = await GetUsersAsync(ids);
        foreach (User? user in users)
        {
            Console.WriteLine($"{user.id}: isEnabled={user.isEnabled}");
        }

        Console.WriteLine("Application ending.");
    }
}

// Example output:
//
// Application started.
// Counting '.NET' phrase in websites...
// https://learn.microsoft.com: 0
// https://learn.microsoft.com/aspnet/core: 57
// https://learn.microsoft.com/azure: 1
// https://learn.microsoft.com/azure/devops: 2
// https://learn.microsoft.com/dotnet: 83
// https://learn.microsoft.com/dotnet/desktop/wpf/get-started/create-app-visual-studio: 31
// https://learn.microsoft.com/education: 0
// https://learn.microsoft.com/shows/net-core-101/what-is-net: 42
// https://learn.microsoft.com/enterprise-mobility-security: 0
// https://learn.microsoft.com/gaming: 0
// https://learn.microsoft.com/graph: 0
// https://learn.microsoft.com/microsoft-365: 0
// https://learn.microsoft.com/office: 0
// https://learn.microsoft.com/powershell: 0
// https://learn.microsoft.com/sql: 0
// https://learn.microsoft.com/surface: 0
// https://dotnetfoundation.org: 16
// https://learn.microsoft.com/visualstudio: 0
// https://learn.microsoft.com/windows: 0
// https://learn.microsoft.com/xamarin: 6
// Total: 238
// Retrieving User objects with list of IDs...
// 1: isEnabled= False
// 2: isEnabled= False
// 3: isEnabled= False
// 4: isEnabled= False
// 5: isEnabled= False
// 6: isEnabled= False
// 7: isEnabled= False
// 8: isEnabled= False
// 9: isEnabled= False
// 0: isEnabled= False
// Application ending.

Các nguồn lực khác

« Trước: Tổng quan
Khóa học qua video:
Lập trình Python All Lập trình C# All SQL Server All Lập trình C All Java PHP HTML5-CSS3-JavaScript
Đăng ký Hội viên
Tất cả các video dành cho hội viên
Copied !!!