What .NET 4.5 (or earlier) higher-level constructs

2019-03-25 04:47发布

问题:

Delegates are a few of the objects that make threading easier in .NET reference. They can be used to asynchronously invoke a method. What other objects exist in framework 4.5 (or earlier) that make the use of threads easier or less error prone?

What are the other abstractions make concurrency and multithreading easier?

Note: This question updates this.

回答1:

I tend to answer a lot of questions related to multithreading and I often see the same basic question asked in various different ways. I will present the most common problems as I have seen them over the years and explain how the newer technologies have made solving these problems easier.

Closing over the loop variable

This is not a problem specific to threading, but the use of threading definitely magnifies the problem. C# 5.0 fixes this problem for the foreach loop by creating a new variable for each iteration. You will no longer have to create a special variable for lambda expression closures. Unfortunately, the for loop will still need to be handle with a special capturing variable.

Waiting for asynchronous tasks to complete

.NET 4.0 introduced the CountdownEvent class which encapsulates a lot of the logic required to wait for the completion of many tasks. Most junior developers used Thread.Join calls or a single WaitHandle.WaitAll call. Both of these have scalability problems. The old pattern was to use a single ManualResetEvent and signal it when a counter reached zero. The counter was updated using the Interlocked class. CountdownEvent makes this pattern much easier. Just remember to treat your main as a worker as well to avoid that subtle race condition that can occur if one worker finishes before all workers have been queued.

.NET 4.0 also introduced the Task class which can have child tasks chained off of it via TaskCreationOptions.AttachedToParent. If you call Task.Wait on a parent it will wait for all child tasks to complete as well.

Producer-Consumer

.NET 4.0 introduced the BlockingCollection class which acts like a normal queue except that it can block when the collection is empty. You can queue an object by calling Add and dequeue an object by calling Take. Take blocks until an item is available. This simplifies producer-consumer logic considerably. It used to be the case that developers were trying to write their own blocking queue class. But, if you do not know what you are doing then you can really screw it up...bad. In fact, for the longest time Microsoft had a blocking queue example in the MSDN documentation that was itself badly broken. Fortunately, it has since been removed.

Updating UI with worker thread progress

The introduction of BackgroundWorker made spinning off a background task from a WinForm application a lot easier for novice developers. The main benefit is that you can call ReportProgress from within the DoWork event handler and the ProgressChanged event handlers will be automatically marshaled onto the UI thread. Of course, anyone that tracks my answers on SO knows how I feel about marshaling operations (via Invoke or the like) as a solution for updating the UI with simple progress information. I rip on it all the time because it is generally a terrible approach. BackgroundWorker still forces the developer into a push model (via marshaling operations in the background), but at least it does all of this behind the scenes.

The inelegance of Invoke

We all know that a UI element can only be accessed from the UI thread. This generally meant that a developer had to use marshaling operations via ISynchronizeInvoke, DispatcherObject, or SynchronizationContext to transfer control back to the UI thread. But lets face it. These marshaling operations look ugly. Task.ContinueWith made this a little more elegant, but the real glory goes to await as part of C# 5's new asynchronous programming model. await can be used to wait for a Task to complete in such a manner that flow control is temporarily interrupted while the task is running and then returned at that very spot in the right synchronization context. There is nothing more elegant and satisfying than using await as a replacement for all those Invoke calls.

Parallel programming

I often see questions asking how things can happen in parallel. The old way was to create a few threads or use the ThreadPool. .NET 4.0 gave use the TPL and PLINQ. The Parallel class is a great way to get the iterations of a loop going in parallel. And PLINQ's AsParallel is a different side of the same coin for plain old LINQ. These new TPL features greatly simplify this category of multithreaded programming.

.NET 4.5 introduces the TPL Data Flow library. It is intended to make elegant an otherwise complex parallel programming problem. It abstracts classes into blocks. They can be target blocks or source blocks. Data can flow from one block to another. There are many different blocks including BufferBlock<T>, BroadcastBlock<T>, ActionBlock<T>, etc. that all do different things. And, of course, the whole library will be optimized for use with the new async and await keywords. It is an exciting new set of classes that I think will slowly catch on.

Graceful termination

How do you get a thread to stop? I see this question a lot. The easiest way is to call Thread.Abort, but we all know the perils of doing this...I hope. There are many different ways to do this safely. .NET 4.0 introduced a more unified concept called cancellation via CancellationToken and CancellationTokenSource. Background tasks can poll IsCancellationRequested or just call ThrowIfCancellationRequested at safe points to gracefully interrupt whatever work they were doing. Other threads can call Cancel to request cancellation.



回答2:

Well let's see here:

  1. The ThreadPool class - kinda old, but still reliable for a simple producer-consumer pattern.
  2. BackgoundWorker (.NET 2.0+) - another old-school construct, providing useful features for executing tasks in the background in GUI applications.
  3. Timers - useful for executing code at specified intervals using a background thread.
  4. The Task class (.NET 4.0+) - threading abstractions that run on the underlying thread pool and provide many useful features like exception marshaling and scheduling. Useful for the so-called "task parallelism" pattern.
  5. Parallel.For, Parallel.ForEach (.NET 4.0+) - good for executing the same operation over a set of data in parallel. Useful for the so-called "data parallelism" pattern.
  6. Parallel.Invoke (.NET 4.0+) - a further abstraction over Tasks. Simply fires off several pieces of code (methods, lambdas) in parallel.
  7. Concurrent collections (.NET 4.0+) - all you need to pass or share data between threads in an efficient and thread-safe manner.


回答3:

Without a doubt, getting to grips with the new Tpl DataFlow library (included in .net 4.5) will give you the biggest boost in terms of concurrent development.

If you're serious about highly concurrent apps, spend a day or two familiarizing yourself with DataFlow. It's seriously good.



回答4:

The Task and Task<T>, but they've been here since .NET 4. async does not necessarily work with threads, see Jon's video from Øredev for a very good explanation.