Occasionally Connected Systems Architecture: The Client

Introduction

The greatest advantages Smart Clients have over their Internet Application counterparts are the ability to work offline and the best user experience. However, one situation where Smart Clients often fail to provide even a decent user experience is when synchronizing with the server when they reconnect. This problem becomes even more exacerbated in high volume environments where performance is absolutely critical.

Why Synchronization Makes the UI Sluggish

Often, Smart Clients show the user lists and aggregates of master data that are updated by many users. As updates arrive, these views need to be updated—new elements added to lists, fields updated in grids, and so forth. When the system is performing these activities, it needs to take control of the UI thread—the thread that enables the user to interact with the system.

When connected, Smart Clients receive an ongoing trickle of updates, where the time required to process these updates and show them to the user is quite small. However, when a large number of updates arrive at once, as occurs when the client reconnects after a period of offline activity, the time required to perform these updates can be quite large, making it difficult for the user to even move the mouse on the screen let alone get any work done.

Certain applications enable the user to perform some very CPU-intensive operations, such as historical trend analysis, simulations, and geographic mapping. If these operations need to be invoked when information arrives from the server, reconnecting the Smart Client may render it unusable for minutes at a time. Consider the implications for trading systems or military applications.

When Synchronizing, Relinquish the UI Thread ASAP

As updates arrive from the server, perform as much processing as possible on background threads. Although CPU-intensive operations will always take their toll, performing them in the background will hint to the operating system that they are not top priority. You may even want to adjust the priority of these background threads, yet this rarely produces any visible benefit.

After all processing on the server updates have been completed and all that is left is to update the UI, do so as quickly as possible and then relinquish the thread. Be aware, though, that while updating the UI, the user will not be able to interact with it. So, if the time required to show all the updates becomes significant (anything more than 0.1 seconds will be felt), consider showing them in chunks. Make sure you wait between chunks; otherwise, the net result will be worse than if you performed all the updates in one shot.

The Dangers of Data Races

Another issue that rears its ugly head in several runtime scenarios occurs when the user is updating data for which a server update has just arrived. This difficult-to-reproduce condition may cause the given entities to enter an invalid state. For instance:

[User thread] Add order line to order
[Background thread] Cancel an existing order line in the order
[Background thread] Set order total
[User thread] Set order total

As a result of poor timing, the order total does not reflect the total amount of the order lines. Hopefully, server-side validation will be able to catch this before the order is saved. However, the smart client may continue using the invalid data without even contacting the server, causing the user to make erroneous decisions as to the discount made available to the client on other orders.

This issue is a special kind of Race Condition that usually does not result in the application crashing. For that reason, it is all the more insidious. Everything appears to be working correctly in the system—that is, until the IRS audits your company. Because this issue is so difficult to detect, reproduce, or debug, it is critical that you prevent it by design.

Avoiding DeadLocks

The key to preventing data races is to prevent multiple threads from interacting with the same objects concurrently. One way of doing this is to have each object perform its own locking, yet this system heightens the chances of deadlocks as follows:

Automatic Synchronization

A locking scheme that can lock groups of objects is required to prevent deadlocks. Fortunately, .NET has a built-in mechanism that does just that—the ContextBoundObject and the SynchronizationAttribute. Unfortunately, this mechanism comes with certain performance implications, to the tune of 100X the overhead of regular method calls. It is for this reason that the mechanism should not be used on the entities themselves. Instead, make use of it on Controller and Service Agent classes.

Controller classes represent the "C" in the well known MVC acronym—Model, View, Controller. The Service Agent classes are those that receive updates from the server and, in turn, update the Model objects.

The way to enable automatic synchronization for a class is to have it inherit from System.ContextBoundObject and to decorate it with the SynchronizationAttribute like so:

Because View classes almost always inherit from the base "Form" class, they cannot inherit from ContextBoundObject as well. For this reason, these classes cannot safely change data on Model objects and must delegate those actions to the Controller classes.

Unfortunately, the result of this locking model is that, once again, the background thread will block the UI thread from servicing the user.

Advertiser Disclosure:
Some of the products that appear on this site are from companies from which QuinStreet receives compensation. This compensation may impact how and where products appear on this site including, for example, the order in which they appear. QuinStreet does not include all companies or all types of products available in the marketplace.

Thanks for your registration, follow us on our social networks to keep up-to-date