为什么在调用者线程上发生任务取消?

时间:2014-02-03 13:43:35

标签: c# multithreading cancellationtokensource

我发现任务取消模式存在问题,我想了解为什么要这样做。

考虑这个小程序,其中辅助线程执行异步“长”任务。同时,主要线程通知取消。

该程序是一个更大的简化版本,可能有许多并发线程执行“长任务”。当用户要求取消时,应取消所有正在运行的任务,从而取消CancellationTokenSource集合。

class Program
{
    static MyClass c = new MyClass();

    static void Main(string[] args)
    {
        Console.WriteLine("program=" + Thread.CurrentThread.ManagedThreadId);
        var t = new Thread(Worker);
        t.Start();
        Thread.Sleep(500);
        c.Abort();

        Console.WriteLine("Press any key...");
        Console.ReadKey();
    }

    static void Worker()
    {
        Console.WriteLine("begin worker=" + Thread.CurrentThread.ManagedThreadId);

        try
        {
            bool result = c.Invoker().Result;
            Console.WriteLine("end worker=" + result);
        }
        catch (AggregateException)
        {
            Console.WriteLine("canceled=" + Thread.CurrentThread.ManagedThreadId);
        }
    }


    class MyClass
    {
        private List<CancellationTokenSource> collection = new List<CancellationTokenSource>();

        public async Task<bool> Invoker()
        {
            Console.WriteLine("begin invoker=" + Thread.CurrentThread.ManagedThreadId);

            var cts = new CancellationTokenSource();
            c.collection.Add(cts);

            try
            {
                bool result = await c.MyTask(cts.Token);
                return result;
            }
            finally
            {
                lock (c.collection)
                {
                    Console.WriteLine("removing=" + Thread.CurrentThread.ManagedThreadId);
                    c.collection.RemoveAt(0);
                }
                Console.WriteLine("end invoker");
            }
        }

        private async Task<bool> MyTask(CancellationToken token)
        {
            Console.WriteLine("begin task=" + Thread.CurrentThread.ManagedThreadId);
            await Task.Delay(2000, token);
            Console.WriteLine("end task");
            return true;
        }

        public void Abort()
        {
            lock (this.collection)
            {
                Console.WriteLine("canceling=" + Thread.CurrentThread.ManagedThreadId);
                foreach (var cts in collection) //exception here!
                {
                    cts.Cancel();
                }
                //collection[0].Cancel();
            };
        }

    }
}

尽管锁定了集合访问权限,但访问它的线程与请求取消的线程相同。也就是说,在迭代期间修改集合,并引发异常。

为了更清晰,您可以注释掉整个“foreach”并取消注释最后一条指令,如下所示:

        public void Abort()
        {
            lock (this.collection)
            {
                Console.WriteLine("canceling=" + Thread.CurrentThread.ManagedThreadId);
                //foreach (var cts in collection) //exception here!
                //{
                //    cts.Cancel();
                //}
                collection[0].Cancel();
            };
        }

这样做,没有例外,程序正常终止。但是,看到所涉及的线程的ID很有意思:

program=10
begin worker=11
begin invoker=11
begin task=11
canceling=10
removing=10
end invoker
Press any key...
canceled=11

显然,“finally”主体在调用者线程上运行,但是一旦关闭“Invoker”,该线程就是次要的。

为什么不在辅助线程中执行“finally”块?

2 个答案:

答案 0 :(得分:4)

运行任务的哪个线程是实现细节。如果您使用知道如何在特定线程上运行代码的任务调度程序,那么您只能确定一个问题。与TaskScheduler.FromCurrentSynchronizationContext()类似。这将永远不会在控制台模式应用程序中工作,因为它没有。

因此,要由Task类实现来确定要使用的线程。并且它将寻找需要线程上下文切换的机会,这些是昂贵的。如果它可以选择启动线程池线程来执行代码并等待它完成直接执行代码,那么它总是会选择最后一个选择,它是优越的。

它在您的代码中找到了一个,您在主线程上调用了Abort()方法。其中,通过Task类管道中的许多层(查看“调用堆栈”窗口),找出了如何在同一个线程上调用 finally 块。这当然是件好事。你应该期待的是,你的线程没有其他任何事情可以这样做,它也可能用于执行任务代码。

与使用CancelAfter()相比,现在你的线程适合执行finally块,你会看到finally块在TP线程上执行。

答案 1 :(得分:0)

似乎一旦你在第一个子线程上调用Cancel()await延续就不能再在这个线程上恢复,而是在调用者/父线程上执行。如果在调用后立即添加catch以生成第二个子线程,则可以在TaskCancelationException之后看到父线程执行的代码,

try
{
    bool result = await c.MyTask(cts.Token);
    return result;
}
catch (Exception exception)
{
    Console.WriteLine("catch invoker exception=" + exception.GetType());
    Console.WriteLine("catch invoker=" + Thread.CurrentThread.ManagedThreadId);
    return true;
}

哪个产生,

program=10
begin worker=11
begin invoker=11
begin task=11
canceling=10
catch invoker exception=TaskCanceledException
catch invoker=10      <-- parent thread resuming on child cancellation
removing=10

它在父线程上执行的原因可能是实现细节,原因是产生新线程以恢复执行的性能原因(Hans Passant解释);同样,如果永远不会取消子线程(注释掉c.Abort();),await执行将在子线程而不是父线程上恢复,

program=10
begin worker=11   <-- first child thread
begin invoker=11
begin task=11
Press any key...
end task=12       <-- second child thread resuming on 'await Task.Delay'
removing=12       <-- second child thread resuming on 'await c.MyTask(cts.Token)'
end invoker=12
end worker=True    
end worker=11     <-- back to the first child thread

其中thread 11已经返回其调用方法(返回Worker),在MyTask处切换线程上下文以恢复可能会更加昂贵,而thread 12 }(假定的第二个孩子),刚刚可用于延续,但仅限Invoker方法'结束,其中thread 11位于最初暂停的确切位置。