在经过长时间运行之前立即取消操作?

时间:2015-01-14 14:26:34

标签: c# .net-4.0 task-parallel-library cancellationtokensource

我使用AsParallel结合WithDegreeOfParallelism和WithCancellation以下列方式

AsParallel().WithCancellation(cs.Token).WithDegreeOfParallelism(2)

这是我对此的理解。一次只处理两个输入序列。一旦其中一个请求完成,则将处理更多项目。但是,如果取消 请求已启动,而未接收的传入队列中的项目将被处理。基于这种理解,我创建了以下代码。

class Employee
    {
        public int ID { get; set;}
        public string FirstName { get; set;}
        public string LastName { get; set;}
    }

    class Program
    {

        private static List<Employee> _Employees;
        static CancellationTokenSource cs = new CancellationTokenSource();
        static Random rand = new Random();

        static void Main(string[] args)
        {
            _Employees = new List<Employee>() 
            {
                new Employee() { ID = 1, FirstName = "John", LastName = "Doe" },
                new Employee() { ID = 2, FirstName = "Peter", LastName = "Saul" },
                new Employee() { ID = 3, FirstName = "Mike", LastName = "Sue" },
                new Employee() { ID = 4, FirstName = "Catherina", LastName = "Desoza" },
                new Employee() { ID = 5, FirstName = "Paul", LastName = "Smith" },
                new Employee() { ID = 6, FirstName = "Paul2", LastName = "Smith" },
                new Employee() { ID = 7, FirstName = "Paul3", LastName = "Smith" },
                new Employee() { ID = 8, FirstName = "Paul4", LastName = "Smith" },
                new Employee() { ID = 9, FirstName = "Paul5", LastName = "Smith" },
                new Employee() { ID = 10, FirstName = "Paul6", LastName = "Smith" },
                new Employee() { ID = 5, FirstName = "Paul7", LastName = "Smith" }
            };

            try
            {
                var tasks = _Employees.AsParallel().WithCancellation(cs.Token).WithDegreeOfParallelism(2).Select(x => ProcessThisEmployee(x, cs.Token)).ToArray();
                Console.WriteLine("Now waiting");
                Thread.Sleep(1000);
                cs.Cancel();
                Task.WaitAll(tasks);
            }
            catch (AggregateException ae)
            {
                // error handling code
                Console.WriteLine("something bad happened");
            }
            catch (Exception ex)
            {
                // error handling code
                Console.WriteLine("something even worst happened");
            }
            // other stuff
            Console.WriteLine("All Done");
        }

        private static async Task ProcessThisEmployee(Employee x, CancellationToken token)
        {
            if (token.IsCancellationRequested)
            {
                Console.WriteLine(string.Format("ThreadID = {0} -> Employee {1} -> Cancelled", System.Threading.Thread.CurrentThread.ManagedThreadId));
                return;
            }
            int Sleep = rand.Next(800, 2000);
            Console.WriteLine(string.Format("ThreadID = {0} -> Employee {1} -> Sleeping for {2}", System.Threading.Thread.CurrentThread.ManagedThreadId, x.ID, Sleep));
            await TaskEx.Run(() => System.Threading.Thread.Sleep(Sleep));

            Console.WriteLine(string.Format("ThreadID = {0} -> Employee {1} finished", System.Threading.Thread.CurrentThread.ManagedThreadId, x.ID));
        }

    }

运行时输出。

  

ThreadID = 3 - &gt;员工1 - &gt;睡1058年   ThreadID = 1 - &gt;员工7 - &gt;睡1187年   ThreadID = 1 - &gt;员工8 - &gt;睡1296年   ThreadID = 1 - &gt;员工9 - &gt;睡觉1614年   ThreadID = 1 - &gt;员工10 - &gt;睡觉1607年   ThreadID = 1 - &gt;员工5 - &gt;睡到1928年   ThreadID = 3 - &gt;员工2 - &gt;睡觉1487年   ThreadID = 3 - &gt;员工3 - &gt;睡觉1535年   ThreadID = 3 - &gt;员工4 - &gt;睡1265年   ThreadID = 3 - &gt;员工5 - &gt;睡1248年   ThreadID = 3 - &gt;员工6 - &gt;睡觉807
  现在等待   ThreadID = 3 - &gt;员工6完成了   ThreadID = 4 - &gt;员工1完成了   ThreadID = 5 - &gt;员工7完成了   ThreadID = 6 - &gt;员工8完成了   ThreadID = 3 - &gt;员工5完成了   ThreadID = 4 - &gt;员工9完成了   ThreadID = 5 - &gt;员工10完成了   ThreadID = 6 - &gt;员工5完成了   ThreadID = 3 - &gt;员工4完成了   ThreadID = 7 - &gt;员工2完成了   ThreadID = 8 - &gt;员工3完成了   全部完成

以下是我的问题(根据我对事情的理解)。

  1. 我原本以为某些员工根本不会调用ProcessThisEmployee,因为它会被取消,但需要所有员工
  2. 即使调用ProcessThisEmployee方法,它也会经历以下代码路径,但也没有发生

    if ( token.IsCancellationRequested )
    {
        Console.WriteLine(string.Format("ThreadID = {0} -> Employee {1} -> Cancelled",System.Threading.Thread.CurrentThread.ManagedThreadId));
        return;
    }
    
  3. 然后我改变了ProcessThisEmployee,基本上在Sleep之后移动了token.IsCancellationRequested消息,如下所示。

    private static async Task ProcessThisEmployee(Employee x, CancellationToken token)
    {
    
        int Sleep = rand.Next(800, 2000);
        Console.WriteLine(string.Format("ThreadID = {0} -> Employee {1} -> Sleeping for {2}", System.Threading.Thread.CurrentThread.ManagedThreadId, x.ID, Sleep));
        await TaskEx.Run(() => System.Threading.Thread.Sleep(Sleep));
        if (token.IsCancellationRequested)
        {
            Console.WriteLine(string.Format("ThreadID = {0} -> Employee {1} -> Cancelled", System.Threading.Thread.CurrentThread.ManagedThreadId));
            return;
        }
        Console.WriteLine(string.Format("ThreadID = {0} -> Employee {1} finished", System.Threading.Thread.CurrentThread.ManagedThreadId, x.ID));
    }
    

    现在我得到以下输出。

    ThreadID = 3 -> Employee 1 -> Sleeping for 1330  
    ThreadID = 1 -> Employee 7 -> Sleeping for 1868  
    ThreadID = 3 -> Employee 2 -> Sleeping for 903  
    ThreadID = 3 -> Employee 3 -> Sleeping for 1241  
    ThreadID = 3 -> Employee 4 -> Sleeping for 1367  
    ThreadID = 3 -> Employee 5 -> Sleeping for 1007  
    ThreadID = 3 -> Employee 6 -> Sleeping for 923  
    ThreadID = 1 -> Employee 8 -> Sleeping for 1032  
    ThreadID = 1 -> Employee 9 -> Sleeping for 1948  
    ThreadID = 1 -> Employee 10 -> Sleeping for 1456  
    ThreadID = 1 -> Employee 5 -> Sleeping for 1737  
    Now waiting  
    ThreadID = 5 -> Employee 2 finished  
    ThreadID = 3 -> Employee 6 finished  
    something bad happened  
    All Done  
    

    我的问题是我对这个工作流程的误解。我基本上想在不经过长时间运行的情况下尽快取消操作(在这种情况下睡眠只是一个例子,但它可能是非常昂贵的东西)

1 个答案:

答案 0 :(得分:2)

该代码存在一些问题:

1。)ToArray()具体化序列,即只有在源序列的所有输入都经过Select(...)之后才会返回。

由于您之后致电cs.Cancel(),因此在token.IsCancellationRequested

开头不会立即触发ProcessThisEmployee

2。)WithDegreeOfParallelism(2).Select(x => ProcessThisEmployee(x, cs.Token))看起来不错,但实际上并没有真正做你想做的事情,因为ProcessThisEmployee是一个异步方法,它会在第一次返回或第一次返回时立即返回等待到达。

您可能想要做的是执行只有2度并行度的长时间运行的ProcessThisEmployee方法。你实际做的是创建一堆只有2度并行度的Tasks。在此之后,任务本身都会同时运行。

我不知道如何根据您的具体情况解决这个问题,因为我不了解情况。但也许这对你有所帮助。


更新以回复您的评论:我正在做ToArray和ProcessThisEmployee是一种异步方法,因为此代码将成为库的一部分,可以从WPF应用程序中使用。最终用户可能希望在UI上提供更新,因此我不希望在操作完成之前阻止(john smith)

不要为那些本质上不异步的事情编写异步包装,即主要是文件,网络或数据库访问。如果使用库的开发人员想要在异步上下文中调用某些东西,他仍然可以执行await Task.Run(...)。有关此问题的详细信息,请参阅此文章,了解您是否should expose asynchronous wrappers for synchronous methods

在我看来,如果你已经有一个有效的LINQ查询,PLINQ最有用,并希望加快速度,因为该查询适合并行处理。

在您的情况下,最简单的方法可能是使用2个线程的工作队列。我很确定网上有这些例子。