任务继续使用未按我预期工作

6
考虑以下代码。我从一个什么都不做的任务开始,然后使用ContinueWith()来启动10个调用一个增加计数器的方法的任务。
当我运行这个程序时,它打印“0”,表示增量(increment)方法根本没有被调用。我原本期望这个方法被调用10次,因为我调用了10次ContinueWith()。
如果我取消注释“Thread.Sleep(20)”这一行,那么就会按预期打印“10”。
这种情况在发布模式和调试模式下都会发生。我的系统是带有超线程技术(8个逻辑核心)的核2四核处理器,运行Windows 7 x64。
我认为我对Task.ContinueWith()的工作方式存在某种基本误解....
using System;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleApplication4
{
    class Program
    {
        static void Main()
        {
            using (var task = Task.Factory.StartNew(()=>{}))
            {
                for (int i = 0; i < 10; ++i)
                {
                    task.ContinueWith(_=> increment());
                    // Thread.Sleep(20);  // Uncomment to print 10 instead of 0.
                }

                task.Wait();
            }

            // This prints 0 UNLESS you uncomment the sleep above.
            Console.WriteLine(counter); 
        }

        static void increment()
        {
            Interlocked.Increment(ref counter);
        }

        private static int counter;
    }
}

有人能够解释一下这里到底发生了什么吗?

3个回答

9
原因很简单:您在等待已经完成的任务。您真正想要的是等待您在循环中创建的十个任务。
var tasks = new List<Task>();
for (int i = 0; i < 10; ++i)
{
    tasks.Add(task.ContinueWith(_=> increment()));
}

Task.WaitAll(tasks.ToArray());

啊,我的错误在于认为原始任务的Wait()方法会等待附加到它的任何ContinueWith()任务! - Matthew Watson
我真正想做的是按顺序运行多个任务。实际代码使用APM异步从文件读取数据,然后在单独的线程中对数据块进行校验和检查。我只是尝试使用ContinueWith()来实现这一点,但似乎使用不同的方法会更好。事实上,我的先前尝试只是将数据块添加到BlockingCollection<byte[]>中,我的校验和线程通过GetConsumingEnumerable()方法访问该集合。我现在认为这是最好的方法。 - Matthew Watson
@Matthew:嗯,通过Jon的解决方案可以实现按顺序运行它们。但是你之前的方法也很好。那就是我会实现它的方式。 - Daniel Hilgarth

4
当初始任务完成时,你正在打印出该值 - 你没有等待连续发生。换句话说,当初始任务完成时,会发生以下11件事情:
- 启动10个新任务,每个任务都会增加计数器 - 你打印出计数器的值
如果你想的话,你可以有效地将所有这些任务链接在一起:
task = task.ContinueWith(_=> increment());

当原始任务完成后,一个增量器将会被触发。当那个完成后,下一个增量器将会被触发。当那个完成时[...]。当最后一个增量器完成后,您的Wait调用将返回。


你的代码将按顺序执行任务。他的 - 和我的 - 是否并行执行这十个任务? - Daniel Hilgarth
1
@Daniel:可能吧。这取决于默认的调度程序是什么,我猜...即使调度程序是针对线程池的,这些任务也会足够快地完成,以至于我预计在创建新线程之前需要半秒钟的时间。线程编程真有趣,不是吗? :) - Jon Skeet
我知道在这种情况下它不应该有任何影响,但对我来说,从文档中并不清楚。 - Daniel Hilgarth

3
如果你按照以下方式更改主体内容:
        using (var task = Task.Factory.StartNew(() => { }))
        {
            var t = task;
            for (int i = 0; i < 10; ++i)
            {
               t = t.ContinueWith(_ => increment());
                // Thread.Sleep(20);  // Uncomment to print 10 instead of 0.
            }

            t.Wait();
        }

如果你不睡觉,你就会得到10。最主要的变化是 t = t.ContinueWith(...) 并且必须使用t,因为你无法更改 using() 变量。


我想我也创建了一堆任务,但没有处理它们(这可能是我可以忽略的事情...) - Matthew Watson

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接