【问题标题】:When I use CancelAfter(), the Task is still running当我使用 CancelAfter() 时,任务仍在运行
【发布时间】:2019-01-25 22:25:36
【问题描述】:

我想使用 CancellationTokenSource 停止任务。 我的测试如下:

测试 1:使用 Cancel() 成功停止任务。

测试 2:使用 CancelAfter() 并没有停止任务,为什么?

任务动作是:

    static Action testFun = () => {
        Thread.Sleep(4000); // or other a long time operation
        Console.WriteLine("Action is end");
    };

test1 代码:

        CancellationTokenSource source = new CancellationTokenSource(); 
        CancellationToken token = source.Token;
        //Register the cancel action
        token.Register(() =>
        {
            Console.WriteLine("Task is canceled");
        });
        Task task = new Task(testFun, token); 
        task.Start();
        source.Cancel();     
        Console.ReadLine();

输出是:

Task is canceled

Test2 代码:

        CancellationTokenSource source = new CancellationTokenSource(); 
        CancellationToken token = source.Token;
        //Register the cancel action
        token.Register(() =>
        {
            Console.WriteLine("Task is canceled");
        });
        Task task = new Task(testFun, token); 
        task.Start();
        source.CancelAfter(100); // the time 100ms < the task 4000ms
        Console.ReadLine();

输出是:

Task is canceled
Action is end

我的问题是为什么在CancellationTokenSource 上调用CancelAfter() 时任务仍在运行

如何修改 test2 ?谢谢。

【问题讨论】:

  • 答案是:Sleep 方法忽略了 CancellationToken。但是我用其他一些长时间的Funtion()替换了Sleep(),问题还在。我想知道CancelAfter()如何不停止Task。
  • Sleep 方法只不过是调用 Thread.Sleep()
  • 问题在于cancelationToken 具有误导性。一旦任务开始,除非您明确检查cancelationToken,否则您无法取消它。您只能阻止任务甚至开始,而这正是 Cancel 发生的事情
  • 正如预期的那样,任务一旦启动就不会停止,看看我的回答什么是camcelation token背后的哲学......

标签: c# asynchronous task


【解决方案1】:

CancelationToken 有点误导。问题是如果Task 已经启动,除非您明确检查CancelationToken,否则它无法停止,例如CancellationToken.ThrowIfCancellationRequestedCancelationToken 的目的是防止 Task 在它仍在计划时启动。

这在您的示例中有所不同,使用 Cancel 您取消了仍在计划中的任务,但使用 CancelAfter 任务已经启动并且无法再停止它。

首先,CancellationToken。如果您在 continuation 被安排,那么 continuation 委托永远不会 实际运行 - 它被取消了。但是,请注意,令牌不 一旦开始就取消继续。换句话说, CancellationToken 取消对延续的调度,而不是 延续本身。出于这个原因,我认为 CancellationToken 参数具有误导性,我自己从不使用它。

https://blog.stephencleary.com/2015/01/a-tour-of-task-part-7-continuations.html

部分解释了Task 的延续,但Task 本身也是如此,无论如何,延续都会安排新任务。

CancellationTokenSource source = new CancellationTokenSource();
CancellationToken token = source.Token;

//If cancelAfter > taskWaiting - task will be finished
//If canceAfter < taskWaiting - task will be canceled
//If cancelAfter == taskWaiting - unexpected :)
int cancelAfter = 100;
int taskWaiting = 200;          

//If token is canceled before run of the task it won't start at all
//source.Cancel();

//Register the cancel action
token.Register(() =>
{
    Console.WriteLine("Task is canceled");
});

Task.Run(() =>
{
    Console.WriteLine("Action is started");
    Thread.Sleep(taskWaiting);

    if (token.IsCancellationRequested)
        return;             

    Console.WriteLine("Action is finished");
}, token);          

source.CancelAfter(cancelAfter);

【讨论】:

  • 谢谢。您的意思是任务正在运行,除非检查 CancelationToken,否则无法停止?但在一些长期行动中,检查是在行动之后。有没有检查 CancelationToken 的好方法?
  • 是的,Task 一旦启动就不会停止。检查IsCancellationRequestedThrowIfCancellationRequested() 的方式。请注意,异常可以打包在AggregatedException 中,这取决于您使用TPL api 的方式(await 会冒泡原始异常,task.wait 会打包它...)。
  • 如上运行source.Cancel(),确实无法启动任务。但是在任务运行代码span中调用source.Cancel(),可以启动任务然后停止。他们的两种方法停止任务: 1-使用 "thread.Abort()" ,线程是正在运行的任务的线程; 2 调用 source.Cancel() 。这些步骤必须在任务运行代码中运行。示例:'Task.Run(() => { Console.WriteLine("Task start"); source.Cancel(); token.ThrowIfCancellationRequested(); }, token);'
  • 如果您在运行任务之前设置了source.Cancel,那么任务调度程序将抛出TaskCanceledException,但您没有处理它。您可以尝试await task 并用 try-catch 块包装它...... Task.Delay(4000) Evk 建议的情况也是如此......顺便说一句,Thread.Abort 是有害的,应该避免
【解决方案2】:

Cancel() 成功取消了该任务,因为任务甚至没有机会启动。当任务被安排并即将运行时 - 会检查它是否尚未取消。您可以通过像这样修改testFun 来验证它:

static Action testFun = () => {
    Console.WriteLine("start");
    Thread.Sleep(4000); // or other a long time operation
    Console.WriteLine("Action is end");
};

请注意,当您使用 Cancel 时 - 控制台没有“开始”输出,因此尚未启动任务。

当您使用 CancelAfter 引入延迟时,或者只是这样做:

Task task = new Task(testFun, token);
task.Start();
Thread.Sleep(10); // < small delay
source.Cancel();

任务有机会启动,启动后 - 取消令牌没有任何效果,因为testFunc 的正文中没有任何内容检查令牌是否已被取消。如果没有任务代码的合作,任务不可能在执行过程中被神奇​​地取消。

合作可以是这样的(虽然通常不建议以这种方式使用Task.Delay):

static Action<CancellationToken> testFun = (CancellationToken ct) => {
    Console.WriteLine("start");
    Task.Delay(4000, ct).Wait();
    Console.WriteLine("Action is end");
};

然后当你开始任务时 - 你在那里传递取消令牌:

Task task = new Task(() => testFun(token), token);

现在有合作了——Task.Delay 会注意到令牌被取消,并会取消Task.Delay 操作,这反过来会取消你的任务(通过抛出OperationCanceledException)。

使用 async\await 也可以做到这一点:

static async Task TestFun(CancellationToken ct) {
    Console.WriteLine("start");
    await Task.Delay(4000, ct);
    Console.WriteLine("Action is end");
}

Task task = TestFun(token);
// no need for task.Start() here - task is already started
source.CancelAfter(100);
Console.ReadLine();

【讨论】:

  • 关注问题:Task.Delay 定义了在时间跨度之后运行的任务,所以它可以工作。我的目的是运行一个类似 http 请求的任务。
  • 我替换了 Task.Delay 'await Task.Run(() => { WebRequest request = WebRequest.Create(url); WebResponse response = request.GetResponse(); Console.WriteLine("Response是结束"); }, ct); // 等待 Task.Delay(0,ct);'当 url 超时时(取消操作运行),但任务没有停止。过了一会儿,“响应结束”输出。
  • @longt well web 请求不同。您可以使用WebRequest.Timeout(如果超时后没有收到响应,GetResponse 将抛出异常),或者使用此处描述的技术:stackoverflow.com/q/19211972/5311735
  • 我知道WebRequest.Timeout,我只是在Task中感到困惑。这个问题已经解决。当任务运行时,创建一个新任务检查 CancellationToken ,当取消事件发生时,使用 Thread.Abort() 停止正在运行的任务。
  • “使用 Thread.Abort() 停止正在运行的任务”永远不是解决方案。
【解决方案3】:

核心问题是Task一旦启动就不会停止。

必须检查 IsCancellationRequested。

有一种方法可以解决这个问题:

任务运行时,使用Thread.CurrentThread获取线程,新建Task监听正在运行的任务的CancellationToken。在监听函数中:监听cancel的状态,如果cancel事件为ture,调用thread.Abort()。使用 Abort() 是一种不安全的方法,但它可以停止任务。

代码如下:

    static void Main(string[] args)
    {
        // If the task running used 3000ms, stop.
        int timeOut = 3000;
        CancellationTokenSource source = new CancellationTokenSource();
        source.CancelAfter(timeOut);
        CancellationToken token = source.Token;
        token.Register(() =>
        {
            Console.WriteLine("Task Is TimeOut!!!!! Stop");
        });
        //start the task:
        Task.Run(() =>
        {
            Console.WriteLine("Task start");
            Thread thread = Thread.CurrentThread;
            //create a new task listening the token;
            Task.Factory.StartNew(() =>
            {
                Console.WriteLine("Listening start");
                while (!token.IsCancellationRequested)
                {
                    Console.WriteLine("Listening...");
                    Thread.Sleep(800);
                }
                Console.WriteLine("Listening End");
                thread.Abort();
            }, token);

            Stopwatch time = Stopwatch.StartNew();
            #region A long time operation:;
            for (int i = 0; i < 10; i++)
            {
                Console.WriteLine("run...");
                Thread.Sleep(100);
            }
            #endregion
            time.Stop();
            Console.WriteLine("Task end. cost:{0}", time.ElapsedMilliseconds);
            source.Cancel();
            Console.WriteLine("Task End");
            token.ThrowIfCancellationRequested();
        }, token);
        Console.ReadLine();
    }

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2021-10-18
    • 2021-10-07
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多