WinINet asynchronous mode disaster - c++

Sorry for such a long question. It's just I've spent several days trying to solve my problem, and I'm exhausted.
I'm trying to use WinINet in an asynchronous mode. And I must say... this is simply insane. I really can't understand this. It does so many things, but unfortunately its asynchronous API is so much poorly designed that it just can't be used in a serious application with high stability demands.
My problem is the following: I need to do a lot of HTTP/HTTPS transactions serially, whereas I also need to be able to abort them immediately at request.
I was going to use WinINet in the followig way:
Initialize WInINet usage via InternetOpen function with INTERNET_FLAG_ASYNC flag.
Install a global callback function (via InternetSetStatusCallback).
Now, in order to perform a transaction that's what I thought to do:
Allocate a per-transaction structure with various members describing the transaction state.
Call InternetOpenUrl to initiate the transaction. In the asynchronous mode it usually immediately returns with an error, which is ERROR_IO_PENDING. One of its parameters is the 'context', the value which will be passed to the callback function. We set it to the pointer to the per-transaction state structure.
Very shortly after this the global callback function is called (from another thread) with status INTERNET_STATUS_HANDLE_CREATED. At this moment we save the WinINet session handle.
Eventually callback function is invoked with INTERNET_STATUS_REQUEST_COMPLETE when the transaction is complete. This allows us to use some notification mechanism (such as setting an event) to notify the originating thread that the transaction is complete.
The thread that issued the transaction realizes that it's complete. Then it does the cleanup: closes the WinINet session handle (by InternetCloseHandle), and deletes the state structure.
So far there seems to be no problem.
How to abort a transaction which is in the middle of execution? One way is to close the appropriate WinINet handle. And since WinINet doesn't have functions such as InternetAbortXXXX - closing the handle seems to be the only way to abort.
Indeed this worked. Such a transaction completes immediately with ERROR_INTERNET_OPERATION_CANCELLED error code.
But here all the problems begin...
The first unpleasant surprise that I've encountered is that WinINet tends to invoke sometimes the callback function for the transaction even after it has already been aborted.
According to the MSDN the INTERNET_STATUS_HANDLE_CLOSING is the last invocation of the callback function. But it's a lie. What I see is that sometimes there's a consequent INTERNET_STATUS_REQUEST_COMPLETE notification for the same handle.
I also tried to disable the callback function for the transaction handle right before closing it, but this didn't help. It seems that the callback invocation mechanism of the WinINet is asynchronous. Hence - it may call the callback function even after the transaction handle has been closed.
This imposes a problem: as long as WinINet may call the callback function - obviously I can't free the transaction state structure. But how the hell do I know whether or not WinINet will be so kind to call it? From what I saw - there's no consistency.
Nevertheless I've worked this around. Instead I now keep a global map (protected by the critical section of course) of allocated transaction structures. Then, inside the callback function I ensure that the transaction indeed exists and lock it for the duration of the callback invocation.
But then I've discovered another problem, which I couldn't solve so far. It arises when I abort a transaction very shortly after it's started.
What happens is that I call InternetOpenUrl, which returns the ERROR_IO_PENDING error code. Then I just wait (very short usually) until the callback function will be called with the INTERNET_STATUS_HANDLE_CREATED notification. Then - the transaction handle is saved, so that now we have an opportunity to abort without handle/resource leaks, and we may go ahead.
I tried to do the abort exactly after this moment. That is, close this handle immediately after I receive it.
Guess what happens? WinINet crashes, invalid memory access! And this is not related to whatever I do in the callback function. The callback function isn't even called, the crash is somewhere deep inside the WinINet.
On the other hand if I wait for the next notification (such as 'resolving name') - usually it works. But sometimes crashes as well!
The problem seems to disappear if I put some minimal Sleep between obtaining the handle and closing it. But obviously this can't be accepted as a serious solution.
All this makes me conclude: The WinINet is poorly designed.
There's no strict definition about the scope of the callback function invocation for the specific session (transaction).
There's no strict definition about the moment from which I'm allowed to close WinINet handle.
Who knows what else?
Am I wrong? Is that something that I don't understand? Or WinINet just can't be safely used?
EDIT:
This is the minimal code block that demonstrates the 2nd issue: crash.
I've removed all the error handling and etc.
HINTERNET g_hINetGlobal;
struct Context
{
HINTERNET m_hSession;
HANDLE m_hEvent;
};
void CALLBACK INetCallback(HINTERNET hInternet, DWORD_PTR dwCtx, DWORD dwStatus, PVOID pInfo, DWORD dwInfo)
{
if (INTERNET_STATUS_HANDLE_CREATED == dwStatus)
{
Context* pCtx = (Context*) dwCtx;
ASSERT(pCtx && !pCtx->m_hSession);
INTERNET_ASYNC_RESULT* pRes = (INTERNET_ASYNC_RESULT*) pInfo;
ASSERT(pRes);
pCtx->m_hSession = (HINTERNET) pRes->dwResult;
VERIFY(SetEvent(pCtx->m_hEvent));
}
}
void FlirtWInet()
{
g_hINetGlobal = InternetOpen(NULL, INTERNET_OPEN_TYPE_PRECONFIG, NULL, NULL, INTERNET_FLAG_ASYNC);
ASSERT(g_hINetGlobal);
InternetSetStatusCallback(g_hINetGlobal, INetCallback);
for (int i = 0; i < 100; i++)
{
Context ctx;
ctx.m_hSession = NULL;
VERIFY(ctx.m_hEvent = CreateEvent(NULL, FALSE, FALSE, NULL));
HINTERNET hSession = InternetOpenUrl(
g_hINetGlobal,
_T("http://ww.google.com"),
NULL, 0,
INTERNET_FLAG_NO_UI | INTERNET_FLAG_PRAGMA_NOCACHE | INTERNET_FLAG_RELOAD,
DWORD_PTR(&ctx));
if (hSession)
ctx.m_hSession = hSession;
else
{
ASSERT(ERROR_IO_PENDING == GetLastError());
WaitForSingleObject(ctx.m_hEvent, INFINITE);
ASSERT(ctx.m_hSession);
}
VERIFY(InternetCloseHandle(ctx.m_hSession));
VERIFY(CloseHandle(ctx.m_hEvent));
}
VERIFY(InternetCloseHandle(g_hINetGlobal));
}
Usually on first/second iteration the application crashes. One of the thread created by the WinINet generates an access violation:
Access violation reading location 0xfeeefeee.
Worth to note that the above address has special meaning to the code written in C++ (at least MSVC).
AFAIK when you delete an object that has a vtable (i.e. - has virtual functions) - it's set to the above address.
So that it's an attempt to call a virtual function of an already-deleted object.

declaration of Context ctx is the source of problem, it is declared within a for(;;) loop, so it's a local variable created for each loop, it will be destroyed and no longer accessible at end of each loop.
as a result, when a callback is invoked, ctx has been destroyed already, pointer being passed to callback points to a destroyed ctx, invalid memory pointer causes the crash.

Special thanks to Luke.
All the problems disappear when I explicitly use InternetConnect + HttpOpenRequest + HttpSendRequest instead of all-in-one InternetOpenUrl.
I don't receive any notifications on the request handle (not to confuse with the 'connection' handle). Plus no more crashes.

Related

C++ SetConsoleCtrlHandler, passing data for cleanup without globals

I'm trying to check when the console is closed through the close button on Windows. I read about SetConsoleCtrlHandler and I thought I'd use that, but there's some cleanup I want to do in my main function. I'll make a small example describing what I want to do for my larger program.
BOOL CtrlHandler( DWORD fdwCtrlType )
{
switch( fdwCtrlType )
{
//Cleanup exit
case CTRL_CLOSE_EVENT:
bool* programIsOn = &???; //How do I pass the address to that variable in this function?
*programIsOn = false;
return( TRUE );
default:
return FALSE;
}
}
int main(){
MyObject obj = new MyObject();
bool programIsOn = true;
//How do I pass the address of programIsOn here?
if(!SetConsoleCtrlHandler( (PHANDLER_ROUTINE) CtrlHandler, TRUE )){
cout << "Could not set CtrlHandler. Exiting." << endl;
return 0;
}
while(programIsOn){
//...
}
//CLEANUP HERE
delete obj;
return 0;
}
I want to perform cleanup when my program closes via the console close event, however if I just close the console the main function doesn't terminate and is forced to stop. I thought of passing in programIsOn's address to the CtrlHandler callback but I have no idea how to do this without using a global variable.
TL;DR: Proper handling of this control signal is complicated. Don't bother with any 'clean-up' unless it's absolutely necessary.
The system creates a new thread (see the Remarks) in your application, which is then used to execute the handler function you registered. That immediately causes a few issues and forces you in a particular design direction.
Namely, your program suddenly became multi-threaded, with all the complications that brings. Just setting a 'program should stop' (global) boolean variable to true in the handler is not going to work; this has to be done in a thread-aware manner.
Another complication this handler brings is that the moment it returns the program is terminated as per a call to ExitProcess. This means that the handler should wait for the program to finish, again in a thread-aware manner. Queue the next complication, where the OS gives you only 10 seconds to respond to the handler before the program is terminated anyway.
The biggest issue here, I think, is that all these issues force your program to be designed in a very particular way that potentially permeates every nook and cranny of your code.
It's not necessary for your program to clean up any handles, objects, locks or memory it uses: these will all be cleaned up by Windows when your program exits.
Therefore, your clean-up code should consists solely of those operations that need to happen and otherwise wouldn't happen, such as write the end of a log file, delete temporary files, etc.
In fact, it is recommended to not perform such clean-up, as it only slows down the closing of the application and can be so hard to get right in 'unexpected termination' cases; The Old New Thing has a wonderful post about it that's also relevant to this situation.
There are two general choices here for the way to handle the remaining clean-up:
The handler routine does all the clean-up, or
the main application does all the clean-up.
Number 1 has the issue that it's very hard to determine what clean-up to perform (as this depends on where the main program is currently executing) and it's doing so 'while the engine is still running'. Number 2 means that every piece of code in the the main application needs to be aware of the possibility of termination and have short-circuit code to handle such.
So if you truly must, necessarily, absolutely, perform some additional clean-up, choose method 2. Add a global variable, preferably a std::atomic<bool> if C++11 is available to you, and use that to track whether or not the program should exit. Have the handler set it to true
// Shared global variable to track forced termination.
std::atomic<bool> programShouldExit = false;
// In the console handler:
BOOL WINAPI CtrlHandler( DWORD fdwCtrlType )
{
...
programShouldExit = true;
Sleep(10000); // Sleep for 10 seconds; after this returns the program will be terminated if it hasn't already.
}
// In the main application, regular checks should be made:
if (programShouldExit.load())
{
// Short-circuit execution, such as return from function, throw exception, etc.
}
Where you can pick your favourite short-circuiting method, for instance throwing an exception and using the RAII pattern to guard resources.
In the console handler, we sleep for as long as we think we can get away with (it doesn't really matter); the hope is that the main thread will have exited by then causing the application to exit. If not, either the sleep ends, the handler returns and the application is closed, or the OS became impatient and killed the process.
Conclusion: Don't bother with clean-up. Even if there is something you prefer to have done, such as deleting temporary files, I'd recommend you don't. It's truly not worth the hassle (but that's my opinion). If you really must, then use thread-safe means to notify the main thread that it must exit. Modify all longer-running code to handle the exit status and all other code to handle the failure of the longer-running code. Exceptions and RAII can be used to make this more manageable, for instance.
And this is why I feel that it's a very poor design choice, born from legacy code. Just being able to handle an 'exit request' requires you to jump through hoops.

Why is this COM code leaking?

I'm maintaining an application which uses Windows Explorer overlay icons. Occasionally some operations require me to forcibly refresh explorers view for a particular folder. I do so using the following function which uses COM:
void RefreshExplorerView(CString strPath)
{
CComPtr<IShellWindows> pShellWindows;
CoInitialize(NULL);
if(SUCCEEDED(pShellWindows.CoCreateInstance(CLSID_ShellWindows)))
{
IDispatch* pFolder=NULL;
VARIANT variant;
V_VT(&variant) = VT_I4;
for(V_I4(&variant) = 0; pShellWindows->Item(variant, &pFolder) == S_OK; V_I4(&variant)++)
{
CComPtr<IWebBrowserApp> pWebBrowserApp;
if(SUCCEEDED(pFolder->QueryInterface(IID_PPV_ARGS(&pWebBrowserApp))))
{
BSTR LocationURL = NULL;
pWebBrowserApp->get_LocationURL(&LocationURL);
if(LocationURL != NULL && strPath.CompareNoCase(LocationURL) == 0)
{
CComPtr<IServiceProvider> pServiceProvider;
if(SUCCEEDED(pWebBrowserApp->QueryInterface(IID_PPV_ARGS(&pServiceProvider))))
{
CComPtr<IShellBrowser> pShellBrowser;
if(SUCCEEDED(pServiceProvider->QueryInterface(IID_PPV_ARGS(&pShellBrowser))))
{
IShellView* pShellView;
if(SUCCEEDED(pShellBrowser->QueryActiveShellView(&pShellView)))
{
pShellView->Refresh();
pShellView->Release();
}
}
}
}
SysFreeString(LocationURL);
}
pFolder->Release();
pFolder = NULL;
}
}
CoUninitialize();
}
I've noticed that when my program does this refresh regularly it slowly grows in size and UMDH has shown me that I appear to be leaking pFolder and pShellWindow instances every time this runs. I can't work out why on earth this happens since as far as I can tell these are released properly. Can anyone see what I'm missing?
You release pShellWindows after CoUninitialize, which is incorrect.
The rest of interfaces seem to be released fine. Note that you could improve cleanless and readability greatly by using CComQIPtr instead of QueryInterface, and not using raw pointers at all (BSTR, IFoo*) and replace them with smart auto-releasing wrappers.
pFolder might be leaking too, if Item call is successful but returns code other than S_OK. Again, use of CComPtr<IFolder> instead of IFolder* would immediately resolve this problem without even drawing any attention to it.
CoInitialize(NULL);
There's more than one problem with this statement. #Roman explained how you can leak by uninitializing too soon. But this will also go bad in more than one way, the apartment state of a thread is a Really Big Deal in COM:
You are not checking the return value of CoInitialize(). This will blow up the client app that calls this function if it has already called CoInitializeEx() and selected MTA instead of STA. That will make CoInitialize() fail, you cannot change the thread state after it was committed. Your CoUninitialize() call will blow the client app to smithereens, making all of its subsequent COM calls fail.
Selecting STA also requires that you implement the contract for a single threaded apartment. Which states that you never block the thread, you're okay with that. And that you pump a message loop. The message loop is crucial to marshaling calls to the single-threaded apartment. You are not okay with that, nor can you reasonably ensure that this is taken care of in a function like this. Particularly important for the shell interfaces, the vast majority of them are not thread-safe. The consequence of not pumping is deadlock. You may get away with not pumping, it is not a guaranteed deadlock. You'll get a bit of leeway here since these are probably out-of-process interfaces.
Particularly the last requirement can only be met by the code that created the thread that calls this function, only it is in control over what the thread does beyond calling your function. If you cannot get the guarantee that the client app initializes COM correctly then the only truly safe thing to do is to create a thread yourself.

can the infamous `ERROR_NETNAME_DELETED' error be considered an error at all? [duplicate]

This question already has an answer here:
Calling WSAGetLastError() from an IOCP thread return incorrect result
(1 answer)
Closed 7 years ago.
I'm writing a tcp server in Windows NT using completion ports to exploit asynchronous I/O.
I have a TcpSocket class, a TcpServer class and some (virtual functions) callbacks to call when an I/O operation is completed, e.g. onRead() for when a read is completed. I have also onOpen() for when the connection is established and onEof() for when the connection is closed, and so on.
I always have a pending read for the socket, so if the socket effectively gets data (the read will be completed with size > 0) it calls onRead(), instead if the client closes the socket from the client side (the read will be completed with size == 0) it calls onEof(), and the server is aware of when the client closes the socket with closesocket(server_socket); from its side.
All works gracefully, but I have noticed a thing:
when i call closesocket(client_socket); on the server's side endpoint of the connection, instead of the client side, (either with setting linger {true, 0} or not), the pending read will be completed as erroneous,
that is, the read size will not only be == 0, but also GetLastError() returns an error: 64, or 'ERROR_NETNAME_DELETED'. I have searched much about this on the web, but didn't find nothing interesting.
Then I asked myself: but is this a real error? I mean, can this really be considered an error?
The problem is that on the server side, the onError() callback will be called when I closesocket(client_socket); instead of the onEof(). So I thought this:
What about if I, when this 'ERROR_NETNAME_DELETED' "error" is received, call onEof() instead of onError() ?
Would that introduce some bugs or undefined behavior?
Another important point that made me ask this question is this:
When I have received this read completion with 'ERROR_NETNAME_DELETED', I have checked the OVERLAPPED
structure, in particular the overlapped->Internal parameter which contain the NTSTATUS error code
of the underlying driver. If we see a list of NTSTATUS error codes [ http://www.tenox.tc/links/ntstatus.html ]
we can clearly see that the 'ERROR_NETNAME_DELETED' is generated by the NTSTATUS 0xC000013B, which is an error, but it is called 'STATUS_LOCAL_DISCONNECT'. Well, it doesn't look like a name for an error. It seems more like `ERROR_IO_PENDING' which is an error, but also a status for a correct behavior.
So what about checking the OVERLAPPED structure's Internal parameter, and when this is == to 'STATUS_LOCAL_DISCONNECT' a call to the onEof() callback is performed? Would mess things up?
In addition, I have to say that from the server side, if I call DisconnectEx() before calling
closesocket(client_socket); I will not receive that error. But what about I don't want to call DisconnectEx() ? E.g. when the server is shutting down and doesn't want to wait all DisconnectEx() completions, but just want to close all client's connected.
It's entirely up to you how you treat an error condition. In your case this error condition is entirely to be expected, and it's perfectly safe for you to treat it as an expected condition.
Another example of this nature is when you call an API function but don't know how large a buffer to provide. So you provide a buffer that you hope will be big enough. But if the API call fails, you then check that the last error is ERROR_INSUFFICIENT_BUFFER. That's an expected error condition. You can then try again with a larger buffer.
It's up to you how to treat an error condition, but the question is a sign of potential problems in your code (from logic errors to undefined behavior).
The most important point is that you shouldn't touch SOCKET handle after closesocket. What do you do on EOF? It would be logical to closesocket on our side when we detect EOF, but that's what you cannot do in ERROR_NETNAME_DELETED handler, because closesocket already happened and the handle is invalid.
It's also profitable to imagine what happens if pending read completes (with real data available) just before closesocket, and your application detects it right after closesocket. You handle incoming data and... Do you send an answer to the client using the same socket handle? Do you schedule the next read on that handle? It would be all wrong, and there would be no ERROR_NETNAME_DELETED to tell you about it.
What happens if pending read completes with EOF in that very unfortunate moment, just before closesocket? If your regular OnEof callback is fired, and that callback does closesocket, it would be wrong again.
The problem you describe might hint at more serious problem if closesocket is done in one thread, while another thread waits for I/O completion. Are you sure that another thread is not calling WSARecv/ReadFile while the first thread is calling closesocket? That's undefined behavior, even though winsock makes it look as if it worked most of the time.
To summarize, the code handling completing (or failing) reads cannot be correct if it's unaware of socket handle being useless because it was closed. After closesocket, it's useful to wait for pending I/O completion because you can't reuse OVERLAPPED structure if you don't; but there's no point in handling this kind of completion as if it happened during normal operation, with socket being still open (error/status code is irrelevant).
You're calling the wrong method. You should be calling WSAGetLastError(). The result of GetLastError() after a Winsock API call is meaningless.

C++ - Should data passed to a thread be volatile?

In Microsoft Visual C++ I can call CreateThread() to create a thread by starting a function with one void * parameter. I pass a pointer to a struct as that parameter, and I see a lot of other people do that as well.
My question is if I am passing a pointer to my struct how do I know if the structure members have been actually written to memory before CreateThread() was called? Is there any guarantee they won't be just cached? For example:
struct bigapple { string color; int count; } apple;
apple.count = 1;
apple.color = "red";
hThread = CreateThread( NULL, 0, myfunction, &apple, 0, NULL );
DWORD WINAPI myfunction( void *param )
{
struct bigapple *myapple = (struct bigapple *)param;
// how do I know that apple's struct was actually written to memory before CreateThread?
cout << "Apple count: " << myapple->count << endl;
}
This afternoon while I was reading I saw a lot of Windows code on this website and others that passes in data that is not volatile to a thread, and there doesn't seem to be any memory barrier or anything else. I know C++ or at least older revisions are not "thread aware" so I'm wondering if maybe there's some other reason. My guess would be the compiler sees that I've passed a pointer &apple in a call to CreateThread() so it knows to write out members of apple before the call.
Thanks
No. The relevant Win32 thread functions all take care of the necessary memory barriers. All writes prior to CreateThread are visible to the new thread. Obviously the reads in that newly created thread cannot be reordered before the call to CreateThread.
volatile would not add any extra useful constraints on the compiler, and merely slow down the code. In practice thiw wouldn't be noticeable compared to the cost of creating a new thread, though.
No, it should not be volatile. At the same time you are pointing at the valid issue. Detailed operation of the cache is described in the Intel/ARM/etc papers.
Nevertheless you can safely assume that the data WILL BE WRITTEN. Otherwise too many things will be broken. Several decades of experience tell that this is so.
If thread scheduler will start thread on the same core, the state of the cache will be fine, otherwise, if not, kernel will flush the cache. Otherwise, nothing will work.
Never use volatile for interaction between threads. It is an instruction on how to handle data inside the thread only (use a register copy or always reread, etc).
First, I think optimizer cannot change the order at expense of the correctness. CreateThread() is a function, parameter binidng for function calls happens before the call is made.
Secondly, volatile is not very helpful for the purpose you intend. Check out this article.
You're struggling into a non-problem, and are creating at least other two...
Don't worry about the parameter given to CreateThread: if they exist at the time the thread is created they exist until CreateThread returns. And since the thread who creates them does not destroy them, they are also available to the other thread.
The problem now becomes who and when they will be destroyed: You create them with new so they will exist until a delete is called (or until the process terminates: good memory leak!)
The process terminate when its main thread terminate (and all other threads will also be terminated as well by the OS!). And there is nothing in your main that makes it to wait for the other thread to complete.
Beware when using low level API like CreateThread form languages that have thir own library also interfaced with thread. The C-runtime has _beginthreadex. It call CreateThread and perform also other initialization task for the C++ library you will otherwise miss. Some C (and C++) library function may not work properly without those initializations, that are also required to properly free the runtime resources at termination. Unsing CreateThread is like using malloc in a context where delete is then used to cleanup.
The proper main thread bnehavior should be
// create the data
// create the other thread
// // perform othe task
// wait for the oter thread to terminate
// destroy the data
What the win32 API documentation don't say clearly is that every HANDLE is waitable, and become signaled when the associate resource is freed.
To wait for the other thread termination, you main thread will just have to call
WaitForSingleObject(hthread,INFINITE);
So the main thread will be more properly:
{
data* pdata = new data;
HANDLE hthread = (HANDLE)_beginthreadex(0,0,yourprocedure, pdata,0,0);
WaitForSingleObject(htread,INFINITE);
delete pdata;
}
or even
{
data d;
HANDLE hthread = (HANDLE)_beginthreadex(0,0,yourprocedure, &d,0,0);
WaitForSingleObject(htread,INFINITE);
}
I think the question is valid in another context.
As others have pointed out using a struct and the contents is safe (although access to the data should by synchronized).
However I think that the question is valid if you hav an atomic variable (or a pointer to one) that can be changed outside the thread. My opinion in that case would be that volatile should be used in this case.
Edit:
I think the examples on the wiki page are a good explanation http://en.wikipedia.org/wiki/Volatile_variable

Cancelling scheduled work/io/timer items in WIN32 thread pool

I've been playing around with Windows' (new?) thread pool API. I've been following through with the example in the Using the Thread Pool Functions and I've been taking a good hard look at the API on MSDN. There's something I don't get about cleanup groups.
When invoking the SetThreadpoolCallbackCleanupGroup(), the third parameter is described as
The cleanup callback to be called if the cleanup group is canceled before the associated object is released. The function is called when you call CloseThreadpoolCleanupGroupMembers().
If my understanding is correct, the means that you can cancel pending work/io/timer items and ask it to invoke the cleanup callback function on each of these objects instead of the originally queue work/io/timer item's callback. This sounds cool, and I'd like to use it.
Unfortunately, the PTP_CLEANUP_GROUP_CANCEL_CALLBACK type used for the callback in question is not documented on MSDN and the example in question does not use this feature.
Taking the law into my own hands, I've traced back the definition to WinNT.h and found the following.
typedef VOID (NTAPI *PTP_CLEANUP_GROUP_CANCEL_CALLBACK)(
__inout_opt PVOID ObjectContext,
__inout_opt PVOID CleanupContext
);
Removing the cruft on this funny looking declaration gets you:
typedef void ( __stdcall * PTP_CLEANUP_GROUP_CANCEL_CALLBACK )
( void* ObjectContext, void* CleanupContext );
Question: If you would have to take an educated guess, what do you think ObjectContext and CleanupContext refer to?
My 1st guess is that CleanupContext is what you specify at the moment you initiate cleanup: thus the 3rd parameter to CloseThreadpoolCleanupGroupMembers(). I'm pretty confident this guess is correct because the API calls are so directly related.
My 2nd guess is that ObjectContext is what you specify at the moment you submit the work/io/timer item: this the 2nd parameter to CreateThreadpoolWork() et al. I'm totally unsure that this is the case.
Can someone confim that these guesses are correct? Has anyone used this feature before?
The optional cleanup callback you specify using the SetThreadpoolCallbackCleanupGroup function is called for each object that is associated with the same callback environment that has not already been closed by the time CloseThreadpoolCleanupGroupMembers is called. The callback’s first parameter, the object context, is the value of the void* parameter you specify when using the TrySubmitThreadpoolCallback, CreateThreadpoolWork, etc. functions. The callback’s second parameter, the cleanup context, is the value of the void* parameter you specify when using the CloseThreadpoolCleanupGroupMembers function.
The important thing to remember is that whether the cleanup callback is called for a particular object is not dependent on whether or not that object has outstanding callbacks. It is only called for objects that have not yet been closed. In other words it’s entirely possible that the object’s callback is called and then the cleanup callback is called for that same object.
If for example you create a work object using the CreateThreadpoolWork function and fail to call the CloseThreadpoolWork function prior to calling CloseThreadpoolCleanupGroupMembers then the cleanup callback will be called for that object even if the object’s callback has already executed. Failing to call CloseThreadpoolWork is not a bug as CloseThreadpoolCleanupGroupMembers will close any objects associated with the cleanup group.
Another twist to watch out for is when using the TrySubmitThreadpoolCallback function. This is a simpler version of CreateThreadpoolWork in that you don’t have to think about creating, submitting, and closing the work object. The trick is that the thread pool with automatically close the work object once its callback has executed. This means that the cleanup callback will only be called for this object if its callback is still pending and you specify TRUE when calling CloseThreadpoolCleanupGroupMembers to cancel any pending callbacks.