วิธีที่ง่ายที่สุดในการจุดไฟและวิธีการลืมใน c # 4.0


108

ฉันชอบคำถามนี้มาก:

วิธีที่ง่ายที่สุดในการจุดไฟและวิธีการลืมใน C #?

ฉันแค่อยากรู้ว่าตอนนี้เรามีส่วนขยายแบบขนานใน C # 4.0 แล้วมีวิธีที่ดีกว่าในการทำ Fire & Forget ด้วย Parallel linq หรือไม่?


1
คำตอบสำหรับคำถามนั้นยังคงใช้กับ. NET 4.0 ไฟและการลืมไม่ได้ง่ายไปกว่า QueueUserWorkItem
Brian Rasmussen

คำตอบ:


118

ไม่ใช่คำตอบสำหรับ 4.0 แต่น่าสังเกตว่าใน. Net 4.5 คุณสามารถทำให้สิ่งนี้ง่ายขึ้นด้วย:

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

pragma คือการปิดใช้งานคำเตือนที่บอกคุณว่าคุณกำลังเรียกใช้งานนี้เป็นไฟและลืม

หากวิธีการภายในวงเล็บปีกกาส่งคืนงาน:

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

มาทำลายมัน:

Task.Run ส่งคืนงานซึ่งสร้างคำเตือนคอมไพเลอร์ (คำเตือน CS4014) โดยสังเกตว่าโค้ดนี้จะทำงานในพื้นหลังนั่นคือสิ่งที่คุณต้องการดังนั้นเราจึงปิดการเตือน 4014

โดยค่าเริ่มต้น Tasks จะพยายาม "Marshal กลับสู่เธรดเดิม" ซึ่งหมายความว่างานนี้จะทำงานในพื้นหลังจากนั้นพยายามกลับไปที่เธรดที่เริ่มต้น มักจะเริ่มทำงานและลืม Tasks เสร็จสิ้นหลังจากเธรดเดิมเสร็จสิ้น ซึ่งจะทำให้ ThreadAbortException ถูกโยน ในกรณีส่วนใหญ่สิ่งนี้ไม่เป็นอันตราย - แค่บอกคุณว่าฉันพยายามเข้าร่วมอีกครั้งฉันล้มเหลว แต่คุณไม่สนใจอยู่ดี แต่ก็ยังมีเสียงดังเล็กน้อยที่จะมี ThreadAbortExceptions ทั้งในบันทึกของคุณในการผลิตหรือในดีบักเกอร์ของคุณใน dev ในเครื่อง .ConfigureAwait(false)เป็นเพียงวิธีการรักษาความเป็นระเบียบเรียบร้อยและพูดอย่างชัดเจนเรียกใช้สิ่งนี้ในพื้นหลังเท่านี้เอง

เนื่องจากสิ่งนี้เป็นคำพูดโดยเฉพาะ pragma ที่น่าเกลียดฉันจึงใช้วิธีไลบรารีสำหรับสิ่งนี้:

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

การใช้งาน:

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

7
@ksm แนวทางของคุณประสบปัญหาอยู่ - คุณได้ทดสอบหรือยัง? แนวทางดังกล่าวเป็นเหตุผลว่าทำไมคำเตือน 4014 จึงมีอยู่ การเรียกใช้เมธอด async โดยไม่ต้องรอและไม่ได้รับความช่วยเหลือจาก Task Run ... จะทำให้เมธอดนั้นทำงานใช่ แต่เมื่อเสร็จสิ้นมันจะพยายามมาร์แชลกลับไปที่เธรดเดิมที่มันถูกไล่ออก บ่อยครั้งที่เธรดนั้นจะดำเนินการเสร็จสิ้นแล้วและโค้ดของคุณจะระเบิดด้วยวิธีที่สับสนและไม่แน่นอน อย่าทำ! การเรียกไปที่ Task.Run เป็นวิธีที่สะดวกในการพูดว่า "เรียกใช้สิ่งนี้ในบริบทสากล" โดยไม่ต้องทิ้งอะไรไว้ให้มันพยายามจอมพล
Chris Moschini

1
@ChrisMoschini ยินดีที่ได้ช่วยเหลือและขอบคุณสำหรับการอัปเดต! ในอีกเรื่องหนึ่งที่ฉันเขียนว่า "คุณกำลังเรียกมันด้วย func นิรนามแบบไม่ระบุตัวตน" ในความคิดเห็นด้านบนมันทำให้ฉันสับสนโดยสุจริตซึ่งเป็นความจริง ทั้งหมดที่ฉันรู้คือรหัสทำงานเมื่อไม่รวม async ในรหัสการโทร (ฟังก์ชันไม่ระบุตัวตน) แต่นั่นหมายความว่ารหัสการโทรจะไม่ทำงานในลักษณะอะซิงโครนัส (ซึ่งไม่ดี gotcha ที่แย่มาก) หรือไม่? ดังนั้นฉันไม่แนะนำให้ไม่มี async มันเป็นเรื่องแปลกที่ในกรณีนี้ทั้งสองทำงาน
Nicholas Petersen

9
ฉันไม่เห็นจุดConfigureAwait(false)ในTask.Runเมื่อคุณทำไม่ได้awaitงาน วัตถุประสงค์ของฟังก์ชั่นอยู่ในชื่อ: "กำหนดค่าawait " หากคุณไม่ได้awaitทำงานคุณจะไม่ได้ลงทะเบียนความต่อเนื่องและไม่มีรหัสสำหรับงานในการ "กลับสู่เธรดเดิม" อย่างที่คุณพูด ความเสี่ยงที่ใหญ่กว่าคือข้อยกเว้นที่ไม่สามารถสังเกตได้ซึ่งจะถูกเปลี่ยนใหม่ในเธรด Finalizer ซึ่งคำตอบนี้ไม่ได้ระบุไว้ด้วยซ้ำ
Mike Strobel

3
@stricq ไม่มีการใช้โมฆะ async ที่นี่ หากคุณกำลังอ้างถึง async () => ... ลายเซ็นคือ Func ที่ส่งคืนงานไม่ใช่โมฆะ
Chris Moschini

2
ใน VB.net เพื่อระงับคำเตือน:#Disable Warning BC42358
Altiano Gerung

87

ด้วยTaskคลาสใช่ แต่ PLINQ มีไว้สำหรับการค้นหาคอลเลกชัน

สิ่งต่อไปนี้จะทำกับงาน

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

หรือแม้กระทั่ง...

Task.Factory.StartNew(FireAway);

หรือ...

new Task(FireAway).Start();

ที่ไหนFireAwayเป็น

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

ดังนั้นด้วยความเที่ยงตรงของคลาสและชื่อเมธอดสิ่งนี้จะทำให้เวอร์ชันเธรดพูลอยู่ระหว่างหกถึงสิบเก้าอักขระขึ้นอยู่กับตัวที่คุณเลือก :)

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

แน่นอนว่ามันไม่เทียบเท่าฟังก์ชันการทำงาน?
Jonathon Kresner

6
มีความแตกต่างทางความหมายเล็กน้อยระหว่าง StartNew และงานใหม่เริ่ม แต่อย่างอื่นใช่ พวกเขาทั้งหมดจัดคิว FireAway เพื่อรันบนเธรดในเธรดพูล
Ade Miller

ทำงานในกรณีนี้: fire and forget in ASP.NET WebForms and windows.close()?
PreguntonCojoneroCabrón

36

ฉันมีปัญหาสองสามข้อเกี่ยวกับคำตอบหลักของคำถามนี้

ครั้งแรกในจริงไฟและลืมสถานการณ์ที่คุณอาจจะไม่ได้งานจึงไม่มีประโยชน์ที่จะผนวกawait ConfigureAwait(false)หากคุณไม่ได้awaitรับค่าที่ส่งคืนมาConfigureAwaitก็จะไม่มีผลใด ๆ

ประการที่สองคุณต้องระวังว่าจะเกิดอะไรขึ้นเมื่องานเสร็จสมบูรณ์โดยมีข้อยกเว้น ลองพิจารณาวิธีง่ายๆที่ @ ade-miller แนะนำ:

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

สิ่งนี้ทำให้เกิดอันตราย: หากข้อยกเว้นที่ไม่สามารถจัดการได้หลุดรอดSomeMethod()ข้อยกเว้นนั้นจะไม่ถูกสังเกตและ1อาจถูกเปลี่ยนใหม่ในเธรด Finalizer ทำให้แอปพลิเคชันของคุณหยุดทำงาน ฉันจึงขอแนะนำให้ใช้วิธีการช่วยเหลือเพื่อให้แน่ใจว่ามีการปฏิบัติตามข้อยกเว้นที่เกิดขึ้น

คุณสามารถเขียนสิ่งนี้:

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

การนำไปใช้งานนี้ควรมีค่าใช้จ่ายน้อยที่สุด: ความต่อเนื่องจะถูกเรียกใช้ก็ต่อเมื่องานไม่เสร็จสมบูรณ์และควรเรียกใช้แบบซิงโครนัส (ตรงข้ามกับการกำหนดเวลาแยกจากงานดั้งเดิม) ในกรณี "ขี้เกียจ" คุณจะไม่ได้รับการจัดสรรสำหรับผู้รับมอบสิทธิ์ต่อเนื่อง

การเริ่มต้นการดำเนินการแบบอะซิงโครนัสจะกลายเป็นเรื่องเล็กน้อย:

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

1. นี่เป็นลักษณะการทำงานเริ่มต้นใน. NET 4.0 ใน. NET 4.5 ลักษณะการทำงานเริ่มต้นมีการเปลี่ยนแปลงเพื่อให้ข้อยกเว้นที่ไม่สามารถสังเกตได้จะไม่ถูกเปลี่ยนใหม่ในเธรด Finalizer (แม้ว่าคุณจะยังคงสังเกตเห็นได้จากเหตุการณ์ UnobservedTaskException บน TaskScheduler) อย่างไรก็ตามการกำหนดค่าเริ่มต้นสามารถถูกแทนที่ได้และแม้ว่าแอปพลิเคชันของคุณจะต้องใช้. NET 4.5 แต่คุณก็ไม่ควรคิดว่าข้อยกเว้นของงานที่ไม่ได้รับการตรวจจับจะไม่เป็นอันตราย


1
หากมีการส่งตัวจัดการเข้ามาและContinueWithถูกเรียกใช้เนื่องจากการยกเลิกคุณสมบัติข้อยกเว้นของก่อนหน้านี้จะเป็น Null และข้อยกเว้นการอ้างอิงที่เป็นโมฆะจะถูกโยนทิ้ง การตั้งค่าตัวเลือกความต่อเนื่องของงานเป็นOnlyOnFaultedจะขจัดความจำเป็นในการตรวจสอบค่าว่างหรือตรวจสอบว่าข้อยกเว้นเป็นโมฆะหรือไม่ก่อนใช้งาน
sanmcp

ต้องมีการแทนที่เมธอด Run () สำหรับลายเซ็นของเมธอดอื่น ดีกว่าที่จะทำเช่นนี้เป็นวิธีการขยายงาน
stricq

1
@stricq คำถามถามเกี่ยวกับการดำเนินการ "ไฟและการลืม" (กล่าวคือไม่เคยตรวจสอบสถานะและไม่เคยสังเกตผลลัพธ์) นั่นคือสิ่งที่ฉันมุ่งเน้น เมื่อคำถามคือวิธียิงตัวเองอย่างหมดจดที่สุดการถกเถียงกันว่าโซลูชันใด "ดีกว่า" จากมุมมองของการออกแบบก็ค่อนข้างจะน่าเบื่อ :) ที่ดีที่สุดคำตอบคือเนื้อหา "ทำไม่ได้" แต่ที่ลดลงระยะสั้นของความยาวคำตอบขั้นต่ำดังนั้นผมจึงมุ่งเน้นไปที่การให้บริการโซลูชั่นรัดกุมว่าปัญหาหลีกเลี่ยงนำเสนอในคำตอบอื่น ๆ อาร์กิวเมนต์ถูกปิดไว้อย่างง่ายดายและไม่มีค่าส่งคืนการใช้Taskจะกลายเป็นรายละเอียดการใช้งาน
Mike Strobel

@stricq ที่กล่าวว่าฉันไม่เห็นด้วยกับคุณ ทางเลือกที่คุณเสนอคือสิ่งที่ฉันเคยทำในอดีตแม้ว่าจะมีเหตุผลที่แตกต่างกัน "ฉันต้องการหลีกเลี่ยงการหยุดทำงานของแอปเมื่อนักพัฒนาไม่สามารถสังเกตเห็นความผิดพลาดTask" ไม่เหมือนกับ "ฉันต้องการวิธีง่ายๆในการเริ่มต้นการทำงานเบื้องหลังไม่เคยสังเกตผลลัพธ์ของแอปและฉันไม่สนใจว่าจะใช้กลไกใด ที่จะทำ " บนพื้นฐานที่ว่าผมยืนอยู่เบื้องหลังคำตอบของฉันกับคำถามที่ถูกถาม
Mike Strobel

การทำงานสำหรับกรณีนี้: fire and forgetในASP.NET WebFormsและwindows.close()?
PreguntonCojoneroCabrón

8

เพื่อแก้ไขปัญหาบางอย่างที่จะเกิดขึ้นกับคำตอบของ Mike Strobel:

หากคุณใช้var task = Task.Run(action)และหลังจากกำหนดความต่อเนื่องให้กับงานนั้นคุณจะเสี่ยงต่อการTaskทิ้งข้อยกเว้นก่อนที่คุณจะกำหนดความต่อเนื่องของตัวจัดการข้อยกเว้นให้กับไฟล์Task. ดังนั้นชั้นเรียนด้านล่างควรไม่มีความเสี่ยงนี้:

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

ที่นี่taskไม่ได้ทำงานโดยตรง แต่จะถูกสร้างขึ้นมีการกำหนดความต่อเนื่องและจากนั้นงานจะถูกรันเพื่อกำจัดความเสี่ยงที่งานจะเสร็จสิ้นการดำเนินการ (หรือทิ้งข้อยกเว้นบางส่วน) ก่อนที่จะกำหนดความต่อเนื่อง

Runวิธีการที่นี่ผลตอบแทนต่อเนื่องTaskดังนั้นผมสามารถที่จะเขียนการทดสอบหน่วยการทำให้แน่ใจว่าการดำเนินการเสร็จสมบูรณ์แล้ว คุณสามารถเพิกเฉยได้อย่างปลอดภัยในการใช้งานของคุณ


มันเป็นความตั้งใจที่คุณไม่ได้ทำให้มันเป็นคลาสคงที่?
Deantwo

คลาสนี้ใช้อินเทอร์เฟซ IAsyncManager ดังนั้นจึงไม่สามารถเป็นคลาสแบบคงที่ได้
Mert Akcakaya

ฉันไม่คิดว่าสถานการณ์นี้กำลังจัดการอยู่ (ข้อบกพร่องของงานก่อนที่จะเพิ่มความต่อเนื่อง) มีความสำคัญ หากสิ่งนี้เกิดขึ้นสถานะที่ผิดพลาดและข้อยกเว้นจะถูกเก็บไว้ในงานและจะถูกจัดการโดยความต่อเนื่องเมื่อมีการแนบ ประเด็นสำคัญที่ต้องทำความเข้าใจคือข้อยกเว้นจะไม่เกิดขึ้นจนกว่าจะมีการรอ task / task.Wait () / task.Result / task.GetAwaiter (). GetResult () - ดูdocs.microsoft.com/en-us/dotnet/ มาตรฐาน / โปรแกรมขนาน / … .
Rhys Jones
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.