Previous section   Next section

12.3 Delegates and Callback Mechanisms

There are two ways to get your laundry done. The first way is to put your laundry into the machine, put in a few quarters and then wait for the machine to run. You wait. And then you wait. About 30 minutes later the machine stops and you take your laundry back.

The second way to get your laundry done is to take it to the Laundromat and say "Here, please clean this clothing and call me back when you are done. Here's my cell number." The person in the Laundromat does the work while you go off and do something else. When your laundry is done, they call you and say "Your clothes are clean. Your pick up number is 123." When you return, you give the person at the desk the number 123, and you get back your clean clothes.

The .NET Framework supports the notion of a "callback." (Callbacks have been in use for many years, and Windows programmers have been using callbacks at least since Win 3.x.) The idea of a callback is that you say to a method, "Do this work, and call me back when you are done." It is a simple and clean mechanism for multitasking.

The .NET Framework provides a class, FileStream, which provides asynchronous reading of a file. You do not have to create the threads yourself; FileStream will read the file for you asynchronously, and callback a method you designate when it has data for you, as illustrated in Example 12-4.

Example 12-4. Using callbacks
Option Strict On
Imports System
Imports System.IO
Imports System.Text

   Public Class AsynchIOTester
        Private inputStream As Stream

        ' delegated method
        Private myCallBack As AsyncCallback

        ' buffer to hold the read data
        Private buffer( ) As Byte

        ' the size of the buffer
        Private Const BufferSize As Integer = 256

        ' constructor
        Sub New( )
            ' open the input stream
            inputStream = New FileStream( _
             "C:\temp\streams.txt", _
            FileMode.Open, _
            FileAccess.Read, _
            FileShare.ReadWrite, _
            1024, _
            True)

            ' allocate a buffer
            buffer = New Byte(BufferSize) {}

            ' assign the call back
            ' myCallBack = New AsyncCallback(AddressOf OnCompletedRead)
            myCallBack = AddressOf OnCompletedRead

        End Sub 'New

        Public Shared Sub Main( )
            ' create an instance of AsynchIOTester
            ' which invokes the constructor
            Dim theApp As New AsynchIOTester( )

            ' call the instance method
            theApp.Run( )
        End Sub 'Main

        Sub Run( )
            inputStream.BeginRead( _
            buffer, _
            0, _
            buffer.Length, _
            myCallBack, _
            Nothing)

            Dim i As Long
            For i = 0 To 499999
                If i Mod 1000 = 0 Then
                    Console.WriteLine("i: {0}", i)
                End If
            Next i
        End Sub 'Run

        ' call back method
        Sub OnCompletedRead(ByVal asyncResult As IAsyncResult)
            Dim bytesRead As Integer = inputStream.EndRead(asyncResult)

            ' if we got bytes, make them a string 
            ' and display them, then start up again. 
            ' Otherwise, we're done.
            If bytesRead > 0 Then
                Dim s As String = _
                  Encoding.ASCII.GetString(buffer, 0, bytesRead)
                Console.WriteLine(s)
                inputStream.BeginRead( _
                   buffer, 0, buffer.Length, myCallBack, Nothing)
            End If
        End Sub 'OnCompletedRead
   End class

In Example 12-4, you open a FileStream object, passing in the (hardwired) name of the file, the fileMode (e.g., Open), the FileAccess flag (e.g., Read), and the FileShare mode (e.g., ReadWrite). You also pass in an integer signifying the buffer size and a Boolean indicating whether the FileStream should be opened asynchronously:

inputStream = New FileStream( _
 "C:\temp\streams.txt", _
FileMode.Open, _
FileAccess.Read, _
FileShare.ReadWrite, _
1024, _
True)

The FileStream object provides a method, BeginRead( ), to provide asynchronous reading of the file (reading a block of text into memory while your other code does its work). You must pass in a buffer in which it will place your data, along with the offset into that buffer into which it will begin reading. You pass in the length of the buffer and you must tell BeginRead( ) the method you want to call back to.

You designate the method you want to call back to by passing in a delegate. You'll create that delegate in the next example as a member of your class:

Private myCallBack As AsyncCallback

The type of the delegate was determined by the author of the FileStream class, which designated that you must pass in a Delegate of type AsyncCallback. The AsyncCallback delegate is defined in the documentation as follows:

Public Delegate Sub AsyncCallback( _
   ByVal ar As IAsyncResult)

That is, it is a subroutine (and thus returns no value) and takes as its single parameter an object that implements the interface IAsyncResult. You do not have to implement that class yourself. All you need to do is create a method that declares a parameter of type IAsyncResult. Such an object will be passed to you by the FileStream's BeginRead( ) method, and you will use it as a token that you will return to the FileStream by calling EndRead( ). Here is the declaration of the OnCompletedRead( ) method, which you'll encapsulate in your AsyncCallback delegate:

Sub OnCompletedRead(ByVal asyncResult As IAsyncResult)
 '...
End Sub 'OnCompletedRead

You instantiate the delegate in the constructor to your class:

myCallBack = New AsyncCallback(AddressOf OnCompletedRead)

As an alternative, you can simply write:

myCallBack = AddressOf OnCompletedRead

and the compiler will figure out that you are instantiating an AsyncCallback delegate based on the declared type of myCallBack.

You are now ready to start the callback process. You begin in your test class's Run( ) method by calling BeginRead( ):

Sub Run( )
    inputStream.BeginRead( _
    buffer, _       
    0, _            
    buffer.Length, _
    myCallBack, _
    Nothing)

The first parameter is a buffer, declared in this case as a member variable:

Private buffer( ) As Byte

The second parameter (0) is the offset into that buffer. By entering 0, the data read from the disk will be written to the buffer starting at offset 0. The third parameter is the length of the buffer. The fourth parameter is the one we care about: the AsyncCallBack delegate you declared and instantiated earlier. The fifth and final parameter is a state object. The state object can be any object you want; typically it is used to hold the current state of your calling object. In the case shown, you pass Nothing, a VB.NET keyword that indicates that you have no state object.

After you call BeginRead( ), you go on with your other work. In Example 12-4, that work is simulated by counting to half a million:

Dim i As Long
For i = 0 To 499999
    If i Mod 1000 = 0 Then
        Console.WriteLine("i: {0}", i)
    End If
Next i

The FileStream will go off and open the file on your behalf. It will then read from the file and fill your buffer. When it is ready for you to process the data, it will interrupt your work in Run( ), and will call the method you encapsulated with the delegate. You will remember that the delegated method is called OnCompletedRead( ):

Sub OnCompletedRead(ByVal asyncResult As IAsyncResult)
    Dim bytesRead As Integer = inputStream.EndRead(asyncResult)

    ' if we got bytes, make them a string 
    ' and display them, then start up again. 
    ' Otherwise, we're done.
    If bytesRead > 0 Then
        Dim s As String = Encoding.ASCII.GetString(buffer, 0, bytesRead)
        Console.WriteLine(s)
        inputStream.BeginRead(buffer, 0, buffer.Length, myCallBack, Nothing)
    End If

When the FileStream calls your method, it will pass in an instance of a class that implements the IAsyncResult interface. The first thing you do in this method is pass that IAsyncResult object to the FileStream's EndRead( ) method. EndRead( ) returns an integer indicating the number of bytes successfully read from the file. If that value is greater than zero, your buffer has data in it.

The buffer is a buffer of bytes, but you need a string to display. To convert the buffer to a string, you will call Encoding.ASCII.GetString( )—a shared method that will take your buffer, an offset, and the number of bytes read and return an ASCII string. You can then display that string to the console.

Finally, you'll call BeginRead( ) again, passing back the buffer, the offset (again 0), the length of the buffer, and the delegate, as well as Nothing for the state object. This begins another round. Control will return to the Run( ) method, and you will continue counting.

The effect is that you ping-pong back and forth between the work you are doing in Run( ) (counting to 500,000) and the work you are doing in OnCompletedRead( ). You have achieved multitasking without instantiating or managing any threads; you have only to write the callback mechanism and let the FileStream do the thread management for you.


  Previous section   Next section
Top