Example: A Thread-Safe DLL for Socket Messages
Chapter 5). These two functions are similar to and essentially replace ReceiveMessage, listed earlier in this chapter, and the functions used in Program 12-1 and 12-2.The DllMain function is a representative solution of a multithreaded persistent state problem, and it combines TLS and DLLs. The resource deallocation in the DLL_THREAD_DETACH case is especially important in a server environment; without it, the server would eventually exhaust resources, typically resulting in either failure or performance degradation or both. Note: This example illustrates concepts that are not directly related to sockets, but it is included here, rather than in earlier chapters, because this is a convenient place to illustrate thread-safe DLL techniques in a realistic example.The book's Web site contains client and server code, slightly modified from Program 12-1 and 12-2, that uses this DLL.
Program 12-4. SendReceiveSKST: THRead-Safe DLL
/* SendReceiveSKST.c -- Multithreaded streaming socket DLL. */
/* Messages are delimited by end-of-line characters ('\0') */
/* so the message length is not known ahead of time. Incoming */
/* data is buffered and preserved from one function call to */
/* the next. Therefore, use Thread Local Storage (TLS) */
/* so that each thread has its own private "static storage." */
#define _NOEXCLUSIONS
#include "EvryThng.h"
#include "ClntSrvr.h" /* Defines request and response records. */
typedef struct STATIC_BUF_T {
/* "static_buf" contains "static_buf_len" bytes of residual data. */
/* There may or may not be end-of-string (null) characters. */
char static_buf [MAX_RQRS_LEN];
LONG32 static_buf_len;
} STATIC_BUF;
static DWORD TlsIx = 0; /* TLS index -- EACH PROCESS HAS ITS OWN. */
/* A single-threaded library would use the following:
static char static_buf [MAX_RQRS_LEN];
static LONG32 static_buf_len; */
/* DLL main function. */
BOOL WINAPI DllMain (HINSTANCE hinstDLL,
DWORD fdwReason, LPVOID lpvReserved)
{
STATIC_BUF * pBuf;
switch (fdwReason) {
case DLL_PROCESS_ATTACH:
TlsIx = TlsAlloc ();
/* There is no thread attach for the primary thread, so it is
necessary to carry out the thread attach operations as well
during process attach. */
case DLL_THREAD_ATTACH:
/* Indicate that memory has not been allocated. */
TlsSetValue (TlsIx, NULL);
return TRUE; /* This value is actually ignored. */
case DLL_PROCESS_DETACH:
/* Detach the primary thread as well. */
pBuf = TlsGetValue (TlsIx);
if (pBuf != NULL) {
free (pBuf);
pBuf = NULL;
}
return TRUE;
case DLL_THREAD_DETACH:
pBuf = TlsGetValue (TlsIx);
if (pBuf != NULL) {
free (pBuf);
pBuf = NULL;
}
return TRUE;
}
}
_declspec (dllexport)
BOOL ReceiveCSMessage (REQUEST *pRequest, SOCKET sd)
{
/* TRUE return indicates an error or disconnect. */
BOOL Disconnect = FALSE;
LONG32 nRemainRecv = 0, nXfer, k; /* Must be signed integers. */
LPSTR pBuffer, message;
CHAR TempBuf [MAX_RQRS_LEN + 1];
STATIC_BUF *p;
p = (STATIC_BUF *) TlsGetValue (TlsIx);
if (p == NULL) { /* First-time initialization. */
/* Only threads that need this storage will allocate it. */
/* Other thread types can use the TLS for other purposes. */
p = malloc (sizeof (STATIC_BUF));
TlsSetValue (TlsIx, p);
if (p == NULL) return TRUE; /* Error. */
p->static_buf_len = 0; /* Initialize state. */
}
message = pRequest->Record;
/* Read up to the new-line character, leaving residual data
in the static buffer. */
for (k = 0;
k < p->static_buf_len && p->static_buf [k] != '\0'; k++) {
message [k] = p->static_buf [k];
} /* k is the number of characters transferred. */
if (k < p->static_buf_len) { /* A null was found in static buf. */
message [k] = '\0';
p->static_buf_len -= (k + 1); /* Adjust static buffer state. */
memcpy (p->static_buf, &(p->static_buf [k + 1]),
p->static_buf_len);
return FALSE; /* No socket input required. */
}
/* The entire static buffer was transferred. No eol found. */
nRemainRecv = sizeof (TempBuf) - 1 - p->static_buf_len;
pBuffer = message + p->static_buf_len;
p->static_buf_len = 0;
while (nRemainRecv > 0 && !Disconnect) {
nXfer = recv (sd, TempBuf, nRemainRecv, 0);
if (nXfer <= 0) {
Disconnect = TRUE;
continue;
}
nRemainRecv -= nXfer;
/* Transfer to target message up to null, if any. */
for (k = 0; k < nXfer && TempBuf [k] != '\0'; k++) {
*pBuffer = TempBuf [k];
pBuffer++;
}
if (k >= nXfer) { /* End of line not found, read more. */
nRemainRecv -= nXfer;
} else { /* End of line has been found. */
*pBuffer = '\0';
nRemainRecv = 0;
memcpy (p->static_buf, &TempBuf [k + 1], nXfer - k - 1);
p->static_buf_len = nXfer - k - 1;
}
}
return Disconnect;
}
_declspec (dllexport)
BOOL SendCSMessage (RESPONSE *pResponse, SOCKET sd)
{
/* Send the request to the server on socket sd. */
BOOL Disconnect = FALSE;
LONG32 nRemainSend, nXfer;
LPSTR pBuffer;
pBuffer = pResponse->Record;
nRemainSend = strlen (pBuffer) + 1;
while (nRemainSend > 0 && !Disconnect) {
/* Send does not guarantee that the entire message is sent. */
nXfer = send (sd, pBuffer, nRemainSend, 0);
if (nXfer <= 0) {
fprintf (stderr,
"\nServer disconnect before complete request sent");
Disconnect = TRUE;
}
nRemainSend -=nXfer; pBuffer += nXfer;
}
return Disconnect;
}
Comments on the DLL and Thread Safety
- DllMain, with DLL_THREAD_ATTACH, is called whenever a new thread is created, but there is not a distinct DLL_THREAD_ATTACH call for the primary thread. The DLL_PROCESS_ATTACH case must handle the primary thread.
- In general, and even in this case (consider the accept thread), some threads may not require the allocated memory, but DllMain cannot distinguish the different thread types. Therefore, the DLL_THREAD_ATTACH case does not actually allocate any memory; it only initializes the TLS value. The ReceiveCSMessage entry point allocates the memory the first time it is called. In this way, the thread-specific memory is allocated only by threads that require it, and different thread types can allocate exactly the resources they require.
- While this DLL is thread-safe, a given thread can use these routines with only one socket at a time because the persistent state is associated with the thread, not the socket. The next example addresses this issue.
- The DLL source code on the Web site is instrumented to print the total number of DllMain calls by type.
- There is still a resource leak risk, even with this solution. Some threads, such as the accept thread, may never terminate and therefore will never be detached from the DLL. ExitProcess will call DllMain with DLL_PROCESS_DETACH but not with DLL_THREAD_DETACH for threads that are still active. This does not cause a problem in this case because the accept thread does not allocate any resources, and even memory is freed when the process terminates. There would, however, be an issue if threads allocated resources such as temporary files; the ultimate solution would be to create a globally accessible list of resources. The DLL_PROCESS_DETACH code would then have the task of scanning the list and deallocating the resources.