Cách đơn giản nhất để thực hiện phương pháp chữa cháy và quên trong c # 4.0


99

Tôi thực sự thích câu hỏi này:

Cách đơn giản nhất để thực hiện phương pháp chữa cháy và quên trong C #?

Tôi chỉ muốn biết rằng bây giờ chúng ta có các tiện ích mở rộng Song song trong C # 4.0, có cách nào tốt hơn để thực hiện Fire & Forget với Parallel linq không?


1
Câu trả lời cho câu hỏi đó vẫn áp dụng cho .NET 4.0. Fire and forget không đơn giản hơn QueueUserWorkItem.
Brian Rasmussen

Câu trả lời:


107

Không phải là câu trả lời cho 4.0, nhưng đáng chú ý là trong .Net 4.5, bạn có thể làm cho việc này đơn giản hơn với:

#pragma warning disable 4014
Task.Run(() =>
{
    MyFireAndForgetMethod();
}).ConfigureAwait(false);
#pragma warning restore 4014

Pragma là vô hiệu hóa cảnh báo cho biết bạn đang chạy Tác vụ này như lửa và quên.

Nếu phương thức bên trong dấu ngoặc nhọn trả về một Tác vụ:

#pragma warning disable 4014
Task.Run(async () =>
{
    await MyFireAndForgetMethod();
}).ConfigureAwait(false);
#pragma warning restore 4014

Hãy chia nhỏ điều đó:

Task.Run trả về một Tác vụ tạo ra cảnh báo trình biên dịch (cảnh báo CS4014) lưu ý rằng mã này sẽ được chạy trong nền - đó chính xác là những gì bạn muốn, vì vậy chúng tôi vô hiệu hóa cảnh báo 4014.

Theo mặc định, Task cố gắng "Marshal trở lại Thread ban đầu", có nghĩa là Task này sẽ chạy ở chế độ nền, sau đó cố gắng quay trở lại Thread đã bắt đầu nó. Thường kích hoạt và quên Nhiệm vụ hoàn thành sau khi Chủ đề ban đầu hoàn thành. Điều đó sẽ khiến một ThreadAbortException được ném ra. Trong hầu hết các trường hợp, điều này là vô hại - nó chỉ nói với bạn rằng, tôi đã cố gắng tham gia lại, tôi đã thất bại, nhưng dù sao thì bạn cũng không quan tâm. Nhưng vẫn hơi ồn ào khi có ThreadAbortExceptions trong nhật ký của bạn trong Sản xuất hoặc trong trình gỡ lỗi của bạn trong nhà phát triển cục bộ. .ConfigureAwait(false)chỉ là một cách để giữ cho gọn gàng và nói rõ ràng là chạy cái này ở chế độ nền, và thế là xong.

Vì điều này là dài dòng, đặc biệt là pragma xấu xí, tôi sử dụng phương pháp thư viện cho điều này:

public static class TaskHelper
{
    /// <summary>
    /// Runs a TPL Task fire-and-forget style, the right way - in the
    /// background, separate from the current thread, with no risk
    /// of it trying to rejoin the current thread.
    /// </summary>
    public static void RunBg(Func<Task> fn)
    {
        Task.Run(fn).ConfigureAwait(false);
    }

    /// <summary>
    /// Runs a task fire-and-forget style and notifies the TPL that this
    /// will not need a Thread to resume on for a long time, or that there
    /// are multiple gaps in thread use that may be long.
    /// Use for example when talking to a slow webservice.
    /// </summary>
    public static void RunBgLong(Func<Task> fn)
    {
        Task.Factory.StartNew(fn, TaskCreationOptions.LongRunning)
            .ConfigureAwait(false);
    }
}

Sử dụng:

TaskHelper.RunBg(async () =>
{
    await doSomethingAsync();
}

7
@ksm Rất tiếc, cách tiếp cận của bạn gặp một số rắc rối - bạn đã thử nghiệm chưa? Cách tiếp cận đó chính là lý do tại sao Cảnh báo 4014 tồn tại. Việc gọi một phương thức không đồng bộ mà không cần chờ đợi và không có sự trợ giúp của Task.Run ... sẽ khiến phương thức đó chạy, vâng, nhưng sau khi hoàn tất, nó sẽ cố gắng điều chỉnh trở lại luồng ban đầu mà nó đã được kích hoạt. Thường thì luồng đó đã hoàn thành việc thực thi và mã của bạn sẽ phát nổ theo những cách khó hiểu, không xác định. Đừng làm điều đó! Lệnh gọi Task.Run là một cách thuận tiện để nói "Chạy cái này trong bối cảnh toàn cầu", không để lại gì cho nó để cố gắng điều khiển.
Chris Moschini

1
@ChrisMoschini rất vui được trợ giúp và cảm ơn bạn đã cập nhật! Về vấn đề khác, nơi tôi đã viết "bạn đang gọi nó bằng một func ẩn danh không đồng bộ" trong nhận xét ở trên, thực sự khiến tôi khó hiểu đó là sự thật. Tất cả những gì tôi biết là mã hoạt động khi không đồng bộ không được bao gồm trong mã gọi (hàm ẩn danh), nhưng điều đó có nghĩa là mã gọi sẽ không được chạy theo cách không đồng bộ (thật tệ, một lỗi rất tệ)? Vì vậy, tôi không khuyên bạn nên không có async, thật kỳ lạ là trong trường hợp này, cả hai đều hoạt động.
Nicholas Petersen

8
Tôi không nhìn thấy điểm của ConfigureAwait(false)trên Task.Runkhi bạn không awaitnhiệm vụ. Mục đích của chức năng là trong tên của nó: "cấu hình await ". Nếu bạn không thực hiện awaitnhiệm vụ, bạn sẽ không đăng ký một phần tiếp theo và không có mã nào cho nhiệm vụ để "điều khiển trở lại chuỗi ban đầu", như bạn nói. Rủi ro lớn hơn là một ngoại lệ không được quan sát được phát triển lại trên chuỗi hoàn thiện, mà câu trả lời này thậm chí không giải quyết.
Mike Strobel

3
@stricq Không có sử dụng vô hiệu hóa không đồng bộ ở đây. Nếu bạn đang đề cập đến async () => ... thì chữ ký là một Hàm trả về một Tác vụ, không phải là vô hiệu.
Chris Moschini

2
Trên VB.net để nhấn cảnh báo:#Disable Warning BC42358
Altiano Gerung

85

Với Tasklớp là có, nhưng PLINQ thực sự là để truy vấn qua các bộ sưu tập.

Một cái gì đó như sau sẽ làm điều đó với Task.

Task.Factory.StartNew(() => FireAway());

Hoặc thậm chí ...

Task.Factory.StartNew(FireAway);

Hoặc là...

new Task(FireAway).Start();

Trong trường hợp FireAway

public static void FireAway()
{
    // Blah...
}

Vì vậy, nhờ độ ngắn gọn của lớp và phương thức, điều này đánh bại phiên bản threadpool từ sáu đến mười chín ký tự tùy thuộc vào ký tự bạn chọn :)

ThreadPool.QueueUserWorkItem(o => FireAway());

Chắc chắn chúng không phải là chức năng tương đương mặc dù?
Jonathon Kresner

6
Có một sự khác biệt nhỏ về ngữ nghĩa giữa StartNew và Task.Start mới, nhưng ngược lại thì có. Tất cả chúng đều xếp hàng FireAway để chạy trên một luồng trong threadpool.
Ade Miller

Làm việc cho trường hợp này fire and forget in ASP.NET WebForms and windows.close():?
PreguntonCojoneroCabrón

32

Tôi có một vài vấn đề với câu trả lời hàng đầu cho câu hỏi này.

Đầu tiên, trong một tình huống cháy nổ thực sự , bạn có thể sẽ không thực awaithiện nhiệm vụ, vì vậy việc phụ thêm cũng vô ích ConfigureAwait(false). Nếu bạn không awaittrả về giá trị ConfigureAwait, thì nó không thể có bất kỳ tác dụng nào.

Thứ hai, bạn cần biết điều gì sẽ xảy ra khi nhiệm vụ hoàn thành với một ngoại lệ. Hãy xem xét giải pháp đơn giản mà @ ade-miller đã đề xuất:

Task.Factory.StartNew(SomeMethod);  // .NET 4.0
Task.Run(SomeMethod);               // .NET 4.5

Điều này dẫn đến một mối nguy hiểm: nếu một ngoại lệ chưa được xử lý thoát khỏi SomeMethod(), ngoại lệ đó sẽ không bao giờ được quan sát và 1 có thể được phát triển lại trên chuỗi trình hoàn thiện, làm hỏng ứng dụng của bạn. Do đó, tôi khuyên bạn nên sử dụng phương pháp trợ giúp để đảm bảo rằng mọi ngoại lệ kết quả đều được tuân thủ.

Bạn có thể viết một cái gì đó như thế này:

public static class Blindly
{
    private static readonly Action<Task> DefaultErrorContinuation =
        t =>
        {
            try { t.Wait(); }
            catch {}
        };

    public static void Run(Action action, Action<Exception> handler = null)
    {
        if (action == null)
            throw new ArgumentNullException(nameof(action));

        var task = Task.Run(action);  // Adapt as necessary for .NET 4.0.

        if (handler == null)
        {
            task.ContinueWith(
                DefaultErrorContinuation,
                TaskContinuationOptions.ExecuteSynchronously |
                TaskContinuationOptions.OnlyOnFaulted);
        }
        else
        {
            task.ContinueWith(
                t => handler(t.Exception.GetBaseException()),
                TaskContinuationOptions.ExecuteSynchronously |
                TaskContinuationOptions.OnlyOnFaulted);
        }
    }
}

Việc triển khai này phải có chi phí tối thiểu: phần tiếp tục chỉ được gọi nếu tác vụ không hoàn thành thành công và nó phải được gọi đồng bộ (trái ngược với việc được lên lịch riêng với tác vụ ban đầu). Trong trường hợp "lười biếng", bạn thậm chí sẽ không bị phân bổ cho đại biểu tiếp tục.

Bắt đầu một hoạt động không đồng bộ sau đó trở nên tầm thường:

Blindly.Run(SomeMethod);                              // Ignore error
Blindly.Run(SomeMethod, e => Log.Warn("Whoops", e));  // Log error

1. Đây là hành vi mặc định trong .NET 4.0. Trong .NET 4.5, hành vi mặc định đã được thay đổi để các ngoại lệ không được quan sát sẽ không được phát triển lại trên chuỗi trình hoàn thiện (mặc dù bạn vẫn có thể quan sát chúng qua sự kiện UnobservedTaskException trên TaskScheduler). Tuy nhiên, cấu hình mặc định có thể bị ghi đè và ngay cả khi ứng dụng của bạn yêu cầu .NET 4.5, bạn không nên cho rằng các ngoại lệ tác vụ không được quan sát sẽ vô hại.


1
Nếu một trình xử lý được chuyển vào và ContinueWithđược gọi do bị hủy, thuộc tính ngoại lệ của tiền đề sẽ là Null và một ngoại lệ tham chiếu null sẽ được ném ra. Đặt tùy chọn tiếp tục tác vụ thành OnlyOnFaultedsẽ loại bỏ nhu cầu kiểm tra null hoặc kiểm tra xem ngoại lệ có rỗng không trước khi sử dụng nó.
sanmcp

Phương thức Run () cần được ghi đè cho các chữ ký phương thức khác nhau. Tốt hơn nên làm điều này như một phương pháp mở rộng của Tác vụ.
stricq

1
@stricq Câu hỏi hỏi về hoạt động "cháy và quên" (nghĩa là trạng thái không bao giờ được kiểm tra và kết quả không bao giờ được quan sát), vì vậy đó là điều tôi tập trung vào. Khi câu hỏi đặt ra là làm thế nào để bắn vào chân một cách sạch sẽ nhất, việc tranh luận xem giải pháp nào "tốt hơn" từ góc độ thiết kế trở nên khá tranh luận :). Các tốt nhất câu trả lời được cho là "không", nhưng điều đó đã giảm ngắn chiều dài câu trả lời tối thiểu, vì vậy tôi tập trung vào việc cung cấp một giải pháp ngắn gọn rằng tránh các vấn đề trình bày trong câu trả lời khác. Các đối số dễ dàng được bao bọc trong một bao đóng và không có giá trị trả về, việc sử dụng Tasksẽ trở thành một chi tiết triển khai.
Mike Strobel

@stricq Điều đó nói rằng, tôi không đồng ý với bạn. Giải pháp thay thế bạn đề xuất chính xác là những gì tôi đã làm trong quá khứ, mặc dù vì những lý do khác nhau. "Tôi muốn tránh sự cố ứng dụng khi nhà phát triển không quan sát được lỗi Task" không giống như "Tôi muốn một cách đơn giản để bắt đầu hoạt động nền, không bao giờ quan sát kết quả của nó và tôi không quan tâm cơ chế nào được sử dụng để làm điều đó." Trên cơ sở đó, tôi đứng đằng sau câu trả lời của tôi cho câu hỏi đã được đặt ra .
Mike Strobel

Làm việc cho trường hợp này: fire and forgettrong ASP.NET WebFormswindows.close()?
PreguntonCojoneroCabrón

7

Chỉ để khắc phục một số vấn đề sẽ xảy ra với câu trả lời của Mike Strobel:

Nếu bạn sử dụng var task = Task.Run(action)và sau khi chỉ định một phần tiếp theo cho tác vụ đó, thì bạn sẽ có nguy cơ Taskném một số ngoại lệ trước khi bạn chỉ định một phần tiếp theo của trình xử lý ngoại lệ cho Task. Vì vậy, lớp dưới đây sẽ không có rủi ro này:

using System;
using System.Threading.Tasks;

namespace MyNameSpace
{
    public sealed class AsyncManager : IAsyncManager
    {
        private Action<Task> DefaultExeptionHandler = t =>
        {
            try { t.Wait(); }
            catch { /* Swallow the exception */ }
        };

        public Task Run(Action action, Action<Exception> exceptionHandler = null)
        {
            if (action == null) { throw new ArgumentNullException(nameof(action)); }

            var task = new Task(action);

            Action<Task> handler = exceptionHandler != null ?
                new Action<Task>(t => exceptionHandler(t.Exception.GetBaseException())) :
                DefaultExeptionHandler;

            var continuation = task.ContinueWith(handler,
                TaskContinuationOptions.ExecuteSynchronously
                | TaskContinuationOptions.OnlyOnFaulted);
            task.Start();

            return continuation;
        }
    }
}

Ở đây, lệnh taskkhông được chạy trực tiếp, thay vào đó nó được tạo, một phần tiếp theo được chỉ định và chỉ sau đó tác vụ được chạy để loại bỏ nguy cơ tác vụ hoàn thành việc thực thi (hoặc ném một số ngoại lệ) trước khi chỉ định một phần tiếp theo.

Các Runphương pháp ở đây trả về việc tiếp tục Tasknhư vậy tôi có thể viết bài kiểm tra đơn vị đảm bảo việc thực hiện hoàn tất. Tuy nhiên, bạn có thể bỏ qua nó một cách an toàn trong cách sử dụng của mình.


Có phải bạn cũng cố ý không biến nó thành một lớp tĩnh không?
Deantwo

Lớp này thực hiện giao diện IAsyncManager, vì vậy nó không thể là một lớp tĩnh.
Mert Akcakaya
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.