วิธีการยกเลิกงานในรอ?


164

ฉันกำลังเล่นกับงาน Windows 8 WinRT เหล่านี้และฉันพยายามยกเลิกงานโดยใช้วิธีการด้านล่างและทำงานได้ในบางจุด มีการเรียกใช้เมธอด CancelNotification ซึ่งทำให้คุณคิดว่างานถูกยกเลิก แต่ในพื้นหลังที่งานยังคงทำงานอยู่หลังจากนั้นเสร็จสถานะของงานจะเสร็จสมบูรณ์เสมอและไม่เคยถูกยกเลิก มีวิธีหยุดงานอย่างสมบูรณ์เมื่อมีการยกเลิกหรือไม่?

private async void TryTask()
{
    CancellationTokenSource source = new CancellationTokenSource();
    source.Token.Register(CancelNotification);
    source.CancelAfter(TimeSpan.FromSeconds(1));
    var task = Task<int>.Factory.StartNew(() => slowFunc(1, 2), source.Token);

    await task;            

    if (task.IsCompleted)
    {
        MessageDialog md = new MessageDialog(task.Result.ToString());
        await md.ShowAsync();
    }
    else
    {
        MessageDialog md = new MessageDialog("Uncompleted");
        await md.ShowAsync();
    }
}

private int slowFunc(int a, int b)
{
    string someString = string.Empty;
    for (int i = 0; i < 200000; i++)
    {
        someString += "a";
    }

    return a + b;
}

private void CancelNotification()
{
}

เพิ่งพบบทความนี้ซึ่งช่วยให้ฉันเข้าใจวิธีต่างๆในการยกเลิก
Uwe Keim

คำตอบ:


239

อ่านเกี่ยวกับการยกเลิก (ซึ่งถูกนำมาใช้ใน. NET 4.0 และส่วนใหญ่จะไม่เปลี่ยนแปลงตั้งแต่นั้นมา) และรูปแบบอะซิงโครนัสตามงานซึ่งให้แนวทางเกี่ยวกับวิธีการใช้CancellationTokenกับasyncวิธีการ

เพื่อสรุปคุณส่งผ่านCancellationTokenเข้าไปในแต่ละวิธีที่สนับสนุนการยกเลิกและวิธีนั้นจะต้องตรวจสอบเป็นระยะ

private async Task TryTask()
{
  CancellationTokenSource source = new CancellationTokenSource();
  source.CancelAfter(TimeSpan.FromSeconds(1));
  Task<int> task = Task.Run(() => slowFunc(1, 2, source.Token), source.Token);

  // (A canceled task will raise an exception when awaited).
  await task;
}

private int slowFunc(int a, int b, CancellationToken cancellationToken)
{
  string someString = string.Empty;
  for (int i = 0; i < 200000; i++)
  {
    someString += "a";
    if (i % 1000 == 0)
      cancellationToken.ThrowIfCancellationRequested();
  }

  return a + b;
}

2
ว้าวข้อมูลที่ดี! ทำงานได้อย่างสมบูรณ์ตอนนี้ฉันต้องหาวิธีจัดการข้อยกเว้นในวิธีการแบบอะซิงก์ ขอบคุณคน! ฉันจะอ่านสิ่งที่คุณแนะนำ
คาร์โล

8
ไม่ได้วิธีการซิงโครนัสที่ใช้เวลานานส่วนใหญ่มีวิธีการยกเลิกบางวิธี - บางครั้งโดยการปิดทรัพยากรพื้นฐานหรือเรียกวิธีอื่น CancellationTokenมี hooks ทั้งหมดที่จำเป็นในการทำงานร่วมกับระบบการยกเลิกที่กำหนดเอง แต่ไม่มีสิ่งใดที่สามารถยกเลิกวิธีที่ไม่สามารถยกเลิกได้
Stephen Cleary

1
ฉันเข้าใจแล้ว ดังนั้นวิธีที่ดีที่สุดในการจับ ProcessCancelledException คือการใส่ 'รอ' ในการลอง / จับ บางครั้งฉันได้รับ AggregatedException และไม่สามารถจัดการได้
คาร์โล

3
ขวา. ผมขอแนะนำว่าคุณไม่เคยใช้WaitหรือResultในasyncวิธีการ; คุณควรใช้awaitแทนซึ่งยกเลิกข้อยกเว้นได้อย่างถูกต้อง
Stephen Cleary

11
แค่อยากรู้อยากเห็นมีเหตุผลใดที่ไม่มีตัวอย่างใช้CancellationToken.IsCancellationRequestedและแทนที่จะแนะนำให้โยนข้อยกเว้น?
James M

41

หรือเพื่อหลีกเลี่ยงการแก้ไขslowFunc(เช่นคุณไม่มีสิทธิ์เข้าถึงซอร์สโค้ด):

var source = new CancellationTokenSource(); //original code
source.Token.Register(CancelNotification); //original code
source.CancelAfter(TimeSpan.FromSeconds(1)); //original code
var completionSource = new TaskCompletionSource<object>(); //New code
source.Token.Register(() => completionSource.TrySetCanceled()); //New code
var task = Task<int>.Factory.StartNew(() => slowFunc(1, 2), source.Token); //original code

//original code: await task;  
await Task.WhenAny(task, completionSource.Task); //New code

นอกจากนี้คุณยังสามารถใช้วิธีการขยายที่ดีจากhttps://github.com/StephenCleary/AsyncExและทำให้มันดูเรียบง่ายเหมือน:

await Task.WhenAny(task, source.Token.AsTask());

1
ดูเหมือนยุ่งยากมาก ... เนื่องจากการใช้งานแบบ async-await ฉันไม่คิดว่าสิ่งปลูกสร้างดังกล่าวจะทำให้ซอร์สโค้ดอ่านง่ายขึ้น
Maxim

1
ขอขอบคุณหนึ่งหมายเหตุ - การลงทะเบียนโทเค็นควรถูกกำจัดในภายหลังสิ่งที่สอง - ใช้ConfigureAwaitมิฉะนั้นคุณอาจเจ็บในแอพ UI
astrowalker

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

1
usingที่จริงทั้งหมดก็จะเป็นรังใน
astrowalker

@astrowalker ;-) ใช่คุณพูดถูก ในกรณีนี้นี่เป็นทางออกที่ง่ายกว่ามาก! อย่างไรก็ตามหากคุณต้องการคืน Task.WhenAny โดยตรง (โดยไม่ต้องรอ) คุณต้องมีอย่างอื่น ฉันพูดแบบนี้เพราะฉันเคยมีปัญหาเกี่ยวกับการรีแฟคเตอร์เช่นนี้ก่อนที่ฉันจะใช้ ... รอ จากนั้นฉันก็ลบการรอคอย (และ async บนฟังก์ชั่น) เนื่องจากมันเป็นสิ่งเดียวเท่านั้นโดยไม่สังเกตว่าฉันทำผิดรหัสอย่างสมบูรณ์ ข้อผิดพลาดที่เกิดขึ้นนั้นหายาก ฉันลังเลที่จะใช้ using () ร่วมกับ async / await ฉันรู้สึกว่ารูปแบบการกำจัดไม่สอดคล้องกับสิ่งต่าง ๆ ในแบบอะซิงก์ต่อไป ...
sonatique

15

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

public async Task<Results> ProcessDataAsync(MyData data)
{
    var client = await GetClientAsync();
    await client.UploadDataAsync(data);
    await client.CalculateAsync();
    return await client.GetResultsAsync();
}

หากคุณต้องการสนับสนุนการยกเลิกวิธีที่ง่ายที่สุดก็คือการส่งผ่านโทเค็นและตรวจสอบว่ามีการยกเลิกระหว่างการโทรแต่ละวิธี async (หรือใช้ ContinueWith) หากพวกเขาใช้สายยาวมากแม้ว่าคุณอาจจะรอสักครู่เพื่อยกเลิก ฉันสร้างวิธีการช่วยเหลือเล็กน้อยเพื่อล้มเหลวแทนทันทีที่ยกเลิก

public static class TaskExtensions
{
    public static async Task<T> WaitOrCancel<T>(this Task<T> task, CancellationToken token)
    {
        token.ThrowIfCancellationRequested();
        await Task.WhenAny(task, token.WhenCanceled());
        token.ThrowIfCancellationRequested();

        return await task;
    }

    public static Task WhenCanceled(this CancellationToken cancellationToken)
    {
        var tcs = new TaskCompletionSource<bool>();
        cancellationToken.Register(s => ((TaskCompletionSource<bool>)s).SetResult(true), tcs);
        return tcs.Task;
    }
}

เพื่อใช้งานจากนั้นเพิ่ม.WaitOrCancel(token)การเรียก async:

public async Task<Results> ProcessDataAsync(MyData data, CancellationToken token)
{
    Client client;
    try
    {
        client = await GetClientAsync().WaitOrCancel(token);
        await client.UploadDataAsync(data).WaitOrCancel(token);
        await client.CalculateAsync().WaitOrCancel(token);
        return await client.GetResultsAsync().WaitOrCancel(token);
    }
    catch (OperationCanceledException)
    {
        if (client != null)
            await client.CancelAsync();
        throw;
    }
}

โปรดทราบว่าสิ่งนี้จะไม่หยุดภารกิจที่คุณรอคอยและจะยังคงทำงานต่อไป คุณจะต้องใช้กลไกที่แตกต่างกันที่จะหยุดมันเช่นCancelAsyncการโทรในตัวอย่างหรือดีกว่ายังผ่านเดียวกันCancellationTokenกับTaskเพื่อที่จะสามารถจัดการกับการยกเลิกในที่สุด พยายามที่จะยกเลิกด้ายไม่แนะนำ


1
โปรดทราบว่าในขณะที่การยกเลิกนี้กำลังรองานอยู่ แต่ก็ไม่ได้ยกเลิกงานจริง (เช่นUploadDataAsyncอาจดำเนินการต่อในพื้นหลัง แต่เมื่องานเสร็จสมบูรณ์จะไม่ทำการโทรออกCalculateAsyncเพราะส่วนนั้นหยุดรออยู่) นี่อาจเป็นปัญหาหรือไม่เป็นปัญหาสำหรับคุณโดยเฉพาะถ้าคุณต้องการลองการดำเนินการอีกครั้ง เมื่อผ่านCancellationTokenไปจนสุดจะเป็นตัวเลือกที่ต้องการหากเป็นไปได้
Miral

1
@Miral ที่เป็นจริง แต่มีวิธีการ async จำนวนมากซึ่งไม่ใช้โทเค็นการยกเลิก ใช้ตัวอย่างบริการ WCF ซึ่งเมื่อคุณสร้างลูกค้าด้วยวิธี Async จะไม่รวมโทเค็นการยกเลิก ตามตัวอย่างแสดงให้เห็นและตามที่สตีเฟ่นเคลียร์ก็สังเกตเห็นว่ามันเป็นงานที่ต้องใช้เวลานานในการยกเลิกภารกิจซิงโครนัส
kjbartel

1
นั่นเป็นเหตุผลที่ฉันพูดว่า "เมื่อเป็นไปได้" ส่วนใหญ่ฉันแค่ต้องการให้ข้อแม้นี้ถูกกล่าวถึงเพื่อให้ผู้คนค้นพบคำตอบนี้ในภายหลังจะไม่ได้รับความประทับใจที่ผิด
Miral

@Miral ขอบคุณ ฉันได้อัปเดตเพื่อให้สะท้อนถึงคำเตือนนี้
kjbartel

น่าเสียดายที่วิธีนี้ใช้ไม่ได้กับ 'NetworkStream.WriteAsync'
Zeokat

6

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

Comments.AsAsyncAction().Completed += new AsyncActionCompletedHandler(CommentLoadComplete);

ที่ตัวจัดการเหตุการณ์มีลักษณะเช่นนี้

private void CommentLoadComplete(IAsyncAction sender, AsyncStatus status )
{
    if (status == AsyncStatus.Canceled)
    {
        return;
    }
    CommentsItemsControl.ItemsSource = Comments.Result;
    CommentScrollViewer.ScrollToVerticalOffset(0);
    CommentScrollViewer.Visibility = Visibility.Visible;
    CommentProgressRing.Visibility = Visibility.Collapsed;
}

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

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.