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:
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.
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.
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:
After that the application attempts to start some more calculations, since at least one thread is now available.
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:
// 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:
All single-hit asynchronous add-in functions follow this pattern. In summary: