Lesson 3.11.2 showed that only parallelism makes computation faster. The Task Parallel Library (TPL) comes with ready-made tools for it:
Parallel.For and Parallel.ForEach for loops, PLINQ for queries. They spread the work across the cores, collect exceptions and
can be canceled. What they do not do: decide whether it is worth it. In this lesson you count the appointments per week over three years in
four ways and measure. One version is ten times slower than the simple loop, one is eight times faster, and PLINQ is slower than
no parallelism at all.
Parallel.For and Parallel.ForEach
Parallel.For(0, n, i => …) and Parallel.ForEach(items, item => …) look like loops, but they run the body on several threads of the
pool at the same time. The calling thread works along and returns only when all iterations are done. There are three things you
need to know.
The order is open. Iteration 7 can finish before iteration 3. The body therefore must not depend on what another iteration has already done.
Shared state is the problem. If several iterations write to the same variable or collection, they get in each other's way. A
Dictionary does not tolerate that; at best it throws an exception, at worst it returns wrong numbers. The two ways out are a
lock (lesson 3.11.4), which forces the iterations back into a line, or per-task state: each task counts for itself, and only at the end
is everything merged. Parallel.ForEach has an overload with three parts for this:
localInitcreates the state of a task, for example aDictionaryof its own,- the body receives this state and passes it on,
localFinallymerges it at the end of a task, once per task instead of once per element.
The partitioning matters too. If the work per element is small, distributing costs more than computing. Partitioner.Create(0, n)
cuts a range into chunks, and each task gets a whole chunk instead of single elements. For large amounts of small work this is
almost always the right way.