An important objective for any graphical application is to maintain a responsive user interface. The event loops of many common graphical programming tool kits run single-threaded, and any delays in application-specific code have a noticeable and negative impact on the user experience. Applications often have no choice but to create a separate thread for any operation that has the potential to block.
By default, an Ice invocation uses synchronous semantics similar to that of a regular in-process function call: the calling thread blocks until the invocation completes. However, a synchronous Ice invocation carries additional risk of blocking because of the network activity that Ice performs behind the scenes. As an example, the Ice run time may need to establish a connection to the server, which can take some time. Even if the connection is already established due to an earlier invocation, the seemingly simple act of sending a message over an open connection can also block unexpectedly (for example, due to problems in the network).
Applications such as our graphical chat clients that require non-blocking semantics must instead use Ice's asynchronous programming model, called Asynchronous Method Invocation (AMI). Although this model requires some extra effort, the benefit is worth it: Ice guarantees that asynchronous invocations will never block the calling thread. As a result, graphical applications can safely make Ice invocations from the event loop thread without adversely affecting the user interface.
Ice provides the
Dispatcher facility to give applications control over the thread with which request dispatches are executed. The graphical clients in the chat demo use this facility so that calls on
ChatRoomCallback objects can update the user interface directly.
Creating a Session
One of the first tasks of a push client is establishing a session with the Glacier2 router. This process is greatly simplified by using the Glacier2 helper classes included with Ice, and our graphical clients make extensive use of these classes. (The code shown below was adapted from the Java graphical client, but the C# and C++ codes are similar.)
Before we can create a session, we need to configure the dispatcher to ensure that request dispatches are executed in the UI thread:
As you can see, the dispatcher implementation simply delegates to Swing's
Next, the client creates an instance of Glacier2.
SessionFactoryHelper and passes it a callback object:
The callback is invoked to notify the application about significant events in the lifecycle of a Glacier2 session.
Now we can use the factory to initiate the creation of a session:
The client invokes
connect with the user-supplied account information. The return value is a new
Glacier2.SessionHelper object that is responsible for managing the session and for invoking the client's
SessionCallback object when necessary. For example, since the establishment of a session occurs asynchronously to avoid blocking the thread that calls
connect, the client cannot begin using the session until its
connected callback method is invoked to indicate that the request completed successfully:
session returns a proxy for the newly-created Glacier2 session object. We down-cast this proxy to the derived interface
Chat::ChatSession that we saw earlier.
Next the code creates the callback object that will receive events from the chat room. There's a lot happening in this short snippet of code. First, we instantiate our callback servant class
ChatRoomCallbackI and pass it to the
addWithUUID method. This method registers our servant with an object adapter intended especially for callback objects, creating that object adapter if it doesn't already exist.
addWithUUID also ensures that the object identity associated with the servant includes a UUID and complies with Glacier2's conventions for callback objects. Finally, we narrow the resulting callback proxy to the type
Chat::ChatRoomCallback using an unchecked cast.
The last step is to join the chat room, which we accomplish by invoking
setCallback on the session. We use an asynchronous invocation here to avoid blocking.
The user interface of the .NET chat client is built using Windows Presentation Foundation (WPF),. The chat client's graphical elements are arranged using Extensible Application Markup Language (XAML), while the application logic is implemented in C#. To avoid blocking .NET's event loop, the program uses asynchronous invocations to communicate with the chat server. For example, the code below shows how the client invokes an asynchronous version of the send operation to publish a newly-typed chat message:
sendAsync method accepts the chat message as its only argument and returns a .NET Task. The code then awaits the task and .NET will schedule a continuation to run after the task completes. Upon completion, we dispatch the event or handle the error as we do for a synchronous invocation. The call to
sendAsync is made from a WPF thread and the continuation will be executed using the WPF thread because the WPF SynchronizationContext schedules the continuations using its dispatcher, and therefore it's safe to modify GUI components from there.
The user interface of the C++ chat client is built using the Qt Framework. To avoid blocking Qt's event loop, the program uses asynchronous invocations to communicate with the chat server. For example, the code below shows how the client invokes an asynchronous version of the send operation to publish a newly-typed chat message:
Ice calls the
response callback when the invocation completes successfully; the parameters of this method correspond to the return value and output parameters of the Slice operation. In the example above,
response receives the time stamp assigned by the server. Note that the
exception callbacks are executed in the UI thread because we installed a
Dispatcher, so it's safe for these functions to modify the user interface.