Tài liệu hạn chế xem trước, để xem đầy đủ mời bạn chọn Tải xuống
1
/ 93 trang
THÔNG TIN TÀI LIỆU
Thông tin cơ bản
Định dạng
Số trang
93
Dung lượng
0,95 MB
Nội dung
N ote Fuzzy logic is defined as a form of algebra that uses the values of true and false to make decisions based on imprecise data. Fuzzy logic is generally attributed to artificial intelligence systems. Summary The Reflection and Type classes go hand in hand when you need to discover type information at runtime. These classes enable you to examine objects, load the objects dynamically at runtime, and even generate code as needed. Chapter 33: C# Threading In This Chapter The multithreading power of the .NET Framework enables you to write very robust multithreaded applications in any .NET language. In this chapter, you learn the ins and outs of threading. The chapter starts with an overview of the different types of threading and how they work in the .NET Framework, and then you learn what you can do with multithreading in your own applications. As you read this chapter, carefully consider the dangers of adding multiple threads to your applications before implementing them, because multithreading is not a trivial concept. Understanding Threading Before you start writing multithreaded applications, you should understand what happens when threads are created, and how the operating system handles threads. When an application executes, a primary thread is created, and the application's scope is based on this thread. An application can create additional threads to perform additional tasks. An example of creating a primary thread would be firing up Microsoft Word. The application execution starts the main thread. Within the Word application, the background printing of a document would be an example of an additional thread being created to handle another task. While you are still interacting with the main thread (the Word document), the system is carrying out your printing request. After the main application thread is killed, all other threads created as a result of that thread are also killed. Consider these two definitions from the Microsoft Foundation Classes Software Development Kit (MFCSDK): • Process: An executing instance of an application • Thread: A path of execution within a process C++ and the MFC have long supported the concept of developing multithreaded applications. Because the core of the Windows operating system is written using these tools, it is important that they support the capability to create threads in which tasks can be assigned and executed. In the early days of Windows 3.1, multitasking did not exist; this concept became a reality in Windows NT 3.5, and NT 4.0, and then Windows 95, 98, 98SE, ME, 2000, and XP. To take advantage of the operating system's features, multithreaded applications became more important. Now, performing more than one task at a time is a necessary feature of an application. Visual Basic 6.0 and earlier compiled down to single-threaded applications, which meant that no matter what was going on, the VB application could only do one thing at a time. In reality, on a single-processor system, it doesn't matter what tool you use to write your application; everything is still happening in a linear process. If you are a C++ developer, you can create new threads and perform a task while something else is going on, but it is really just sharing the same time with everything else that is running on the system. If there is only one processor, only one thing can happen at a time. This concept is called preemptive multitasking. Understanding preemptive multitasking Preemptive multitasking splits the processor time between running tasks, or threads. When a task is running, it is using a time slice. When the time slice has expired for the running task, somewhere around 20 milliseconds, depending on the operating system you are using, it is preempted and another task is given a time slice. The system saves the current context of the preempted task, and when the task is allocated another time slice, the context is restored and the process continues. This loop for a task continues repeatedly until the thread is aborted or the task ends. Preemptive multitasking gives the user the impression that more than one thing is happening at a time. Why do some tasks finish before others, even if you started the one that finished last first? Understanding threading priorities and locking When threads are created, they are assigned a priority either by the programmer or by the operating system. If an application seems to be locking up your system, it has the highest priority, and it is blocking other threads from getting any time slices. Priorities determine what happens, and in what order. Your application might be 90 percent complete with a certain process when suddenly a brand-new thread starts and races ahead of the thread that your application is currently executing, causing that thread to be reassigned to a lower priority. This frequently happens in Windows. Certain tasks take priority over others. Consider the new Windows Media Player. Starting up this process basically causes anything that is running to stop responding until it is completely loaded, including the Media Guide page. One of the biggest dangers facing programmers writing applications that are using multiple threads are locking situations, in which two or more threads attempt to use the same resource. A thread lock occurs when a shared resource is being access by a thread and another thread with the same priority attempts to access that resource. If both threads have the same priority, and the lock is not coded correctly, the system slowly dies, because it cannot release either of the high-priority threads that are running. This can easily happen with multithreaded applications. When you assign thread priorities and are sharing global data, you must lock the context correctly in order for the operating system to handle the time slicing correctly. Understanding symmetrical multiprocessing On a multiprocessor system, more than one task can truly occur at the same time. Because each processor can assign time slices to tasks that are requesting work, you can perform more than one task at a time. When you need to run a processor-intensive long-running thread, such as sorting 10 million records by first name, address, Zip code, middle name, and country, using multiple processors gets the job done faster than a single processor. If you could delegate that job to another processor, then the currently running application would not be affected at all. Having more than one processor on a system enables this kind of symmetrical multiprocessing (SMP). Figure 33-1 shows the processor options for SQL Server 2000. Figure 33-1: SQL Server 2000 Processor options dialog box If you are running SQL Server on a multiprocessor machine, you can define the number of processors it should use for labor-intensive, long-running tasks of the sort just mentioned. SQL takes this a step further, performing queries across different processors, bringing the data together after the last thread is completed, and outputting the data to the user. This is known as thread synchronization. The main thread, which creates multiple threads, must wait for all of the threads to complete before it can continue the process. When using an SMP system, note that a single thread still only runs on a single processor. Your single-threaded VB6 application does not perform one iota better if you throw another processor at it. Your 16-bit Access 2.0 application does not run any better either, because 16 bits still equals a single process. You need to actually create processes on the other processors in order to take advantage of them. This means that you do not design a multiprocessor GUI. You create a GUI that creates other processes and can react when those processes are completed or interrupted, while still enabling the user to use the GUI for other tasks. Using resources: the more the merrier Threads consume resources. When too many resources are being used, your computer is painstakingly slow. If you attempt to open 80 instances of Visual Studio .NET while installing Exchange 2000 on a computer with 96MB of RAM, you will notice that the screen does not paint correctly, the mouse doesn't move very fast, and the music you were listening to in Windows Media Player is not playing anymore. These performance problems are caused by too many threads running at the same time on an operating system with hardware that cannot handle this amount of work. If you attempt the same action on your new server, the 32- processor Unisys box with 1 terabyte of RAM, you do not see any performance degradation at all. The more memory you have, the more physical address space there is the running applications to create more threads. When you write applications that create threads, be sure you take this into consideration. The more threads you create, the more resources your application consumes. This could actually cause poorer performance than a single-threaded application, depending on the OS. The more the merrier does not include threads. Therefore, use caution when creating threads in that new version of multithreaded Tetris you are writing in C#. Understanding application domains Earlier, you learned that the MFC SDK defines a process as an executing instance of an application. Each application that is executing creates a new main thread, which lasts the lifetime of that application instance. Because each application is a process, each instance of an application must have process isolation. Two separate instances of Microsoft Word act independently of each other. When you click Spell Check, InstanceA of Word does not spell- check the document running in InstanceB of Word. Even if InstanceA of Word attempts to pass a memory pointer to InstanceB of Word, InstanceB would not know what to do with it, or even know where to look for it, as memory pointers are only relative to the process in which they are running. In the .NET Framework, application domains are used to provide security and application isolation for managed code. Several application domains can run on a single process, or thread, with the same protection that would exist if the applications were running on multiple processes. Overhead is reduced with this concept, as calls do not need to be marshaled across process boundaries if the applications need to share data. Conversely, a single application domain can run across multiple threads. This is possible because of the way the CLR executes code. Once code is ready to execute, it has already gone through the process of verification by the JIT compiler. By passing this verification process, the code is guaranteed not to do invalid things, such as access memory it is not supposed to, causing a page fault. This concept of type-safe code ensures that your code does not violate any rules after the verifier has approved it passing from MSIL to PE code. In typical Win32 applications, there were no safeguards against one piece of code supplanting another piece of code, so each application needed process isolation. In .NET, because type safety is guaranteed, it is safe to run multiple applications from multiple providers within the same application domain. Understanding the benefits of multithreaded applications Several types of applications can take advantage of multithreading. • Applications with long processes • Polling and listener applications • Applications with a Cancel button in the GUI The following sections state the case for each of these reasons. Applications with long processes Applications that involve long processes with which the user does not need to interact can benefit from multithreading because the long-running process can be created on a worker thread that processes information in the background until a notification that the thread has completed is made to the process that called the thread. In the meantime, the user is not kept waiting, staring at an hourglass cursor, to move on to the next task. Polling and listener applications Polling applications and listener applications can benefit from multithreading. Suppose you have an application that has created threads that are listening or polling. When something happens, a thread can consume that particular event, and the other threads can continue to poll or listen for events to occur. An example of this is a service that listens for requests on a network port, or a polling application that checks the state of Microsoft Message Queue (MSMQ) for messages. An example of an off-the-shelf polling applications is Microsoft Biztalk Server. Biztalk is constantly polling for things like files in a directory, or files on an SMTP server. It cannot accomplish all of this on a single thread, so multiple threads poll different resources. Microsoft Message Queue has an add-on for Windows 2000 and a feature in Windows XP called Message Queue Triggers. With MSMQ Triggers, you can set properties that cause a trigger to fire an event. This is a multithreaded service that can handle thousands of simultaneous requests. Cancel buttons Any application that has a Cancel button on a form should follow this process: 1. Load and show the form modally. 2. Start the process that is occurring on a new thread. 3. Wait for the thread to complete. 4. Unload the form. By following these steps, the click event of your Cancel button occurs if the user clicks the button while another thread is executing. If the user does click the Cancel button, it actually clicks, as the process is running on a thread other than the currently running thread handling the click event, your code should then stop the process on the other running thread. This is a GUI feature that turns a good application into a great application. Creating Multithreaded Applications Now it's time to begin creating multithreaded applications. Threading is handled through the System.Threading namespace. The common members of the Thread class that you use are listed in Table 33-1. Table 33-1: Common Thread Class Members Member Description CurrentContext Returns the current context on which the thread is executing CurrentThread Returns a reference to the currently running thread ResetAbort Resets an abort request Sleep Suspends the current thread for a specified length of time Table 33-1: Common Thread Class Members Member Description ApartmentState Gets or sets the apartment state of the thread IsAlive Gets a value that indicates whether the thread has been started and is not dead IsBackground Gets or sets a value indicating whether the thread is a background thread Name Gets or sets the name of the thread Priority Gets or sets the thread priority Threadstate Gets the state of the thread Abort Raises the ThreadAbortException, which can end the thread Interrupt Interrupts a thread that is in the WaitSleepJoin thread state Join Waits for a thread Resume Resumes a thread that has been suspended Start Begins the thread execution Suspend Suspends the thread Creating new threads Creating a variable of the System.Threading.Thread type enables you to create a new thread to start working with. Because the concept of threading involves the independent execution of another task, the Thread constructor requires the address of a procedure that will do the work for the thread you are creating. The ThreadStart delegate is the only parameter the constructor needs to begin using the thread. To test this code, create a new project with the Console application template. The code in Listing 33-1 creates two new threads and calls the Start method of the Thread class to get the thread running. Listing 33-1: Creating New Threads using System; using System.Threading; public class Threads { public void Threader1() { } public void Threader2() { } } public class ThreadTest { public static int Main(String[] args) { Threads testThreading = new Threads(); Thread t1 = new Thread(new ThreadStart(testThreading.Threader1)); t1.Start(); Thread t2 = new Thread(new ThreadStart(testThreading.Threader2)); t2.Start(); Console.ReadLine(); return 0; } } When you create a variable of type thread, the procedure that handles the thread must exist for the ThreadStart delegate. If it does not, an error occurs and your application does not compile. The Name property sets or retrieves the name of a thread. This enables you to use a meaningful name instead of an address or hash code to reference the running threads. This is useful when using the debugging features of Visual Studio .NET. In the debugging toolbar, a drop-down list of the names of the running threads is available. Although you cannot "step out" of a thread and jump into another thread with the debugger, it is useful to know on which thread an error may have occurred. Now that the thread variables are declared, named, and started, you need to do something on the threads you have created. The procedure names that were passed to the thread constructor were called Threader1 and Threader2. You can now add some code to these methods to see how they act. Your code should now look something like Listing 33-2. Listing 33-2: Retreiving Information on Runnnig Threads using System; using System.Threading; public class Threads { public void Threader1() { Console.WriteLine (" *** Threader1 Information ***"); Console.WriteLine ("Name: " + Thread.CurrentThread.Name); Console.WriteLine (Thread.CurrentThread); Console.WriteLine ("State: " + Thread.CurrentThread.ThreadState); Console.WriteLine ("Priority: " + Thread.CurrentThread.Priority); Console.WriteLine(" *** End Threader1 Information ***"); } public void Threader2() { Console.WriteLine (" *** Threader2 Information ***"); Console.WriteLine ("Name: " + Thread.CurrentThread.Name); Console.WriteLine (Thread.CurrentThread); Console.WriteLine ("State: " + Thread.CurrentThread.ThreadState); Console.WriteLine ("Priority: " + Thread.CurrentThread.Priority); Console.WriteLine(" *** End Threader2 Information ***"); } } public class ThreadTest { public static int Main(String[] args) { Threads testThreading = new Threads(); Thread t1 = new Thread(new ThreadStart(testThreading.Threader1)); t1.Name = "Threader1"; t1.Start(); Thread t2 = new Thread(new ThreadStart(testThreading.Threader2)); t2.Name = "Threader2"; t2.Start(); Console.ReadLine(); return 0; } } When you run the application, your console output should look something like that shown in Figure 33-2 . Figure 33-2: Threading application output The output displayed in Figure 33-2 is not very pretty. If you recall, you are working with threads. Without setting a property or two, your Threader1 procedure never completes before Threader2 starts. When the following code executes t1.Start(); it begins the execution of the Threader1 code. Because it is a thread, it has roughly 20 milliseconds of the time slice. In that time period, it reached the second line of code in the function, passed control back to the operating system, and executed the following line of code: t2.start(); The Threader2 procedure then executes for its slice of time and is preempted by the t1 thread. This back-and-forth process continues until both procedures can finish. Understanding thread priority For the Threader1 procedure to finish before the Threader2 procedure begins, you need to set the Priority property to the correct ThreadPriority enumeration to ensure that the t1 thread has priority over any other thread. Before the t1.Start method call, add the following code: t1.Priority = ThreadPriority.Highest; When you set the priority to highest, t1 finishes before t2. If you run the application again, your output should look similar to that shown in Figure 33-3. Figure 33-3: Output after setting the thread priority The ThreadPriority enumeration dictates how a given thread is scheduled based on other running threads. ThreadPriority can be any one of the following: AboveNormal, BelowNormal, Highest, Lowest, or Normal. The algorithm that determines thread scheduling varies depending on the operating system on which the threads are running. By default, when a new thread is created, it is given a priority of 2, which is Normal in the enumeration. Understanding thread state When you create a new thread, you call the Start() method. At this point, the operating system allocates time slices to the address of the procedure passed in the thread constructor. Though the thread might live for a very long time, it still passes in between different states while other threads are being processed by the operating system. This state might be useful to you in your application. Based on the state of a thread, you could determine that something else might need to be processed. Besides Start, the most common thread states you will use are Sleep and Abort. By passing a number of milliseconds to the Sleep constructor, you are instructing the thread to give up the remainder of its time slice. Calling the Abort method stops the execution of the thread. Listing 33-3 shows some code that uses both Sleep and Abort. Listing 33-3: Using the Thread.Sleep Method using System; using System.Threading; public class Threads { public void Threader1() { for(int intX = 0; intX < 50;intX ++) { if(intX == 5){ Thread.Sleep(500); Console.WriteLine("Thread1 Sleeping");} } } public void Threader2() { for(int intX = 0; intX < 50;intX ++) { if(intX == 5){ Thread.Sleep(500); Console.WriteLine("Thread2 Sleeping");} } } } public class ThreadTest { public static int Main(String[] args) { Threads testThreading = new Threads(); Thread t1 = new Thread(new ThreadStart(testThreading.Threader1)); t1.Priority = ThreadPriority.Highest; t1.Start(); Thread t2 = new Thread(new ThreadStart(testThreading.Threader2)); t2.Start(); Console.ReadLine(); return 0; } } [...]... than in your class destructor Registering Classes with COM+ Your C# classes that are designed for use in a COM+ application must follow the same basic rules as C# classes that are designed for use by classic COM clients Chapter 34 describes how C# is used to build COM components Like COM components written in C#, COM+ components written in C# must be compiled into a DLL-based assembly, and must have a... be adorned with the ref keyword in the C# client Parameters that need to be passed by value are not shown with the ampersand in the assembly The C# client doesn't need to use the ref keyword on the parameters in this case Directly referencing the COM DLL from C# In the previous section, you learned how to use the Interop assembly created from Tlbimp.exe in a C# Windows Forms application The main reason... the runtime platform Understanding the System.EnterpriseServices Namespace Any C# class can be used by COM clients as a COM component, regardless of the class's inheritance tree C# classes can be derived from nothing more than System.Object and still be used as COM components Taking advantage of COM+ services within your C# classes, however, requires a more stringent inheritance policy The System.EnterpriseServices... virtual and can be overridden by derived classes to provide specific functionality Listing 35-1 shows a simple COM+ class written in C# This object participates in COM+ object pooling Listing 35-1: Poolable COM+ Component in C# using System.EnterpriseServices; [ObjectPooling(5, 10) ] public class PooledClass : ServicedComponent { public PooledClass() { } ~PooledClass() { } public override bool CanBePooled()... something different about the way the parameters are passed to the methods in C# If you look at the C# code for the SquareIt method, note the addition of the Ref keyword: Num1 = 5; Num2 = 6; // Call the SquareIt method Sum = ObjectInstance.SquareIt(ref Num1, ref Num2); Visual Basic COM servers may pass values by value or by reference Your C# code needs to use the appropriate keywords when passing parameters... Enables the caller to use the Marshal.GetLastWin32Error API function to determine whether an error occurred while executing the method In Visual Basic, the default is True; in C# and C++, the default is False Calling DLL functions from C# is similar to calling them from Visual Basic 6 With the DLLImport attribute, however, you are simply passing the DLL's name and the method that you need to call Note It... learned how to write COM client code in C#, including calling COM methods and working with COM properties As you can see, the NET Framework enables you to easily integrate existing COM code into your NET applications This easy integration gives you the opportunity to slowly move portions of an application to NET, without having to rewrite all of the COM component logic in C# Chapter 35: Working with COM+... components to developers writing componentbased software This chapter examines how to develop C# classes that you can use as configured components with COM+ Caution Although the NET Framework is available on a variety of operating system platforms, COM+ is not available on the same set of platforms Components written in C# that take advantage of COM+ services can only be used on platforms that support COM+... ObjectInstance.Message = "C# Rocks"; COMEventHandlerInstance = new COMObject_COMEventEventHandler(COMEventHandler); ObjectInstance.COMEvent += COMEventHandlerInstance; ObjectInstance.FireCOMEvent(); } void COMEventHandler(ref string Message) { listBox1.Items.Add(Message); } } } The output from this application looks similar to what is shown in Figure 34-6 Figure 34-6: Output from the C# client using the... multithreading in C# with the System.Thread namespace The basic idea behind multithreading is simple: By creating more than one thread, you can accomplish more than one task at a time The number of threads you create has to be determined by solid testing Too many threads can cause resource problems Not creating enough threads can result in your application not performing to its full potential With the . objects, load the objects dynamically at runtime, and even generate code as needed. Chapter 33: C# Threading In This Chapter The multithreading power of the .NET Framework enables you to write. task at a time. When you need to run a processor-intensive long-running thread, such as sorting 10 million records by first name, address, Zip code, middle name, and country, using multiple processors. use caution when creating threads in that new version of multithreaded Tetris you are writing in C#. Understanding application domains Earlier, you learned that the MFC SDK defines a process