Khi nào nên sử dụng TaskCompletionSource <T>?


199

AFAIK, tất cả những gì nó biết là tại một số điểm, phương thức SetResulthoặc SetExceptionphương thức của nó đang được gọi để hoàn thành việc Task<T>tiếp xúc thông qua Tasktài sản của nó .

Nói cách khác, nó đóng vai trò là nhà sản xuất cho một Task<TResult>và hoàn thành.

Tôi thấy ở đây ví dụ:

Nếu tôi cần một cách để thực thi Func không đồng bộ và có một Tác vụ để thể hiện thao tác đó.

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; 
}

Mà có thể được sử dụng * nếu tôi không có Task.Factory.StartNew- Nhưng tôi làmTask.Factory.StartNew.

Câu hỏi:

Có thể ai đó xin vui lòng giải thích bằng ví dụ một kịch bản liên quan trực tiếp đến TaskCompletionSource chứ không phải một giả thuyết tình huống trong đó tôi không có Task.Factory.StartNew?


5
TaskCompletionSource chủ yếu được sử dụng để gói api dựa trên sự kiện async với Nhiệm vụ mà không tạo Chủ đề mới.
Arvis

Câu trả lời:


230

Tôi chủ yếu sử dụng nó khi chỉ có API dựa trên sự kiện khả dụng ( ví dụ: ổ cắm Windows Phone 8 ):

public Task<Args> SomeApiWrapper()
{
    TaskCompletionSource<Args> tcs = new TaskCompletionSource<Args>(); 

    var obj = new SomeApi();

    // will get raised, when the work is done
    obj.Done += (args) => 
    {
        // this will notify the caller 
        // of the SomeApiWrapper that 
        // the task just completed
        tcs.SetResult(args);
    }

    // start the work
    obj.Do();

    return tcs.Task;
}

Vì vậy, nó đặc biệt hữu ích khi được sử dụng cùng với asynctừ khóa C # 5 .


4
bạn có thể viết bằng chữ những gì chúng ta thấy ở đây? Có phải nó SomeApiWrapperđang chờ ở đâu đó cho đến khi nhà xuất bản nêu ra sự kiện khiến nhiệm vụ này hoàn thành?
Royi Namir

hãy xem liên kết tôi vừa thêm
GameScripting

6
Chỉ là một bản cập nhật, Microsoft đã phát hành Microsoft.Bcl.Asyncgói trên NuGet, cho phép các async/awaittừ khóa trong các dự án .NET 4.0 (khuyến nghị VS2012 trở lên).
Erik

1
@ Fran_gg7 bạn có thể sử dụng CancellingToken, xem msdn.microsoft.com/en-us/l Library / dd997394 (v = vs.110) .aspx hoặc như một câu hỏi mới tại đây trên stackoverflow
GameScripting

1
Vấn đề với việc triển khai này là điều này tạo ra rò rỉ bộ nhớ vì sự kiện này không bao giờ được phát hành từ obj.Done
Walter Vehoeven

78

Theo kinh nghiệm của tôi, TaskCompletionSourcelà tuyệt vời để bọc các mẫu không đồng bộ cũ với mẫu hiện đại async/await.

Ví dụ có lợi nhất mà tôi có thể nghĩ đến là khi làm việc với Socket. Nó có các mẫu APM và EAP cũ, nhưng không phải là các awaitable Taskphương thức đã TcpListenerTcpClientđang có.

Cá nhân tôi có một số vấn đề với NetworkStreamlớp và thích nguyên Socket. Vì tôi cũng thích async/awaitmẫu này, tôi đã tạo một lớp mở rộng SocketExtendertạo ra một số phương thức mở rộng cho Socket.

Tất cả các phương thức này sử dụng TaskCompletionSource<T>để bọc các cuộc gọi không đồng bộ như vậy:

    public static Task<Socket> AcceptAsync(this Socket socket)
    {
        if (socket == null)
            throw new ArgumentNullException("socket");

        var tcs = new TaskCompletionSource<Socket>();

        socket.BeginAccept(asyncResult =>
        {
            try
            {
                var s = asyncResult.AsyncState as Socket;
                var client = s.EndAccept(asyncResult);

                tcs.SetResult(client);
            }
            catch (Exception ex)
            {
                tcs.SetException(ex);
            }

        }, socket);

        return tcs.Task;
    }

Tôi vượt qua socketvào BeginAcceptphương pháp để tôi có được một tăng hiệu suất nhẹ ra khỏi trình biên dịch không phải Palăng tham số địa phương.

Sau đó là vẻ đẹp của tất cả:

 var listener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
 listener.Bind(new IPEndPoint(IPAddress.Loopback, 2610));
 listener.Listen(10);

 var client = await listener.AcceptAsync();

1
Tại sao Task.Factory.StartNew không hoạt động ở đây?
Tola Odejayi

23
@Tola Vì điều đó đã tạo ra một tác vụ mới chạy trên một luồng xử lý luồng, nhưng đoạn mã trên sử dụng luồng hoàn thành i / o được bắt đầu bởi BeginAccept, iow: nó không bắt đầu một luồng mới.
Frans Bouma

4
Cảm ơn, @ Frans-Bouma. Vì vậy, TaskCompletionSource là một cách tiện lợi để chuyển đổi mã sử dụng các câu lệnh Begin ... End ... thành một tác vụ?
Tola Odejayi

3
@TolaOdejayi Bit của một phản hồi muộn, nhưng vâng, đó là một trong những trường hợp sử dụng chính mà tôi đã tìm thấy cho nó. Nó hoạt động tuyệt vời cho quá trình chuyển đổi mã này.
Erik

4
Nhìn vào Nhiệm vụ <TResult> .romromync để bọc các Begin.. End...câu lệnh.
MicBig

37

Đối với tôi, một kịch bản cổ điển để sử dụng TaskCompletionSourcelà khi phương pháp của tôi không nhất thiết phải thực hiện một thao tác tốn thời gian. Những gì nó cho phép chúng tôi làm là chọn các trường hợp cụ thể mà chúng tôi muốn sử dụng một chủ đề mới.

Một ví dụ điển hình cho điều này là khi bạn sử dụng bộ đệm. Bạn có thể có một GetResourceAsyncphương thức, tìm trong bộ đệm cho tài nguyên được yêu cầu và trả về cùng một lúc (mà không sử dụng một luồng mới, bằng cách sử dụng TaskCompletionSource) nếu tìm thấy tài nguyên. Chỉ khi tài nguyên không được tìm thấy, chúng tôi mới muốn sử dụng một luồng mới và truy xuất nó bằng cách sử dụng Task.Run().

Một ví dụ mã có thể được nhìn thấy ở đây: Làm thế nào để chạy mã một cách có điều kiện một cách không đồng bộ bằng cách sử dụng các tác vụ


Tôi đã thấy câu hỏi của bạn và cũng là câu trả lời. (nhìn vào bình luận của tôi để trả lời) .... :-) và thực sự đó là một câu hỏi và câu trả lời mang tính giáo dục.
Royi Namir

11
Đây thực sự không phải là một tình huống trong đó TCS là cần thiết. Bạn chỉ có thể sử dụng Task.FromResultđể làm điều này. Tất nhiên, nếu bạn đang sử dụng 4.0 và không có Task.FromResultthứ bạn sử dụng TCS để viết riêng cho bạn FromResult .
Phục vụ

@Servy Task.FromResultchỉ khả dụng kể từ .NET 4.5. Trước đó, đó là cách để đạt được hành vi này.
Adi Lester

@AdiLester Câu trả lời của bạn là tham khảo Task.Run, cho biết đó là 4,5+. Và bình luận trước đây của tôi đặc biệt đề cập đến .NET 4.0.
Phục vụ

@Servy Không phải ai đọc câu trả lời này cũng nhắm mục tiêu .NET 4.5+. Tôi tin rằng đây là một câu trả lời hay và hợp lệ giúp mọi người hỏi câu hỏi của OP (nhân tiện được gắn thẻ .NET-4.0). Dù bằng cách nào, việc bỏ qua nó có vẻ hơi nhiều đối với tôi, nhưng nếu bạn thực sự tin rằng nó xứng đáng với một downvote thì hãy tiếp tục.
Adi Lester

25

Trong bài đăng trên blog này , Levi Botelho mô tả cách sử dụng TaskCompletionSourceđể viết một trình bao bọc không đồng bộ cho một Quy trình để bạn có thể khởi chạy nó và chờ kết thúc.

public static Task RunProcessAsync(string processPath)
{
    var tcs = new TaskCompletionSource<object>();
    var process = new Process
    {
        EnableRaisingEvents = true,
        StartInfo = new ProcessStartInfo(processPath)
        {
            RedirectStandardError = true,
            UseShellExecute = false
        }
    };
    process.Exited += (sender, args) =>
    {
        if (process.ExitCode != 0)
        {
            var errorMessage = process.StandardError.ReadToEnd();
            tcs.SetException(new InvalidOperationException("The process did not exit correctly. " +
                "The corresponding error message was: " + errorMessage));
        }
        else
        {
            tcs.SetResult(null);
        }
        process.Dispose();
    };
    process.Start();
    return tcs.Task;
}

và cách sử dụng của nó

await RunProcessAsync("myexecutable.exe");

14

Có vẻ như không ai đề cập đến, nhưng tôi đoán các bài kiểm tra đơn vị cũng có thể được coi là đủ thực tế .

Tôi thấy TaskCompletionSourcehữu ích khi chế nhạo một phụ thuộc bằng phương thức async.

Trong chương trình thực tế đang thử nghiệm:

public interface IEntityFacade
{
  Task<Entity> GetByIdAsync(string id);
}

Trong bài kiểm tra đơn vị:

// set up mock dependency (here with NSubstitute)

TaskCompletionSource<Entity> queryTaskDriver = new TaskCompletionSource<Entity>();

IEntityFacade entityFacade = Substitute.For<IEntityFacade>();

entityFacade.GetByIdAsync(Arg.Any<string>()).Returns(queryTaskDriver.Task);

// later on, in the "Act" phase

private void When_Task_Completes_Successfully()
{
  queryTaskDriver.SetResult(someExpectedEntity);
  // ...
}

private void When_Task_Gives_Error()
{
  queryTaskDriver.SetException(someExpectedException);
  // ...
}

Xét cho cùng, việc sử dụng TaskCompletionSource này dường như là một trường hợp khác của "một đối tượng Tác vụ không thực thi mã".


11

TaskCompletionSource được sử dụng để tạo các đối tượng tác vụ không thực thi mã. Trong các kịch bản trong thế giới thực, TaskCompletionSource là lý tưởng cho các hoạt động ràng buộc I / O. Bằng cách này, bạn nhận được tất cả các lợi ích của các tác vụ (ví dụ: trả về giá trị, tiếp tục, v.v.) mà không chặn một luồng trong suốt thời gian của hoạt động. Nếu "chức năng" của bạn là một hoạt động ràng buộc I / O, thì không nên chặn một luồng sử dụng Tác vụ mới . Thay vào đó, bằng cách sử dụng TaskCompletionSource , bạn có thể tạo một tác vụ nô lệ để chỉ ra khi nào hoạt động ràng buộc I / O của bạn kết thúc hoặc lỗi.


5

Có một ví dụ thực tế với một lời giải thích hợp lý trong bài đăng này từ blog "Lập trình song song với .NET" . Bạn thực sự nên đọc nó, nhưng dù sao đây cũng là một bản tóm tắt.

Bài đăng trên blog cho thấy hai triển khai cho:

"một phương thức xuất xưởng để tạo ra các nhiệm vụ bị trì hoãn trên các ứng dụng, các nhiệm vụ không thực sự được lên lịch cho đến khi một số thời gian chờ do người dùng cung cấp đã xảy ra."

Việc thực hiện đầu tiên được hiển thị dựa trên Task<>và có hai lỗ hổng lớn. Bài thực hiện thứ hai tiếp tục để giảm thiểu những điều này bằng cách sử dụng TaskCompletionSource<>.

Đây là cách thực hiện thứ hai:

public static Task StartNewDelayed(int millisecondsDelay, Action action)
{
    // Validate arguments
    if (millisecondsDelay < 0)
        throw new ArgumentOutOfRangeException("millisecondsDelay");
    if (action == null) throw new ArgumentNullException("action");

    // Create a trigger used to start the task
    var tcs = new TaskCompletionSource<object>();

    // Start a timer that will trigger it
    var timer = new Timer(
        _ => tcs.SetResult(null), null, millisecondsDelay, Timeout.Infinite);

    // Create and return a task that will be scheduled when the trigger fires.
    return tcs.Task.ContinueWith(_ =>
    {
        timer.Dispose();
        action();
    });
}

sẽ tốt hơn để sử dụng chờ đợi trên tcs.Task và sau đó sử dụng hành động () sau
Royi Namir

5
khi bạn quay lại bối cảnh nơi bạn rời đi, nơi Continuewith không bảo vệ bối cảnh. (không phải theo mặc định) cũng nếu câu lệnh tiếp theo trong hành động () gây ra một ngoại lệ, thật khó để bắt được nó khi sử dụng await sẽ hiển thị cho bạn như một ngoại lệ thông thường.
Royi Namir

3
Tại sao không chỉ await Task.Delay(millisecondsDelay); action(); return;hoặc (trong .Net 4.0)return Task.Delay(millisecondsDelay).ContinueWith( _ => action() );
sgnsajgon

@sgnsajgon chắc chắn sẽ dễ đọc và dễ bảo trì hơn
JwJosefy

@JwJosefy Trên thực tế, phương thức Task.Delay có thể được thực hiện bằng cách sử dụng TaskCompletionSource , tương tự như mã trên. Triển khai thực sự ở đây: Task.cs
sgnsajgon

4

Điều này có thể là quá đơn giản hóa mọi thứ nhưng nguồn TaskCompletion cho phép một người chờ đợi vào một sự kiện. Vì tcs.SetResult chỉ được đặt khi sự kiện xảy ra, người gọi có thể chờ đợi trong nhiệm vụ.

Xem video này để hiểu thêm:

http://channel9.msdn.com/Series/Three-Essential-Tips-for-Async/Lucian03-TipsForAsyncThreadsAndDatabinding


1
Vui lòng đặt mã hoặc tài liệu liên quan ở đây vì các liên kết có thể thay đổi theo thời gian và làm cho câu trả lời này không liên quan.
rfornal

3

Tôi kịch bản thế giới thực mà tôi đã sử dụng TaskCompletionSourcelà khi thực hiện hàng đợi tải xuống. Trong trường hợp của tôi nếu người dùng bắt đầu 100 lượt tải xuống, tôi không muốn tắt tất cả chúng cùng một lúc và vì vậy thay vì trả lại một tác vụ được phân tầng, tôi trả lại một tác vụ kèm theo TaskCompletionSource. Khi quá trình tải xuống được hoàn thành, chuỗi đang hoạt động, hàng đợi sẽ hoàn thành nhiệm vụ.

Khái niệm chính ở đây là tôi đang tách rời khi một khách hàng yêu cầu một nhiệm vụ được bắt đầu từ khi nó thực sự bắt đầu. Trong trường hợp này vì tôi không muốn khách hàng phải xử lý việc quản lý tài nguyên.

lưu ý rằng bạn có thể sử dụng async / await trong .net 4 miễn là bạn đang sử dụng trình biên dịch C # 5 (VS 2012+) xem tại đây để biết thêm chi tiết.


0

Tôi đã từng TaskCompletionSourcechạy một tác vụ cho đến khi nó bị hủy. Trong trường hợp này, đó là thuê bao ServiceBus mà tôi thường muốn chạy miễn là ứng dụng chạy.

public async Task RunUntilCancellation(
    CancellationToken cancellationToken,
    Func<Task> onCancel)
{
    var doneReceiving = new TaskCompletionSource<bool>();

    cancellationToken.Register(
        async () =>
        {
            await onCancel();
            doneReceiving.SetResult(true); // Signal to quit message listener
        });

    await doneReceiving.Task.ConfigureAwait(false); // Listen until quit signal is received.
}

1
Không cần sử dụng 'async' với 'TaskCompletionSource' vì nó đã tạo một tác vụ
Mandeep Janjua

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.