Tài liệu hạn chế xem trước, để xem đầy đủ mời bạn chọn Tải xuống
1
/ 98 trang
THÔNG TIN TÀI LIỆU
Thông tin cơ bản
Định dạng
Số trang
98
Dung lượng
1,75 MB
Nội dung
ptg Running LINQ Queries in Parallel 737 Listing 18.17: Canceling a Parallel Loop using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; public class Program { public static List<string> ParallelEncrypt( List<string> data, CancellationToken cancellationToken) { (item) => Encrypt(item)).ToList(); } public static void Main() { List<string> data = Utility.GetData(1000000).ToList(); Console.WriteLine("Push ENTER to exit."); Task task = Task.Factory.StartNew(() => { data = ParallelEncrypt(data, cts.Token); } , cts.Token); // Wait for the user's input Console.Read(); Console.Write(stars); // } return data.AsParallel(). WithCancellation( cancellationToken).Select( CancellationTokenSource cts = new CancellationTokenSource(); cts.Cancel(); try{task.Wait();} catch (AggregateException){} } OUTPUT 18.8: ERROR: The operation was canceled. From the Library of Wow! eBook ptg Chapter 18: Multithreading738 As with a parallel loop, canceling a PLINQ query requires a Cancella- tionToken, which is available on a CancellationTokenSource.Token prop- erty. However, rather than overloading every PLINQ query to support the cancellation token, the ParallelQuery<T> object returned by IEnumera- ble’s AsParallel() method includes a WithCancellation() extension method that simply takes a CancellationToken. As a result, calling Can- cel() on the CancellationTokenSource object will request the parallel query to cancel—because it checks the IsCancellationRequested property on the CancellationToken. As mentioned, canceling a PLINQ query will throw an exception in place of returning the complete result. Therefore, all canceled PLINQ que- ries will need to be wrapped by try{…}/catch(OperationCanceledExcep- tion){…} blocks to avoid an unhandled exception. Alternatively, as shown in Listing 18.17, pass the CancellationToken to both ParallelEncrypt() and as a second parameter on StartNew(). This will cause task.Wait() to throw an AggregateException whose InnerException property will be set to a TaskCanceledException. Multithreading before .NET Framework 4 TPL is a fantastic library covering a multitude of multithreading patterns with extensibility points to handle even more. However, there is one sig- nificant drawback to TPL: It is available only for the .NET Framework 4 or for use with the Rx library in .NET 3.5. In this section, we cover multi- threading technology before TPL. Asynchronous Operations with System.Threading.Thread Listing 18.18 (with Output 18.9) provides an example. Like TPL, there is a fundamental type, System.Threading.Thread, which is used to control an asynchronous operation. Like System.Threading.Tasks.Task in TPL, Thread includes a Start method and a wait equivalent, Join(). Listing 18.18: Starting a Method Using System.Threading.Thread using System; public class RunningASeparateThread using System.Threading; From the Library of Wow! eBook ptg Multithreading before .NET Framework 4 739 { public const int Repetitions = 1000; public static void Main() { for (int count = 0; count < Repetitions; count++) { Console.Write('-'); } } public static void DoWork() { for (int count = 0; count < Repetitions; count++) { Console.Write('.'); } } } ThreadStart threadStart = DoWork; Thread thread = new Thread(threadStart); thread.Start(); thread.Join(); OUTPUT 18.9: From the Library of Wow! eBook ptg Chapter 18: Multithreading740 Like the output of Listing 18.9, which used TPL, Listing 18.18’s code (see Output 18.9) intersperses . and – in the output. The code that is to exe- cute in a new thread appears in the DoWork() method. The DoWork() method outputs a . during each iteration within a loop. Besides the fact that it contains code for starting another thread, the Main() method is vir- tually identical in structure to DoWork(), except that it displays The resultant output is due to a series of dashes until the thread context switches, at which time the program displays periods until the next thread switch, and so on. 2 In order for code to run under the context of a different thread, you need a delegate of type System.Threading.ThreadStart or System. Threading.ParameterizedThreadStart (the latter allows for a single parameter of type object), identifying the code to execute. Given a Thread instance created using the thread-start delegate constructor, you can start the thread executing with a call to thread.Start(). (Listing 18.18 shows the ThreadStart explicitly to identify the delegate type. In general, DoWork could be passed directly to the thread constructor using C# 2.0’s delegate inference.) Starting the thread simply involves a call to Thread.Start(). As soon as the DoWork() method begins execution, the call to Thread. Start() returns and executes the for loop in the Main() method. The threads are now independent and neither waits for the other. The output from Listing 18.18 and Listing 18.19 will intermingle the output of each thread, instead of creating a series of . followed by Thread Management Threads include a number of methods and properties for managing their execution. • Join(): Once threads are started, you can cause a “wait for comple- tion” with a call to thread.Join(). The calling thread will wait until the thread instance terminates. The Join() method is over- loaded to take either an int or a TimeSpan to support a maximum time to wait for thread completion before continuing execution. 2. As mentioned earlier, it is possible to increase the chances of a thread context switch by using Start /low /b <program.exe> to execute the program. From the Library of Wow! eBook ptg Multithreading before .NET Framework 4 741 • IsBackground: Another thread configuration option is the thread.IsBackGround property. By default, a thread is a foreground thread, meaning the process will not terminate until the thread com- pletes. In contrast, setting the IsBackground property to true will allow process execution to terminate prior to a thread’s completion. • Priority: When using the Join() method, you can increase or decrease the thread’s priority by setting the Priority to a new ThreadPriority enum value (Lowest, BelowNormal, Normal, Above- Normal, or Highest). • ThreadState: A thread’s state is accessible through the ThreadState property, a more precise reflection of the Boolean IsAlive property. The ThreadState enum flag values are Aborted, AbortRequested, Background, Running, Stopped, StopRequested, Suspended, Suspend- Requested, Unstarted, and WaitSleepJoin. The flag names indicate activities that may occur on a thread. Two noteworthy methods are Thread.Sleep() and Abort(). • Thread.Sleep(): Thread.Sleep() is a static method that pauses the current thread for a period. A single parameter (in milliseconds, or a TimeSpan) specifies how long the active thread waits before continu- ing execution. This enables switching to a different thread for a spe- cific period. This method is not for accurate timing. Returns can occur hundreds of milliseconds before or after the specified time. • Abort(): A thread’s Abort() method causes a ThreadAbortException to be thrown within the target thread at whatever location the thread is executing when Abort() is invoked. As already detailed, aborting a thread introduces uncertainty into the thread’s behavior and could cause data integrity and resource cleanup problems. Developers should consider the Abort() method to be a last resort. Instead, they should rely on threads running to completion and/or signaling them to escape out of whatever code is running via some with shared state. From this list of Thread members, only Join() and ThreadState have Task equivalents. For the most part, this is because there are generally preferable From the Library of Wow! eBook ptg Chapter 18: Multithreading742 equivalents or the behavior of the member is undesirable as a best practice. For example, aborting a thread may threaten data integrity or inadequate resource de-allocation, as mentioned earlier in the chapter. Therefore, given the .NET Framework 4, developers should generally avoid these members in favor of their task equivalents or alternative patterns entirely. In summary, the general priority for selecting from the asynchronous class options is Task, ThreadPool, and Thread. In other words, use TPL, but if that doesn’t fit, use ThreadPool; if that still doesn’t suffice, use Thread. One particular Thread member that is likely to crop up more frequently because there is no Task or ThreadPool equivalent is Thread.Sleep(). Although, if it doesn’t introduce too much unnecessary complexity, con- sider using a timer in place of Sleep(). Thread Pooling Regardless of the number of processors, an excess of threads negatively affects performance. To efficiently manage thread creation, TPL makes extensive use of CLR’s thread pool, System.Threading.ThreadPool. Most importantly, the thread pool dynamically determines when to use existing threads rather than creating new ones. Fortunately, the .NET 3.5 Frame- work includes a version of the System.Threading.ThreadPool, so it is available even without TPL. Accessing threads in ThreadPool is similar to explicit use of the Thread class except that the invocation is via a static method, QueueUser- WorkItem() (see Listing 18.19). Listing 18.19: Using ThreadPool Instead of Instantiating Threads Explicitly using System; using System.Threading; public class Program { public const int Repetitions = 1000; public static void Main() { for (int count = 0; count < Repetitions; count++) { ThreadPool.QueueUserWorkItem(DoWork, '.'); From the Library of Wow! eBook ptg Multithreading before .NET Framework 4 743 Console.Write('-'); } // Pause until the thread completes } { for (int count = 0; count < Repetitions; count++) { Console.Write(state); } } } The output is similar to Output 18.9, an intermingling of . and This pro- vides more-efficient execution on single- and multiprocessor computers. The efficiency is achieved by reusing threads over and over, rather than reconstructing them for every asynchronous call. Unfortunately, thread pool use is not without its pitfalls. Activities such as I/O operations and other framework methods that internally use the thread pool can consume threads as well. Consuming all threads within the pool can delay execution and, in extreme cases, cause a dead- lock. Similarly, if the asynchronous code will take a long time to execute, then it is inappropriate to consume a shared thread from the thread pool and instead favor explicit Thread instantiation (use TaskCreationOp- tions.LongRunning given TPL as mentioned earlier). Unfortunately, another disadvantage with the thread pool is that, unlike either Thread or Task, the ThreadPool API does not return a han- dle to the thread or task itself. This prevents the calling thread from con- trolling it with the thread management functions described earlier in the chapter. Just monitoring state is not available without explicitly adding a custom implementation. Assuming these deficiencies are not critical, developers should consider using the thread pool over explicit thread creation because of it increased efficiency—at least prior to .NET Frame- work 4 and TPL; the fact that TPL uses the thread pool internally indi- cates the significance of using it for the majority of multithreading scenarios. Thread.Sleep(1000); public static void DoWork(object state) From the Library of Wow! eBook ptg Chapter 18: Multithreading744 Unhandled Exceptions on the AppDomain To catch all exceptions from a thread (for which appropriate handling is known), you surround the root code block with a try/catch/finally block, just as you would for all code within Main(). However, what happens if a third-party component creates an alternate thread and throws an unhan- dled exception from that thread? Similarly, what if queued work on the thread pool throws an exception? A try/catch block in Main() will not catch an exception on an alternate thread. Furthermore, without access to any “handle” that invoked the thread (such as a Task) there is no way to catch any exceptions that it might throw. Even if there was, the code could never appropriately recover from all possible exceptions and continue exe- cuting (in fact, this is why in .NET 4.0 exceptions such as System.Stack- OverflowException, for example, will not be caught and instead will tear down the application). The general unhandled-exceptions guideline is for the program to shut down and restart in a clean state instead of behaving erratically or hanging because of an invalid state. However, instead of crashing suddenly or ignoring an unhandled exception entirely if it occurs on an alternate thread, it is often desirable to save any working data and/or log the exception for error reporting and future debugging. This requires a mechanism to register for notifications of unhandled exceptions. Registering for unhandled exceptions on the main application domain occurs via an application domain’s UnhandledException event. Listing 18.20 demonstrates that process, and Output 18.10 shows the results. Listing 18.20: Registering for Unhandled Exceptions using System; using System.Threading; public class Program { public static void Main() { try { // Register a callback to // receive notifications // of any unhandled exception. AppDomain.CurrentDomain.UnhandledException += OnUnhandledException; From the Library of Wow! eBook ptg Unhandled Exceptions on the AppDomain 745 ThreadPool.QueueUserWorkItem( state => { throw new Exception( "Arbitrary Exception"); }); // // Wait for the unhandled exception to fire // ADVANCED: Use ManualResetEvent to avoid // timing dependent code. Thread.Sleep(10000); Console.WriteLine("Still running "); } finally { Console.WriteLine("Exiting "); } } public static void ThrowException() { throw new ApplicationException( "Arbitrary exception"); } } static void OnUnhandledException( object sender, UnhandledExceptionEventArgs eventArgs) { Exception exception = (Exception)eventArgs.ExceptionObject; Console.WriteLine("ERROR ({0}):{1} > {2}", exception.GetType().Name, exception.Message, exception.InnerException.Message); } OUTPUT 18.10: Still running Exiting ERROR (AggregateException):One or more errors occurred. > Arbitrary Exception From the Library of Wow! eBook ptg Chapter 18: Multithreading746 The UnhandledException callback will fire for all unhandled exceptions on threads within the application domain, including the main thread. This is a notification mechanism, not a mechanism to catch and process exceptions so that the application can continue. After the event, the application will exit. In fact, the unhandled exception will cause the Windows Error dialog to display (Dr. Watson). And for console applications, the exception will appear on the console. Astute readers will note that in Listing 18.20 we use ThreadPool rather than Task. This is because of the likelihood that the garbage collector will not have executed on Task before the application begins to shut down and any exceptions within the finalization will be suppressed rather than going unhandled. The likelihood of this case in most programs is generally low, but the best practice to avoid significant unhandled exceptions during application exit is to support task cancellation to cancel the task and wait for it to exit before shutting down the application. SUMMARY This chapter delved into the details surrounding the creation and manipu- lation of threads using the .NET Framework 4-introduced Task Parallel Library or TPL. This library includes new APIs for executing for and foreach loops such that iterations can potentially run in parallel. Underly- ing TPL is a new fundamental threading class, System.Threading. Tasks.Task, the basic threading unit on which all of TPL is based. It pro- vides the standard multithreaded programming and monitoring activities and keeps them relatively simple. Given that Task forms the basis for par- allel loops (Parallel.For() and Parallel.ForEach()), PLINQ, and more, it is clear that Task and its peer classes also enable a multitude of more complex threading scenarios—including unhandled exception handling and Task chaining/notifications—via Task.ContinueWith<T>. In addition, the chapter demonstrated Parallel LINQ (PLINQ) in which a single extension method, AsParallel(), transforms all further LINQ queries to run in parallel. The elegance and simplicity with which this fits into the framework is superb. From the Library of Wow! eBook [...]... support), except that the lock keyword does not use it and Mutexes can be named so that they sup- port synchronization across multiple processes Using the Mutex class, you can synchronize access to a file or some other cross-process resource Since Mutex is a cross-process resource, NET 2.0 added support to allow for setting the access control via a System.Security.AccessControl.MutexSecurity object One... and exiting the synchronized block From the Library of Wow! eBook Synchronization 759 Similarly, the code declares _Sync as private so that no synchronization block outside the class can synchronize the same object instance, causing the code to block If the data is public, then the synchronization object may be public so that other classes can synchronize using the same object instance This makes it... Interlock’s Synchronization-Related Methods Method Signature Description public static T CompareExchange( Checks location for the value in comparand If the values are equal, it sets location to value and returns the original data stored in location T location, T value, T comparand ); public static T Exchange( T location, T value ); public static int Decrement( ref int location ); public static int... the data Such methods should internally handle the synchronization In contrast, instance state is not expected to include synchronization Synchronization may significantly decrease performance and increase the chance of a lock contention or deadlock With the exception of classes that are explicitly designed for multithreaded access, programmers sharing objects across multiple threads are expected to handle... execution (or a thread context switch) by the transition of instructions appearing from one column to the other The value of _Count after a particular line has completed appears in the last column In this sample execution, _Count++ executes twice and _Count-occurs once However, the resultant _Count value is 0, not 1 Copying a result back to _Count essentially wipes out any _Count value changes that occurred... as lock(this) (or worse, for the static) discussed in the previous section As a result, it is a best practice to avoid the attribute altogether Declaring Fields as volatile On occasion, the compiler and/or CPU may optimize code in such a way that the instructions do not occur in the exact order they are coded, or some instructions are optimized out Such optimizations are innocuous when code executes... each other to release a synchronization lock For example, Thread 1 requests a lock on _Sync1, and then later requests a lock on _Sync2 before releasing the lock on _Sync1 At the same time, Thread 2 requests a lock on _Sync2, followed by a lock on _Sync1, before releasing the lock on _Sync2 This sets the stage for the deadlock The deadlock actually occurs if both Thread 1 and Thread 2 successfully acquire... single Pulse() call, only one thread (consumer in this case) can enter the ready queue When the producer thread calls Monitor.Exit(), the consumer thread takes the lock (Monitor.Enter() completes) and enters the critical section to begin “consuming” the item Once the consumer processes the waiting item, it calls Exit(), thus allowing the producer (currently blocked with Monitor.Enter()) to produce again... i < _Total; i++) { lock (_Sync) { _Count++; } } task.Wait(); Console.WriteLine("Count = {0}", _Count); } static void Decrement() { for (int i = 0; i < _Total; i++) { lock (_Sync) { _Count ; } From the Library of Wow! eBook 758 Chapter 19: Synchronization and More Multithreading Patterns } } } OUTPUT 19.3: Count = 0 By locking the section of code accessing _Count (using either lock or Monitor), you make... execute than Listing 19.1 does, which demonstrates lock’s relatively slow execution compared to the execution of incrementing and decrementing the count Even when lock is insignificant in comparison with the work it synchronizes, programmers should avoid indiscriminate synchronization in order to avoid the possibility of deadlocks and unnecessary synchronization on multiprocessor computers that could . input Console.Read(); Console.Write(stars); // } return data.AsParallel(). WithCancellation( cancellationToken).Select( CancellationTokenSource cts = new CancellationTokenSource(); cts.Cancel(); . sample execution, _Count++ executes twice and _Count occurs once. However, the resultant _Count value is 0, not 1. Copying a result back to _Count essentially wipes out any _Count value changes. CancellationTokenSource object will request the parallel query to cancel—because it checks the IsCancellationRequested property on the CancellationToken. As mentioned, canceling a PLINQ query will throw an exception