Làm thế nào để bạn tạo một phương thức không đồng bộ trong C #?


196

Mỗi bài đăng trên blog tôi đã đọc cho bạn biết cách sử dụng một phương thức không đồng bộ trong C #, nhưng vì một số lý do kỳ lạ, không bao giờ giải thích cách xây dựng các phương thức không đồng bộ của riêng bạn để tiêu thụ. Vì vậy, tôi có mã này ngay bây giờ tiêu thụ phương thức của tôi:

private async void button1_Click(object sender, EventArgs e)
{
    var now = await CountToAsync(1000);
    label1.Text = now.ToString();
}

Và tôi đã viết phương pháp này đó là CountToAsync:

private Task<DateTime> CountToAsync(int num = 1000)
{
    return Task.Factory.StartNew(() =>
    {
        for (int i = 0; i < num; i++)
        {
            Console.WriteLine("#{0}", i);
        }
    }).ContinueWith(x => DateTime.Now);
}

Đây có phải là cách sử dụng Task.Factory, cách tốt nhất để viết một phương thức không đồng bộ, hay tôi nên viết theo cách khác?


22
Tôi đang hỏi một câu hỏi chung về cách cấu trúc một phương thức. Tôi chỉ muốn biết bắt đầu từ đâu trong việc biến các phương thức đã đồng bộ của mình thành các phương thức không đồng bộ.
Khalid Abuhakmeh

2
OK, vậy một phương thức đồng bộ điển hình làm gìtại sao bạn muốn làm cho nó không đồng bộ ?
Eric Lippert

Giả sử tôi phải xử lý một loạt các tệp và trả về một đối tượng kết quả.
Khalid Abuhakmeh

1
OK, vì vậy: (1) hoạt động có độ trễ cao là gì: có được các tệp - bởi vì mạng có thể bị chậm, hoặc bất cứ điều gì - hoặc thực hiện xử lý - vì nó đòi hỏi nhiều CPU. Và (2) bạn vẫn chưa nói lý do tại sao bạn muốn nó không đồng bộ ngay từ đầu. Có một chủ đề UI mà bạn muốn không chặn, hoặc những gì?
Eric Lippert

21
@EricLippert Ví dụ được cung cấp bởi op là rất cơ bản, nó thực sự không cần phải phức tạp như vậy.
David B.

Câu trả lời:


226

Tôi không khuyên bạn StartNewtrừ khi bạn cần mức độ phức tạp đó.

Nếu phương thức async của bạn phụ thuộc vào các phương thức async khác, cách tiếp cận đơn giản nhất là sử dụng asynctừ khóa:

private static async Task<DateTime> CountToAsync(int num = 10)
{
  for (int i = 0; i < num; i++)
  {
    await Task.Delay(TimeSpan.FromSeconds(1));
  }

  return DateTime.Now;
}

Nếu phương thức async của bạn đang hoạt động CPU, bạn nên sử dụng Task.Run:

private static async Task<DateTime> CountToAsync(int num = 10)
{
  await Task.Run(() => ...);
  return DateTime.Now;
}

Bạn có thể tìm thấy async/ awaitgiới thiệu của tôi hữu ích.


10
@Stephen: "Nếu phương thức async của bạn phụ thuộc vào các phương thức async khác" - ok, nhưng nếu đó không phải là trường hợp. Điều gì xảy ra nếu đang cố gắng bọc một số mã gọi BeginInvoke bằng một số mã gọi lại?
Ricibob

1
@Ricibob: Bạn nên sử dụng TaskFactory.FromAsyncđể bọc BeginInvoke. Không chắc ý của bạn về "mã gọi lại"; hãy gửi câu hỏi của riêng bạn với mã.
Stephen Cleary

@Stephen: Cảm ơn - có TaskFactory.FromAsync là những gì tôi đang tìm kiếm.
Ricibob

1
Stephen, trong vòng lặp for, là lần lặp tiếp theo được gọi ngay lập tức hay sau khi Task.Delaytrả về?
jp2code

3
@ jp2code: awaitlà "chờ không đồng bộ", do đó, nó không tiến hành lần lặp tiếp theo cho đến khi Task.Delayhoàn thành nhiệm vụ .
Stephen Cleary

11

Nếu bạn không muốn sử dụng async / await bên trong phương thức của mình, nhưng vẫn "trang trí" nó để có thể sử dụng từ khóa await từ bên ngoài, TaskCompletionSource.cs :

public static Task<T> RunAsync<T>(Func<T> function)
{ 
    if (function == null) throw new ArgumentNullException(“function”); 
    var tcs = new TaskCompletionSource<T>(); 
    ThreadPool.QueueUserWorkItem(_ =>          
    { 
        try 
        {  
           T result = function(); 
           tcs.SetResult(result);  
        } 
        catch(Exception exc) { tcs.SetException(exc); } 
   }); 
   return tcs.Task; 
}

Từ đâyđây

Để hỗ trợ mô hình như vậy với Nhiệm vụ, chúng ta cần một cách để giữ lại mặt tiền Nhiệm vụ và khả năng tham chiếu một hoạt động không đồng bộ tùy ý như một Nhiệm vụ, nhưng để kiểm soát thời gian của Nhiệm vụ đó theo quy tắc của cơ sở hạ tầng bên dưới cung cấp không đồng bộ và làm như vậy theo cách không tốn kém đáng kể. Đây là mục đích của TaskCompletionSource.

Tôi thấy cũng được sử dụng trong nguồn .NET, vd. WebClient.cs :

    [HostProtection(ExternalThreading = true)]
    [ComVisible(false)]
    public Task<string> UploadStringTaskAsync(Uri address, string method, string data)
    {
        // Create the task to be returned
        var tcs = new TaskCompletionSource<string>(address);

        // Setup the callback event handler
        UploadStringCompletedEventHandler handler = null;
        handler = (sender, e) => HandleCompletion(tcs, e, (args) => args.Result, handler, (webClient, completion) => webClient.UploadStringCompleted -= completion);
        this.UploadStringCompleted += handler;

        // Start the async operation.
        try { this.UploadStringAsync(address, method, data, tcs); }
        catch
        {
            this.UploadStringCompleted -= handler;
            throw;
        }

        // Return the task that represents the async operation
        return tcs.Task;
    }

Cuối cùng, tôi cũng thấy hữu ích như sau:

Tôi được hỏi câu hỏi này tất cả các thời gian. Hàm ý là phải có một số luồng ở đâu đó đang chặn cuộc gọi I / O tới tài nguyên bên ngoài. Vì vậy, mã không đồng bộ giải phóng luồng yêu cầu, nhưng chỉ bằng chi phí của một luồng khác ở nơi khác trong hệ thống, phải không? Không hoàn toàn không. Để hiểu tại sao thang đo yêu cầu không đồng bộ, tôi sẽ theo dõi một ví dụ (đơn giản hóa) của một cuộc gọi I / O không đồng bộ. Giả sử một yêu cầu cần ghi vào một tệp. Chuỗi yêu cầu gọi phương thức ghi không đồng bộ. WriteAsync được Thư viện lớp cơ sở (BCL) triển khai và sử dụng các cổng hoàn thành cho I / O không đồng bộ của nó. Vì vậy, lệnh gọi WriteAsync được truyền xuống HĐH dưới dạng ghi tệp không đồng bộ. Hệ điều hành sau đó giao tiếp với ngăn xếp trình điều khiển, chuyển dữ liệu để ghi vào gói yêu cầu I / O (IRP). Đây là nơi mà mọi thứ trở nên thú vị: Nếu trình điều khiển thiết bị không thể xử lý IRP ngay lập tức, nó phải xử lý không đồng bộ. Vì vậy, trình điều khiển nói với đĩa bắt đầu ghi và trả về một phản hồi đang chờ xử lý của Wap cho HĐH. Hệ điều hành chuyển đáp ứng của Edward đang chờ xử lý đối với BCL và BCL trả về một tác vụ không hoàn chỉnh cho mã xử lý yêu cầu. Mã xử lý yêu cầu đang chờ tác vụ, trả về một tác vụ chưa hoàn thành từ phương thức đó, v.v. Cuối cùng, mã xử lý yêu cầu kết thúc trả về một tác vụ không hoàn chỉnh cho ASP.NET và luồng yêu cầu được giải phóng để trở về nhóm luồng. Mã xử lý yêu cầu đang chờ tác vụ, trả về một tác vụ chưa hoàn thành từ phương thức đó, v.v. Cuối cùng, mã xử lý yêu cầu kết thúc trả về một tác vụ không hoàn chỉnh cho ASP.NET và luồng yêu cầu được giải phóng để trở về nhóm luồng. Mã xử lý yêu cầu đang chờ tác vụ, trả về một tác vụ chưa hoàn thành từ phương thức đó, v.v. Cuối cùng, mã xử lý yêu cầu kết thúc trả về một tác vụ không hoàn chỉnh cho ASP.NET và luồng yêu cầu được giải phóng để trở về nhóm luồng.

Giới thiệu về Async / Await trên ASP.NET

Nếu mục tiêu là cải thiện khả năng mở rộng (chứ không phải khả năng đáp ứng), thì tất cả phụ thuộc vào sự tồn tại của một I / O bên ngoài cung cấp cơ hội để làm điều đó.


1
Nếu bạn sẽ thấy nhận xét phía trên triển khai Task.Run, ví dụ: Xếp hàng công việc đã chỉ định để chạy trên ThreadPool và trả về một điều khiển tác vụ cho công việc đó . Bạn thực sự làm như vậy. Tôi chắc chắn rằng Task.Run sẽ tốt hơn, vì nó quản lý bên trong CancellingToken và thực hiện một số tối ưu hóa với các tùy chọn lập lịch và chạy.
an toànPtr

Vì vậy, những gì bạn đã làm với ThreadPool.QueueUserWorkItem có thể được thực hiện với Task.Run, phải không?
Razvan

-1

Một cách rất đơn giản để làm cho một phương thức không đồng bộ là sử dụng phương thức Task.Yield (). Như MSDN tuyên bố:

Bạn có thể sử dụng await Task.Yield (); trong một phương thức không đồng bộ để buộc phương thức hoàn thành không đồng bộ.

Chèn nó vào đầu phương thức của bạn và sau đó nó sẽ trả về ngay cho người gọi và hoàn thành phần còn lại của phương thức trên một luồng khác.

private async Task<DateTime> CountToAsync(int num = 1000)
{
    await Task.Yield();
    for (int i = 0; i < num; i++)
    {
        Console.WriteLine("#{0}", i);
    }
    return DateTime.Now;
}

Trong một ứng dụng WinForms, phần còn lại của phương thức sẽ hoàn thành trong cùng một luồng (luồng UI). Điều Task.Yield()này thực sự hữu ích, đối với các trường hợp mà bạn muốn đảm bảo rằng trả lại Tasksẽ không được hoàn thành ngay khi tạo.
Theodor Zoulias
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.