Although it’s technically possible to call the same component synchronously and asynchronously, the likelihood that a component will be accessed both ways is low.
The reason is that using a set of components asynchronously requires drastic changes to the workflow of the client program, and as a result the client can’t simply use the same execution sequence logic that it would for synchronous access. Consider, for example, an online store application. Let’s suppose the client (a server-side object executing a customer request) accesses a Store
object, where it places the customer’s order details. The Store
object uses three well-factored helper components to process the order: Order
, Shipment
, and Billing
. In a synchronous scenario, the Store
object calls the Order
object to place the order. Only if the Order
object succeeds in processing the order (i.e., if the item is available in the inventory) does the Store
object call the Shipment
object, and only if the Shipment
object succeeds does the Store
object access the Billing
component to bill the customer. This sequence is shown in Figure 7-1.
The down side to the pattern shown in Figure 7-1 is that the store must process orders synchronously and serially. On the surface, it might seem that if the Store
component invoked its helper objects asynchronously, it could increase its throughput because it could process incoming orders as fast as the client submitted them. The problem in doing so is that it’s possible for the calls to the Order
, Shipment
, and Billing
objects to fail independently. Because their methods would be invoked in a nondeterministic order, depending on thread availability in the thread pool, overall system load, and so on, things could go wrong in many ways. For example, the Order
object might discover there were no items in the inventory matching the customer request after the Billing
object had already billed the customer for it.
Using asynchronous calls on a set of interacting components requires that you change your code and your workflow. To call the helper components asynchronously, the Store
component should call only the Order
object, which in turn should call the Shipment
object only if the order processing was successful (see Figure 7-2), to avoid the potential inconsistencies just mentioned. Similarly, only in the case of successful shipment should the Shipment
object asynchronously call the Billing
object.
In general, if you have more than one component in your asynchronous workflow, you should have each component invoke the next one in the logical execution sequence. Needless to say, such a programming model introduces tight coupling between components (they have to know about each other) and changes to their interfaces (you have to pass in additional parameters, which are required for the desired invocation of components downstream).
The conclusion from this simple example is that using asynchronous instead of synchronous invocation introduces major changes to the component interfaces and the client workflow. Asynchronous invocation on a component that was built for synchronous execution works only in isolated cases. When you’re dealing with a set of interacting components, it’s better to simply spin off a worker thread to call them and use the worker thread to provide asynchronous execution. This will preserve the component interfaces and the original client execution sequence. Of course, to do that, you need to understand .NET concurrency management and multithreading, the subjects of the next chapter.