[Previous] [Contents] [Next]

Call Cancellation

Client applications can make asynchronous calls to ensure that their threads are not blocked during the execution of a method. Sometimes the user might request that a long operation be canceled, so it can be useful for the client application to notify the server that it is no longer interested in the results of a call. COM+ supports call cancellation for such cases. Call cancellation in COM+ is handled through the ICancelMethodCalls interface, which is shown below in IDL notation:

interface ICancelMethodCalls : IUnknown
{
    // Called by the client
    // Requests that a method be canceled
    HRESULT Cancel          ([in] ULONG ulSeconds);

    // Called by the object
    // Determines if a call has been canceled
    HRESULT TestCancel      (void);
}

To request the cancellation of an outstanding call, the client obtains a pointer to the ICancelMethodCalls interface and then calls the ICancelMethodCalls::Cancel method. As long as the object uses standard marshaling, the object need not actually implement the ICancelMethodCalls interface; the proxy manager creates a cancel object that implements this interface when the call is marshaled. After the object requests the cancellation of a method, it will be unable to retrieve any return values and the method will return the RPC_E_CALL_CANCELED code. Objects that use custom marshaling must provide their own implementation of ICancelMethodCalls if they want to support call cancellation. The CoSwitchCallContext function enables custom-marshaled objects to install their own call context object. Proxies use the CoSetCancelObject function to register or unregister a cancel object for use during subsequent cancel operations on the calling thread.

Requesting Method Call Cancellation

When the client makes an asynchronous call, the cancellation request should follow at some point after the Begin_ method but before the Finish_ method. In the code fragment shown below, the client simply calls QueryInterface on the call object to obtain the ICancelMethodCalls interface implemented by the proxy manager and then calls ICancelMethodCalls::Cancel. The Cancel method accepts one argument that specifies the number of seconds the Cancel method should wait for the object to cancel the call. If the value is 0, the Cancel method returns immediately. A value of RPC_C_CANCEL_INFINITE_TIMEOUT indicates the client's willingness to wait however long it might take for the object to cancel the method. Note that for all values except 0, the specified timeout passed to the Cancel method is only a suggestion. The cancel object can consider factors such as the context of the call (in-process, out-of-process, or remote) and the threading model when determining how long to block. The return value of the Cancel method indicates whether the method finished executing (RPC_E_CALL_COMPLETE) or if the cancellation request was submitted (S_OK). Then the ICancelMethodCalls interface pointer is released.

ICancelMethodCalls* pCancelMethodCalls = 0;

hr = pSynchronize->QueryInterface(IID_ICancelMethodCalls, 
    (void**)&pCancelMethodCalls);
if(SUCCEEDED(hr))
{
    hr = pCancelMethodCalls->Cancel(0);

    if(hr == RPC_E_CALL_COMPLETE)
        cout << "The call finished executing before it was cancelled." 
            << endl;
    if(hr == S_OK)
        cout << "The cancel request was submitted." << endl;

    pCancelMethodCalls->Release();
}

When the client makes a synchronous method call, only another thread in the client process can issue the cancellation request because the thread that made the synchronous call is blocked for the duration of the method. In this scenario, COM+ provides the CoEnableCallCancellation and CoGetCancelObject functions. Unlike with asynchronous method calls, call cancellation is disabled by default for synchronous method calls. Before invoking any synchronous methods that might be canceled, the client thread must call the CoEnableCallCancellation function1—otherwise, the CO_E_CANCEL_DISABLED error will be returned by the ICancelMethodCalls::Cancel method. The client thread can then obtain its thread identifier by calling the GetCurrentThreadID Win32 API function before making a synchronous method call.

The code fragment below shows a client thread that gets its thread identifier and stores it in a global variable, enables call cancellation, spawns a new thread, and then makes a synchronous method call:

IPrime* pPrime = 0;
hr = CoCreateInstance(CLSID_InsideCOM, 0, CLSCTX_LOCAL_SERVER, 
    IID_IPrime, (void**)&pPrime);

// Get the current thread identifier and store it in a 
// global variable.
g_threadid = GetCurrentThreadId();

// Turn on call cancellation for this thread.
CoEnableCallCancellation(0);

// Start a new thread to cancel the synchronous method 
// call below.
DWORD newthread = 0;
HANDLE thread_handle = CreateThread(0, 0, 
    (LPTHREAD_START_ROUTINE)MyThread, 0, 
    0, &newthread);

int result = 0;
hr = pPrime->IsPrime(testnumber, &result);
if(hr == RPC_E_CALL_CANCELED)
    cout << "The call was canceled." << endl;
else if(SUCCEEDED(hr))
    if(result)
        cout << testnumber << " is prime." << endl;

pPrime->Release();

The newly created thread uses the thread identifier of the first thread when it calls CoGetCancelObject to obtain the ICancelMethodCalls interface implemented by the cancel object of the first thread. The ICancelMethodCalls::Cancel method is then called in the standard way, as shown in the code below. COM+ also provides the CoCancelCall helper function, which you can use in place of the CoGetCancelObject, ICancelMethodCalls::Cancel, and Release calls.

void __stdcall MyThread(DWORD nothing)
{
    HRESULT hr = CoInitializeEx(NULL, COINIT_MULTITHREADED);

    // Get the cancel object of the first thread.
    ICancelMethodCalls* pCancelMethodCalls = 0;
    hr = CoGetCancelObject(g_threadid, IID_ICancelMethodCalls, 
        (void**)&pCancelMethodCalls);

    // Request that the first thread's synchronous call 
    // be canceled.
    hr = pCancelMethodCalls->Cancel(0);

    // Release the cancel object.
    pCancelMethodCalls->Release();

    CoUninitialize();
}

Terminating the Method

Even when the client requests cancellation of a method call, there is no guarantee that the method executing in the object will actually abort. The object must be coded explicitly to detect a client's cancellation request via the ICancelMethodCalls::TestCancel method and then abort execution. The object obtains a pointer to the cancel object by calling the CoGetCallContext function and requesting an ICancelMethodCalls interface pointer. A modified version of the AsyncIPrime::Begin_IsPrime method that supports call cancellation is shown below; the relevant sections are in boldface:

HRESULT CPrimeCall::Begin_IsPrime(int testnumber)
{
    unsigned long count;
    unsigned long halfnumber = testnumber / 2 + 1;
    HRESULT hr = 0;

    for(count = 2; count < halfnumber; count++)
    {
        ICancelMethodCalls* pCancelMethodCalls = 0;

        CoGetCallContext(IID_ICancelMethodCalls, 
            (void**)&pCancelMethodCalls);

        hr = pCancelMethodCalls->TestCancel();

        pCancelMethodCalls->Release();

        if(hr == RPC_E_CALL_CANCELED)
            return RPC_E_CALL_CANCELED;

        if(testnumber % count == 0)
        {
            m_retval = false;
            m_pSynchronize->Signal();
            return S_OK;
        }

    m_retval = true;
    m_pSynchronize->Signal();
    return S_OK;
}

Alternatively, objects can call the CoTestCancel helper function to determine whether the client has requested the cancellation of a method call. CoTestCancel calls CoGetCallContext to obtain the ICancelMethodCalls interface pointer on the current cancel object, and then it calls the ICancelMethodsCalls::Cancel method. After the cancellation request is submitted, CoTestCancel automatically releases the cancel object.