C# ПІДРУЧНИКИ / c# / Apress - Accelerated C# 2005
.pdf
334 C H A P T E R 1 2 ■ T H R E A D I N G I N C #
newConnection.Send( msg, SocketFlags.None );
}
}
}
static void Main() {
// Start the listening thread. Thread listener = new Thread(
new ThreadStart( EntryPoint.ListenForRequests) );
listener.IsBackground = true; listener.Start();
Console.WriteLine( "Press <enter> to quit" ); Console.ReadLine();
}
}
The previous example creates an extra thread that simply loops around listening for incoming connections and servicing them as soon as they come in. The problems with this approach are many. First of all, only one thread handles the incoming connections. If the connections are flying in at a rapid rate, it will quickly become overwhelmed. Think about a web server that could easily see thousands of requests per second. As it turns out, the Socket class implements the asynchronous calling pattern of the .NET Framework. Using the pattern, you can make the server a little bit better by servicing the incoming requests using the thread pool, as follows:
using System; using System.Text;
using System.Threading; using System.Net;
using System.Net.Sockets;
public class EntryPoint {
private const int CONNECT_QUEUE_LENGTH = 4; private const int LISTEN_PORT = 1234;
static void ListenForRequests() { Socket listenSock =
new Socket( AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp );
listenSock.Bind( new IPEndPoint(IPAddress.Any, LISTEN_PORT) );
listenSock.Listen( CONNECT_QUEUE_LENGTH );
while( true ) {
Socket newConnection = listenSock.Accept();
byte[] msg = Encoding.UTF8.GetBytes( "Hello World!" ); newConnection.BeginSend( msg,
0, msg.Length, SocketFlags.None, null, null );
}
}
static void Main() {
// Start the listening thread.
C H A P T E R 1 2 ■ T H R E A D I N G I N C # |
335 |
Thread listener = new Thread(
new ThreadStart( EntryPoint.ListenForRequests) );
listener.IsBackground = true; listener.Start();
Console.WriteLine( "Press <enter> to quit" ); Console.ReadLine();
}
}
The server is becoming a little more efficient, since it is now sending the data to the incoming connection from a thread in the thread pool. This code also demonstrates a fire-and-forget strategy when using the asynchronous pattern. The caller is not interested in the return object that implements IAsyncResult, nor is it interested in setting a callback method to get called when the work completes. This fire-and-forget call is a valiant attempt to make the server more efficient. However, the result is less than satisfactory, since the using statement from the previous incarnation of the server is gone. The Socket is not closed in a timely manner, and the remote connections are held open until the GC gets around to finalizing the Socket objects. Therefore, the asynchronous call needs to include a callback in order to close the connection. It wouldn’t make sense for the listening thread to wait on the EndSend method, as that would put you back in the same inefficiency boat you were in before.
■Note When you get an object that implements IAsyncResult back from starting an asynchronous operation, that object must implement the IAsyncResult.AsyncWaitHandle property to allow users to obtain a handle they can wait on. In the case of Socket, an instance of OverlappedAsyncResult is returned. That class ultimately derives from System.Net.LazyAsyncResult. It doesn’t actually create the event to wait on until someone accesses it via the IAsyncResult.AsyncWaitHandle property. This lazy creation spares the burden of creating the handle when a large majority of the time, nobody is interested in the handle. Also, it is the responsibility of the OverlappedAsyncResult object to close the OS handle when it is finished with it.
However, before getting to the callback, consider the listening thread for a moment. All it does is spin around listening for incoming requests. Wouldn’t it be more efficient if the server were to use the thread pool to handle the listening too? Of course it would! So, now, let me present the new and improved “Hello World!” server that makes full use of the process thread pool:
using System; using System.Text;
using System.Threading; using System.Net;
using System.Net.Sockets;
public class EntryPoint {
private const int CONNECT_QUEUE_LENGTH = 4; private const int LISTEN_PORT = 1234;
private const int MAX_CONNECTION_HANDLERS = 4;
private static void HandleConnection( IAsyncResult ar ) { Socket listener = (Socket) ar.AsyncState;
Socket newConnection = listener.EndAccept( ar ); byte[] msg = Encoding.UTF8.GetBytes( "Hello World!" ); newConnection.BeginSend( msg,
0, msg.Length,
336 C H A P T E R 1 2 ■ T H R E A D I N G I N C #
SocketFlags.None, new AsyncCallback(
EntryPoint.CloseConnection), newConnection );
// Now queue another accept. listener.BeginAccept(
new AsyncCallback(EntryPoint.HandleConnection), listener );
}
static void CloseConnection( IAsyncResult ar ) { Socket theSocket = (Socket) ar.AsyncState; theSocket.Close();
}
static void Main() { Socket listenSock =
new Socket( AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp );
listenSock.Bind( new IPEndPoint(IPAddress.Any, LISTEN_PORT) );
listenSock.Listen( CONNECT_QUEUE_LENGTH );
// Pend the connection handlers.
for( int i = 0; i < MAX_CONNECTION_HANDLERS; ++i ) { listenSock.BeginAccept(
new AsyncCallback(EntryPoint.HandleConnection), listenSock );
}
Console.WriteLine( "Press <enter> to quit" ); Console.ReadLine();
}
}
Now, the “Hello World” server is making full use of the process thread pool and can handle incoming client requests with the best concurrency. Incidentally, testing the connection is fairly simple using the built-in Windows Telnet client. Simply run Telnet from a command prompt or from the Start Run dialog, and at the prompt enter the following command to connect to port 1234 on the local machine while the server process is running in another command window:
Microsoft Telnet> open 127.0.0.1 1234
Timers
Yet another entry point into the thread pool is via Timer objects in the System.Threading namespace. As the name implies, you can arrange for the thread pool to call a delegate at a specific time as well as at regular intervals. Let’s look at an example of how to use a Timer object:
using System;
using System.Threading;
C H A P T E R 1 2 ■ T H R E A D I N G I N C # |
337 |
public class EntryPoint
{
private static void TimerProc( object state ) { Console.WriteLine( "The current time is {0} on thread {1}",
DateTime.Now, Thread.CurrentThread.GetHashCode() );
Thread.Sleep( 3000 );
}
static void Main() {
Console.WriteLine( "Press <enter> when finished\n\n" );
Timer myTimer =
new Timer( new TimerCallback(EntryPoint.TimerProc), null,
0, 2000 );
Console.ReadLine();
myTimer.Dispose();
}
}
When the timer is created, you must give it a delegate to call at the required time. Therefore, I’ve created a TimerCallback delegate that points back to the static TimerProc method. The second parameter to the Timer constructor is an arbitrary state object that you can pass in. When your timer callback gets called, this state object is passed to the timer callback. In my example, I have no need for a state object, so I just pass null. The last two parameters to the constructor define when the callback gets called. The second-to-last parameter indicates when the timer should fire for the first time. In my example, I pass 0, which indicates that it should fire immediately. The last parameter is the period at which the callback should be called. In my example, I’ve asked for a two-second period. If you don’t want the timer to be called periodically, pass Timeout.Infinite as the last parameter. Finally, to shut down the timer, simply call its Dispose method.
In my example, you may wonder why I have the Sleep() call inside the TimerProc method. It’s there just to illustrate a point, and that is that an arbitrary thread calls the TimerProc(). Therefore, any code that executes as a result of your TimerCallback delegate must be thread-safe. In my example, the first thread in the thread pool to call TimerProc() sleeps longer than the next timeout, so the thread pool calls the TimerProc method 2 seconds later on another thread, as you can see in the generated output. You could really cause some strain on the thread pool if you were to notch up the sleep in the TimerProc().
■Note If you’ve ever used the Timer class in the System.Windows.Forms namespace, you must realize that it’s a completely different beast than the Timer class in the System.Threading namespace. For one, the Forms.Timer is based upon Win32 Windows messaging—namely, the WM_TIMER message. One handy quality of the Forms.Timer is that its timer callback is always called on the same thread. However, the only way that happens in the first place is if the UI thread that the timer is a part of has an underlying UI message pump. If the pump stalls, so do the Forms.Timer callbacks. So, naturally, the Threading.Timer is more powerful in the sense that it doesn’t suffer from this dependency. However, the drawback is that you must code your Threading.Timer callbacks in a thread-safe manner.
338 C H A P T E R 1 2 ■ T H R E A D I N G I N C #
Summary
In this chapter, I’ve covered the intricacies of managed threads in the .NET environment. I covered the various mechanisms in place for managing synchronization between threads, including the
Interlocked, Monitor, AutoResetEvent, ManualResetEvent, WaitHandle-based objects, and so on. I then described the IOU pattern and how the .NET Framework uses it extensively to get work done asynchronously. That discussion centered around the CLR’s usage of the ThreadPool based upon the Windows thread pool implementation.
Threading always adds complexity to applications. However, when used properly, it can make applications more responsive to user commands and more efficient. Although multithreading development comes with its pitfalls, the .NET Framework and the CLR mitigate many of those risks and provide a model that shields you from the intricacies of the operating system—most of the time. For example, thread pools have always been difficult to implement, even after a common implementation was added to the Windows operating system. Not only does the .NET environment provide a nice buffer between your code and the Windows thread pool intricacies, but it also allows your code to run on other platforms that implement the .NET Framework, such as the Mono runtime running on Linux. If you understand the details of the threading facilities provided by the .NET runtime and are familiar with multithreaded synchronization techniques, as covered in this chapter, then you’re well on your way to producing effective multithreaded applications.
In the next chapter, I go in search of a C# canonical form for types. I investigate the checklist of questions you should ask yourself when designing any type using C# for the .NET Framework.
C H A P T E R 1 3
■ ■ ■
In Search of C# Canonical Forms
Many object-oriented languages—C# included—don’t offer anything to force developers to create well-designed software. There is no better example of this than when using C++ to implement an OO design. C# is a little more structured than C++; for example, you cannot create free static functions that exist outside the context of a defined type. Still, C# doesn’t force you to create software that adheres to well-known practices of good software design.
The C++ community quickly identified some canonical forms useful for designing types to meet a specific purpose. Really and truly, these canonical forms are merely checklists, or recipes, you can use while designing new classes. Before a pilot can clear an airplane to back out of the gate, he must go through a strict checklist. The goal of this chapter is to identify such checklists for creating robust types in the C# world.
When you explore these checklists, you need to consider what sorts of behaviors are required of objects of the new type you’re creating. For example, is your new type going to be cloneable? In other words, can it be copied? Does your new type support ordering if instances of it are placed in
a collection? What does it mean to compare two references of this object’s type for equality? In other words, do you want to know if the two references refer to the same instance? Or do you want to know if two instances referred to have exactly the same state? These are the types of questions you should ask yourself when you create a new type.
■Note This chapter is rather long, but it’s important to keep so much useful and related information together. Overall, the chapter is sectioned into two partitions. The first partition covers reference types, while the latter covers value types. I cover the longer partition on reference types first, since some material applies to both reference types and value types. Finally, the chapter concludes with a checklist to go through when designing new types.
Reference Type Canonical Form
First, let’s explore canonical forms for reference types in C#. In C#, objects live on the managed heap and are accessed through value types containing references to them. In C++ terms, you can envision a system where all objects are created dynamically using new, and you only reference them through the pointer returned by new. This is essentially what is happening in the CLR, except the CLR tracks all of these “pointers,” or references, and it knows when the objects on the heap have no more references to them and thus, when they can be destroyed.
To be a little more specific, consider this: Over the years, the C++ community has utilized a vast array of idioms that rely upon the stack to help manage resources. If you create your C++ object on the stack, then the compiler will make sure that your object’s constructors and destructor get called at the appropriate time, thus giving you a controlled point at which to put your resource cleanup code. The dominant idiom here is called Resource Acquisition Is Initialization (RAII), and it’s used
339
340C H A P T E R 1 3 ■ I N S E A R C H O F C # C A N O N I C A L F O R M S
extensively in C++ and any other object-oriented language with deterministic destruction of objects. Basically, the idea behind the idiom is that any resource that requires allocation is acquired in a constructor body, and the release of the resource is in the matching destructor. This idiom is so
entrenched that in order to write robust, exception-safe, and exception-neutral C++ code, you must use this idiom extensively and contain just about every usage of new and delete inside constructors and destructors. In the C# domain, this idiom is not available so easily, due to the fact that C# destructors are not deterministic. Therefore, you must approach the problems solved by this idiom in a different way.
Default to sealed Classes
When you create a new class, you should automatically mark that class sealed, and only remove the sealed keyword if you can think of a bona fide reason why someone would need to derive from your class. Why not go the other way around and make the class unsealed by default and seal it when you know someone should not derive from it? Because it’s impossible to predict how someone will attempt to derive from your class if you don’t put in specific design measures to support inheritance. I’ve seen many designs over the years where someone attempted to derive from a class that was never meant to be derived from. For example, in a good design, classes that have no virtual methods are not normally intended to be derived from. In most likelihood, the lack of virtual methods may indicate that the author of the class didn’t consider whether anyone would even want to inherit from the type and probably should have marked the class sealed. If your class is not sealed, and you intend to allow others to inherit from it, be sure to include adequate documentation so the person deriving from your class doesn’t shoot himself in the foot.
Even classes that do have virtual methods and are purposely meant to be derived from can be problematic. For example, if you derive from a class that provides a virtual method DoSomething, and you’d like to extend that method by overriding it, do you call the base class version in your override? If so, do you call it before or after you get your derived work done? Does the ordering matter? Maybe it does if protected fields are declared in the base class.1 If you don’t have really good documentation for the class you’re deriving from, you may never know the answers to these questions. In fact, this is one reason why extension through containment is generally more flexible, and thus more powerful, at design time than extension through inheritance. Extension through containment is dynamic and performed at run time, whereas inheritance-based extension is more restrictive, since it is static and wired up at compile time. And better yet, you can do containment-based extension even if the class you want to extend is marked sealed.
Unless you can come up with a really good reason why your class should serve as a base class, mark your class sealed. Otherwise, be prepared to offer very detailed documentation on how to best derive from your class. I guarantee that you can produce a different design using interface inheritance together with containment rather than implementation (class) inheritance that can do the same job. Since that is the case, there’s almost no reason why almost all of the classes you design should not be marked sealed. Don’t misunderstand: I’m not saying that all inheritance is bad. On the contrary, it is useful when used properly. Unfortunately, it is greatly misused. A deep hierarchy tree, as opposed to a shallow, flat one, is a common sign that you should rethink the design.
■Note When leaf classes that derive from other classes with virtual methods are marked sealed, or when individual override methods are marked sealed, the runtime can turn calls to those methods into nonvirtual calls, since no more derived implementations of those methods can exist.
1.In Chapter 4, I discussed encapsulation and its importance in object-oriented design. It’s important to note that protected fields break encapsulation.
C H A P T E R 1 3 ■ I N S E A R C H O F C # C A N O N I C A L F O R M S |
341 |
Use the NVI Pattern
Many times, when you design a class specifically capable of acting as a base class in a hierarchy, you often declare methods that are virtual so that deriving classes can modify the behavior. A first pass at such a base class may look something like the following:
using System;
public class Base
{
public virtual void DoWork() { Console.WriteLine( "Base.DoWork()" );
}
}
public class Derived : Base
{
public override void DoWork() { Console.WriteLine( "Derived.DoWork()" );
}
}
public class EntryPoint
{
static void Main() {
Base b = new Derived(); b.DoWork();
}
}
Not surprisingly, the output from the previous example looks like this:
Derived.DoWork()
However, the design could be subtly more robust. Imagine that you’re the writer of Base, and you have deployed Base to millions of users. Many people are happily using Base all over the world when you decide, for some good reason, that you should do some preand postprocessing within DoWork(). For example, suppose you would like to provide a debug version of Base that tracks how many times the DoWork() method is called. As written previously, you cannot do such a thing without forcing breaking changes onto the millions of users who have used your class Base. For example, you could introduce two more methods, named PreDoWork and PostDoWork, and ask kindly that your users reimplement their overrides so that they call these methods at the correct time. Ouch! Now, let’s consider a minor modification to the original design that doesn’t even change the public interface of Base:
using System;
public class Base
{
public void DoWork() { CoreDoWork();
}
protected virtual void CoreDoWork() { Console.WriteLine( "Base.DoWork()" );
}
}
342C H A P T E R 1 3 ■ I N S E A R C H O F C # C A N O N I C A L F O R M S
public class Derived : Base
{
protected override void CoreDoWork() { Console.WriteLine( "Derived.DoWork()" );
}
}
public class EntryPoint
{
static void Main() {
Base b = new Derived(); b.DoWork();
}
}
This nifty little pattern is called the Non-Virtual Interface (NVI) pattern, and it does exactly that: It makes the public interface to the base class nonvirtual, but the overrideable behavior is moved into another protected method named CoreDoWork. The NVI pattern is similar to the Template Method pattern as described by Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides in
Design Patterns: Elements of Reusable Object-Oriented Software (Boston, MA: Addison-Wesley Professional, 1995). .NET Framework libraries use the NVI pattern widely, and it’s circulated in library design guidelines at Microsoft for obviously good reasons. In order to add some metering to the DoWork method, you only need to modify Base and the assembly that contains it. Any of the other classes that derive from assembly don’t even have to change.
Another technique that typically gets used with NVI in the C++ world is that of actually declaring the virtual method private, as in the following code that unfortunately won’t compile in C# for reasons I’ll explain shortly:
// WILL NOT COMPILE!!!!! using System;
public class Base
{
public void DoWork() { CoreDoWork();
}
// WILL NOT COMPILE!!!!!
private virtual void CoreDoWork() { Console.WriteLine( "Base.DoWork()" );
}
}
public class Derived : Base
{
// WILL NOT COMPILE!!!!!
private override void CoreDoWork() { Console.WriteLine( "Derived.DoWork()" );
}
}
public class EntryPoint
{
static void Main() {
Base b = new Derived(); b.DoWork();
}
}
C H A P T E R 1 3 ■ I N S E A R C H O F C # C A N O N I C A L F O R M S |
343 |
This code would actually compile in the initial .NET 1.0 release of C#, and the technique was perfectly valid in the CLR based upon the fact that the CLI spec at the time and C# wanted to match the C++ semantics as closely as possible. Before I explain why this won’t work in C# now, let me explain why you would want to do it in the first place.
There is a fundamental difference between a method’s visibility and its accessibility. If the method is in the declaration of a class or struct, no matter what its protection level, it is visible. And traditionally, in order for a derived class to override a method, it merely has to be visible, and not accessible.
■Note The only thing private means on a C++ private virtual method is that the derived class may not call the base class’s implementation. If you don’t believe me, try this example in native C++. You’ll find that it works as expected.
The beauty of being able to declare private virtual methods is that you don’t have to worry about derived classes misusing your Base class. For example, maybe you require that they don’t call your base class implementation of the virtual method. Fine, just make it private virtual. In fact, using such a technique, you should only make your method protected virtual if you know there is a good reason why the base class would need to call it. And if you do that, you must strictly document at what point the override should call the base implementation. Many people believe that just because the method is declared private, it cannot be overridden. But in the strict sense, restricted accessibility doesn’t make it invisible to overriding.
Now let me explain why this feature was turned off in the .NET 1.1 release of C#. This was a mystery to me until Brandon Bray from the Microsoft Visual C++ team explained it neatly. The fact that you can inherit across assembly boundaries turns this feature into a sort of security hole. With native C++, it was never an issue. Consider this: If a private virtual method can be overridden, then so can an internal virtual method. And therein lies the problem. It would allow you to override the behavior of an internal virtual method on some random class in a particular assembly, and that is the source of the security hole. So, a tradeoff was made, and every release starting with the 1.1 release has this feature disabled. Incidentally, this same fix was added to C++/CLI. Although native C++ classes can use the private virtual method feature effectively, ref C++ classes cannot. And that, of course, is due to the fact that ref C++ classes represent .NET ref types that can be inherited across assembly boundaries. How about that!
Is the Object Cloneable?
As you know, objects in C# and in the CLR live on the heap and are accessed through references. You’re not actually making a copy of the object when you assign one object variable to another, as in the following code.
Object obj = new Object();
Object objCopy = obj;
After this code executes, objCopy doesn’t refer to a copy of obj; rather, you now have two references to the same Object instance.
However, sometimes it makes sense to be able to make a copy of an object. For that purpose, the Standard Library defines the ICloneable interface. When your object implements this interface, it is saying that it supports the ability to have copies of itself made. In other words, it claims that it can be used as a prototype to create new instances of objects. Objects of this type could participate in a prototype factory design pattern. This is opposite of the C++ world, where objects are able to be copied by default and you have to explicitly restrict objects of a type from being copied.
