XLL+ Class Library

MtCalc.cpp

Threads

The worker thread and the thread dispatcher are discussed first.

unsigned long _stdcall WorkerThread(void* pvData) {
    CMtCalcData* data = (CMtCalcData*)pvData;
    
    // Do the long slow task
    data->Calculate();

    // Create a result message and post it to the main thread
    CMtCalcMsg* msg = new CMtCalcMsg(data);
    data->m_app->PostMessage(msg);

    return 0;
}

The worker thread runs the calculation, then posts the data packet back to the main thread, wrapped up in a message object.

void CMtCalcApp::StartCalculations() {
    // Only attempt to start as many threads as are allowed
    for (int i = m_nThreadMax - m_threads.size(); i > 0; i--) {
        // If the queue is empty give up
        if (m_queue.size() == 0)
            break;

        // Look at the first item in the queue
        CMtCalcData* data = *m_queue.begin();

        // Create a thread, but don't start it yet
        data->m_thread = CreateThread(NULL, 0, WorkerThread, data, 
            CREATE_SUSPENDED, NULL);
        if (data->m_thread == NULL)
            break;

        // Remove the data packet from the queue
        // - it is now owned by the thread
        m_queue.erase(m_queue.begin());

        // Note the new thread
        m_threads.insert(data);

        // Finally, kick off the new thread
        ResumeThread(data->m_thread);
    }
}

The dispatcher does the following steps:

  1. If no calculations are queued, exit.
  2. Create a new thread, suspended.
  3. Put the thread handle into the data packet. (We may need it later, to kill the thread.)
  4. Remove the packet from the queue.
  5. Add the new thread to the threads collection.
  6. Start the thread.

Initialisation: OnXllOpenEx()

OnXllOpenEx() is called once in the lifetime of the add-in, when it is opened. It must call the parent class method first, before any other initialisation. It should then initialize any application resources. If any of these initializations fail, it should show an error message and return FALSE.

int CMtCalcApp::OnXllOpenEx() {
    if (!CXllPushApp::OnXllOpenEx())
        return FALSE;

    // Set push properties
    SetFormatChangedCells(FALSE);

    // Register functions which will be pushed
    AddFunction("MtcValue");

    return TRUE;
}

Calling the parent class is essential, and must precede any other initialization.

After that the function initialises the push engine. It would be inappropriate to format changed cells, since cells change value only once, from "#WAIT!" to the result; therefore formatting is switched off.

Finally, the function returns TRUE to indicate successful completion.

Termination: OnXllClose()

OnXllClose() is called once in the lifetime of the add-in, just before it is closed. It should terminate any application resources.

void CMtCalcApp::OnXllClose() {
    threads_type::iterator it, itE;
    for (it = m_threads.begin(), itE = m_threads.end(); it != itE; it++) {
        ::CloseHandle((*it)->m_thread);
        delete *it;
    }
    m_threads.clear();
    m_bStopped = TRUE;
}

The function first closes any threads still running, and deletes their data. It then sets the m_bStopped flag to ensure that any messages already posted by background threads will be ignored during the rest of closedown.

ProcessAsyncMessage

CMtCalcApp::ProcessAsyncMessage() is called every time a message is received from the background thread. (The message will have been posted using CXllPushApp::PostMessage().)

void CMtCalcApp::ProcessAsyncMessage(CXllMtMsg* msg) {
    // If processing has stopped, then we're about to exit.
    // Just clean up and return.
    if (m_bStopped) {
        delete msg;
        return;
    }

    // Downcast the message
    CMtCalcMsg* myMsg = static_cast<CMtCalcMsg*>(msg);

    // Detach the data (so that is not deleted)
    CMtCalcData* data = myMsg->RemoveData();

    // Note the death of the worker thread
    m_threads.erase(data);

    // Update the cache
    m_cache.Set(data->m_strKey.c_str(), data);

    // Update any affected cells
    UpdateCells(data->m_strKey.c_str());

    // Delete the message (but not the data it contains)
    delete msg;

    // Kick off some more calculations
    StartCalculations();
}

This implementation has the usual four steps:

  1. Downcast the message to our message class.
  2. Put the new data in the data cache.
  3. Update any cells connected to the topic.
  4. Delete the message.

After that the application attempts to start some more calculations, since at least one thread is now available.

Asynchronous add-in function

Next, examine the add-in function itself, MtcValue(). This is a different shape from standard async add-in functions, since this is a single-hit function - the data changes only once, and thereafter remains unchanged.

extern "C" __declspec( dllexport )
LPXLOPER MtcValue(BOOL Put, double Spot, double Vol, double Loan,
    double Discount, double DivYield, long ValueDate, long
    Maturity, const COper* AvgInDates, const COper* AvgOutDates,
    long Iterations)
{
    CXlOper xloResult;
    BOOL bOk = TRUE;
    std::vector<long> vecAvgInDates;
    bOk = bOk && AvgInDates->ReadVector(vecAvgInDates, "AvgInDates", xloResult);
    std::vector<long> vecAvgOutDates;
    bOk = bOk && AvgOutDates->ReadVector(vecAvgOutDates, "AvgOutDates", xloResult);
    if (!bOk)
        return xloResult.Ret();
//}}XLP_SRC

    // Sort date vectors
    std::sort(vecAvgInDates.begin(), vecAvgInDates.end());
    std::sort(vecAvgOutDates.begin(), vecAvgOutDates.end());

    CMtCalcApp* app = (CMtCalcApp*)XllGetApp();
    CMtCalcData* dataOld = 0;

    // Build a data packet
    CMtCalcData* data = new CMtCalcData(Put ? 1 : 0,
        Spot, Vol, Loan, Discount, DivYield,
        ValueDate, Maturity, vecAvgInDates, vecAvgOutDates,
        Iterations);

    // Look up an old calculation
    if (app->m_cache.Lookup(data->m_strKey.c_str(), dataOld)) {
        if (dataOld == 0) {
            // We're still waiting
            xloResult = "#WAIT!";
            // Make sure we continue to wait
            app->AddConnection(data->m_strKey.c_str());
        }
        else {
            // Return result or error
            if (dataOld->m_ok)
                xloResult = dataOld->m_result;
            else
                xloResult.Format("#Error: %s", dataOld->m_strError.c_str());
            // Note - do not call AddConnection() so that we kill the link
        }
        delete data;
    }

    // Put the new calculation on the queue
    else {
        xloResult = "#WAIT!";
        app->m_cache.Set(data->m_strKey.c_str(), 0);
        app->m_queue.push_back(data);
        app->AddConnection(data->m_strKey.c_str());
        app->StartCalculations();
    }

    return xloResult.Ret();
}

Let us examine each section in turn.

    // Sort date vectors
    std::sort(vecAvgInDates.begin(), vecAvgInDates.end());
    std::sort(vecAvgOutDates.begin(), vecAvgOutDates.end());

Isn't STL wonderful?

    // Build a data packet
    CMtCalcData* data = new CMtCalcData(Put ? 1 : 0,
        Spot, Vol, Loan, Discount, DivYield,
        ValueDate, Maturity, vecAvgInDates, vecAvgOutDates,
        Iterations);

The CMtCalcData constructor does two important things:

  1. It makes copies of all the inputs, so that they can used by another thread.
  2. It creates a string key, concatenated from all the inputs.
    // Look up an old calculation
    if (app->m_cache.Lookup(data->m_strKey.c_str(), dataOld)) {
       ...
        delete data;
    }

The function attempts to look up the key in the data cache. If it is found, then we are dealing with a calculation that has already been requested, perhaps by this cell, perhaps by another. So we know that this calculation is either pending or complete, and we do not need to start a new calculation thread, and we will eventually be able to throw away the new data packet.

        if (dataOld == 0) {
            // We're still waiting
            xloResult = "#WAIT!";
            // Make sure we continue to wait
            app->AddConnection(data->m_strKey.c_str());
        }

If the data cache contains a null pointer then the calculation is still pending. We send back a place-holder result - "#WAIT!" - and call AddConnection() to ensure that the cell continues to wait for the result.

        else {
            // Return result or error
            if (dataOld->m_ok)
                xloResult = dataOld->m_result;
            else
                xloResult.Format("#Error: %s", dataOld->m_strError.c_str());
            // Note - do not call AddConnection() so that we kill the link
        }

If the data cache contains a valid pointer, then the calculation is complete. We return either the value or an error string, depending on the result of the calculation.

Note that we do not call AddConnection(). The connection is therefore dropped, and the cell will not be updated again. This is appropriate, since the calculation is complete, and the results will not change again.

    // Put the new calculation on the queue
    else {
        xloResult = "#WAIT!";
        app->m_cache.Set(data->m_strKey.c_str(), 0);
        app->m_queue.push_back(data);
        app->AddConnection(data->m_strKey.c_str());
        app->StartCalculations();
    }

The last section deals with a new calculation, which we have not encountered before. The function carries out the following steps:

  1. Sets the result to be "#WAIT!".
  2. Puts a null pointer in the cache for this key.
  3. Puts the new calculation packet into the queue.
  4. Registers a connection between the topic (the key containing all the inputs) and the cell that called the function.
  5. Attempts to kick off some calculations, by calling the dispatcher.

All single-hit asynchronous add-in functions follow this pattern. In summary:

  1. Create a topic key from the concatenated inputs.
  2. If the topic exists, but no result is available, return "#WAIT!", and call AddConnection().
  3. If the topic exists and the result is available, return the result, but do not call AddConnection().
  4. If the topic does not exist, call AddConnection() and start the calculation in a background thread.

Next: MtCalcGui.xla >>