Click here to Skip to main content
11,647,659 members (75,084 online)
Click here to Skip to main content

Tagged as

Avoiding Data Loss in Thread Communication

, 7 Sep 2010 CPOL 14.5K 12
Rate this:
Please Sign up or sign in to vote.
Avoiding Data Loss in Thread Communication

Contents

  • 1 - Problem
  • 2 - Solution
  • 3 - Program
  • 3.1 - Buffer
  • 3.2 - Development
  • 3.2.1 - Buffer Creation
  • 3.2.2 - Write in the Buffer
  • 3.2.3 - Read from the Buffer
  • 3.2.4 - Create Nodes
  • 3.2.5 - Delete the Buffer
  • 4 - Test
  • 5 - Makefile

1 - Problem

Suppose you have a device that makes readings and sends them to a computer through, for instance, a socket. If between two readings the computer has to do some calculations it is possible that, when the rate of the communication is higher, some data will be lost on the computer side.

Socket.jpg

2 - Solution

To solve this situation, you can create a thread in the computer side witch only task is to take the data out of the socket and put it in a buffer where the main program will go read it as soon as he is able to. Besides, as you don't know witch is the best length for the buffer, it will be even better if the buffer can grow as it is needed.

Thread.jpg

3 - Program

3.1 - Buffer

The buffer is a ring of a variable number of nodes each one with the following constitution:

  • A pointer to the next node in the ring

  • A pointer to a cell of fixed length where an item of data is stored

  • A character witch indicates the last operation done in the cell, a reading or a writing

Besides the nodes it also has a control structure, called "Ring", witch has the following constitution:

  • A pointer to the last read node

  • A pointer to the last wrote node

  • The number of nodes in the buffer

  • The size in bytes of the cell

Ring.jpg

3.2 - Development

The program developed consists of the necessary functions to:

  • Create the buffer
  • Write in the buffer
  • Read from the buffer
  • Add more space to the buffer
  • Delete the buffer

3.2.1 - Buffer Creation

To create the buffer you use the following function:

void ring_init(ring_struct *ring, int cell_size)
{
    node_struct *fst = NULL, *lst = NULL;
    ring_create_nodes(NODES_ON_INIT, cell_size, &fst, &lst);
    ring->r = ring->w = lst->nxt = fst;
 
    ring->node_num = 
NODES_ON_INIT;
    ring->cell_size = cell_size;
}

It begins by allocating NODES_ON_INIT nodes, then adjusts the pointers in the ring structure and makes the last created node to point at the first and finally fills the remaining of the ring structure.

3.2.2 - Write in the Buffer

The following function is used when it is needed to write in the buffer:

int ring_write(ring_struct *ring, void *cell_source)
{
    node_struct *fst = NULL, *lst = NULL;
    if(ring->w->nxt->status == 'r')
    {
        ring->w = ring->w->nxt;
        memmove(ring->w->cell, cell_source, 
ring->cell_size);
        ring->w->status = 'w';
    }
    else
    {
        ring->node_num += NODES_ON_ADD;
        if((NODES_MAX_NUM != 0) && (ring->node_num 
> NODES_MAX_NUM))
            return(-1);
        ring_create_nodes(NODES_ON_ADD, 
ring->cell_size, &fst, &lst);
        lst->nxt = ring->w->nxt;
        ring->w->nxt = fst;
        ring_write(ring, cell_source);
    }
    return(0);
}

Here you can find two situations, the next node is marked as read or it is marked as wrote. In the former case there is no problem of writing over it, in the second case if you write over it you are loosing data because that node was not read yet.

As sought, in the former case you point to the next node, copy the data into the cell and mark the node as wrote. In the second case there are a few more steps to take. In order to write more data you are going to make the buffer bigger by adding to it some mode nodes. First because the buffer could be configured with a maximum size you ask if with the nodes you are about to add you don't reach that size yet. If it is ok you call the function that creates the nodes to add more NODES_ON_ADD nodes witch are created with the read flag, adjust the pointers and the function call itself because now the next node is already marked as read.

3.2.3 - Read from the Buffer

To read from the buffer you use the following function:

int 
ring_read(ring_struct *ring, void *cell_dest)
{
    if(ring->r->nxt->status == 'r')
        return(0);
    else
    {
        ring->r = ring->r->nxt;
        memmove(cell_dest, ring->r->cell, 
ring->cell_size);
        ring->r->status = 'r';
        return(1);
    }
}

Here you ask again if the next node is marked as read or as wrote. In the former case there is nothing to read because all cells are marked with read. If it is marked as wrote then you can make a reading by copying the contents of the cell, adjusting the pointers and marking the node as read.

3.2.4 - Create Nodes

The simple but more complex function is to create nodes. The code to do that is:

void ring_create_nodes(int n, int cell_size, node_struct **fst, 
node_struct **lst)
{
    int i;
    node_struct *node = NULL, *ptr = NULL;
    for(i = 0; i < n; i++)
    {
        node = (node_struct *) 
malloc(sizeof(node_struct));
        if(!node)
            
ring_error_and_exit((char *) "Error in 'malloc' for 'node'");
        node->status = 'r';
        node->cell = malloc(cell_size);
        if(!node->cell)
            
ring_error_and_exit((char *) "Error in 'malloc' for 'cell'");
        if(!i)
        {
            *lst = node;
            node->nxt = 
NULL;
        }
        else
            node->nxt = 
ptr;
        ptr = node;
    }
    *fst = node;
}

The goal here is to create a finite number of nodes connected between them and with the first and last node opened. For that you create a cycle that, for the number of nodes that you want to create, begins by allocating space for the node, for the cell, marking the node as read and connecting it to the last created node. If it is the first node to be created (it will be the last in the queue because the queue grows to the back) it will point to NULL and the argument LST witch must points to the last node, will point to it. Finally the argument FST, that must point to the first node in the queue, points to the last created node.

3.2.5 - Delete the Buffer

When there is no more need for the buffer you must delete it. To do so you use the following code:

void ring_free(ring_struct *ring)
{
    node_struct *fst = NULL, *del = NULL;
    fst = ring->w;
    while(1)
    {
        del = ring->w;
        ring->w = del->nxt;
        free(del->cell);
        free(del);
        if(ring->w == fst)
            break;
    }
}

You begin by any node in the buffer, you take note of it and then you go by all nodes in the buffer freeing the space allocated to the cell and then the space allocated to the node until you reach the first node.

4 - Test

In order to test the program it was developed an application witch creates a thread that writes data to the buffer and the main program reads it and shows it on the screen. It was included a delay in the writing and a bigger one in the reading. This way the buffer gets full and it is possible to test it's growing. It was also included a maximum dimension for the buffer that when it is reached makes the application to end.

5 - Makefile

To build and test the whole application it was created a makefile that compiles all the modules. The application was tested under Cygwin environment but it will run as well under any Linux or Unix environment.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Luis Vital
Software Developer
Portugal Portugal
No Biography provided

You may also be interested in...

Comments and Discussions

 
GeneralBetter solutions - Missing Concurrency Management Pin
federico.strati14-Sep-10 2:57
memberfederico.strati14-Sep-10 2:57 
The solution you discuss is flawed in respect of the concurrent access of the buffer by the reader and writer threads. Your solution may lead to data corruption as a thread access does not consider the locking of the shared resource (i.e. the buffer). It is not enough to mark the node as read or wrote. I urge you to reconsider your solution. If you used such solution in a software in a production environment you may end up with a "lossy" bug: your code may simply misteriously ends up not processing at all some nodes. In realtime processing systems (I work with by 7+ years) it is customary to tackle such aspects with a technique known as "circular Buffer" in a situation where only one reader and one writer thread are present. For instance, you may want to take a look at the following link: http://www.codeproject.com/KB/threads/LockFree.aspx Lock-Free Single-Producer - Single Consumer Circular Queue If more than one reader / writer threads are present you need to use explicit locking with a threaded queue. I attach here a couple of C++ classes developed for such purposes under Windows. You may use them under Linux just replacing the Heap allocation functions with malloc / free calls and using a mutex instead of a critical section (available only under windows). First the solution for just two threads: 1 reader / 1 writer: //---------- ! Circular Buffer Class - Start ------------ //---------- Start header file: CircBuffer.h ------------ // Light Weight Circular Buffer with elementary locking #ifndef CBUFFER_H #define CBUFFER_H #if _MSC_VER &gt; 1000 #pragma warning (disable: 4786) #pragma warning (disable: 4748) #pragma warning (disable: 4103) #endif /* _MSC_VER &gt; 1000 */ #include &lt;afx.h&gt; #include &lt;afxwin.h&gt; // Default number of elements in buffer #define CBUFFER_NELEM 0x0000FFFF // The Circular Buffer errors #define CBUFFER_ERRCODE_OK 0 #define CBUFFER_ERRCODE_NO_MEMORY 1 #define CBUFFER_ERRCODE_READ_ERROR 2 #define CBUFFER_ERRCODE_WRITE_ERROR 3 #define CBUFFER_ERRCODE_NO_DATA 4 #define CBUFFER_NULL_PTR 0xFFFFFFFF class CCircBuffer { // Constructors, Destructor public: CCircBuffer(UINT32 unElements = CBUFFER_NELEM); ~CCircBuffer(void); // Operations public: // Read the data UINT32 GetTheData(void*&amp; lpData); // Write the data UINT32 SetTheData(void* lpData); // Increment the read pointer UINT32 IncTheReadPtr(void); // Increment the write pointer UINT32 IncTheWritePtr(void); // Check if there is any space for reading data bool IsReadyForRead(void); // Check if there is any space for writing new data bool IsReadyForWrite(void); // Get the last known value for the read pointer UINT32 GetLastReadPtr(void); // Get the last known value for the write pointer UINT32 GetLastWritePtr(void); // Attributes private: // The Circular Buffer of void pointers void** TheCircularBuffer; SIZE_T unElems; // The Circular Buffer pointers UINT32 unReadPtr, unWritePtr; // The Circular Buffer last error UINT32 unLastErrorCode; }; #endif /* ! defined(CBUFFER_H) */ //---------- Start implementation file: CircBuffer.cpp -- #include "StdAfx.h" #include "CircBuffer.h" CCircBuffer::CCircBuffer(UINT32 unElements /* = CBUFFER_NELEM */) { unLastErrorCode = CBUFFER_ERRCODE_OK; // Allocate the Buffer unElems = (SIZE_T) unElements; if ( unElems &gt;= CBUFFER_NULL_PTR ) // max size unElems = CBUFFER_NULL_PTR - 1; TheCircularBuffer = (void **) HeapAlloc(GetProcessHeap(), 0, unElems * sizeof(void*)); // Cannot allocate memory if(!TheCircularBuffer) { unLastErrorCode = CBUFFER_ERRCODE_NO_MEMORY; } else { SecureZeroMemory(TheCircularBuffer, sizeof(TheCircularBuffer)); } // Set Pointers unReadPtr = unWritePtr = 0; } CCircBuffer::~CCircBuffer(void) { unLastErrorCode = CBUFFER_ERRCODE_OK; BOOL bResult = HeapFree(GetProcessHeap(), 0, TheCircularBuffer); if( bResult != TRUE ) { unLastErrorCode = CBUFFER_ERRCODE_NO_MEMORY; } } // Operations // Read the data UINT32 CCircBuffer::GetTheData(void*&amp; lpData) { unLastErrorCode = CBUFFER_ERRCODE_OK; // Check if there is any data to be read in the buffer if( ! IsReadyForRead() ) { // No data to be read in buffer unLastErrorCode = CBUFFER_ERRCODE_NO_DATA; return CBUFFER_NULL_PTR; } // Get the data lpData = (void*) TheCircularBuffer[unReadPtr]; // Increment the read pointer unReadPtr++; if( unReadPtr &gt;= unElems ) unReadPtr = 0; // Restart return unReadPtr; } // Write the data UINT32 CCircBuffer::SetTheData(void* lpData) { unLastErrorCode = CBUFFER_ERRCODE_OK; // Check if there is any way of writing the data if( ! IsReadyForWrite() ) { // No way to write data in Buffer unLastErrorCode = CBUFFER_ERRCODE_NO_DATA; return CBUFFER_NULL_PTR; } // Set the data TheCircularBuffer[unWritePtr] = (void*) lpData; // Increment the write pointer unWritePtr++; if( unWritePtr &gt;= unElems ) unWritePtr = 0; // Restart return unWritePtr; } // Increment the read pointer UINT32 CCircBuffer::IncTheReadPtr(void) { unLastErrorCode = CBUFFER_ERRCODE_OK; // Check if there is any data to be read in the buffer if ( ! IsReadyForRead() ) { // No data available to be read unLastErrorCode = CBUFFER_ERRCODE_NO_DATA; return CBUFFER_NULL_PTR; } // Increment the read pointer unReadPtr++; if( unReadPtr &gt;= unElems ) unReadPtr = 0; // restart return unReadPtr; } // Increment the write pointer UINT32 CCircBuffer::IncTheWritePtr(void) { unLastErrorCode = CBUFFER_ERRCODE_OK; // Checks if there is any way of writing the data if( ! IsReadyForWrite() ) { // No way to write data unLastErrorCode = CBUFFER_ERRCODE_NO_DATA; return CBUFFER_NULL_PTR; } // Increment the write pointer unWritePtr++; if( unWritePtr &gt;= unElems ) unWritePtr = 0; // restart return unWritePtr; } // Check if there is any way of reading the data bool CCircBuffer::IsReadyForRead(void) { unLastErrorCode = CBUFFER_ERRCODE_OK; // Check if there is any way of reading the data // The test pointer UINT32 unCheckReadPtr = unReadPtr; if( unCheckReadPtr &gt;= unElems ) unCheckReadPtr = 0; // Restart if( unCheckReadPtr == unWritePtr ) { // No way to read data unLastErrorCode = CBUFFER_ERRCODE_NO_DATA; return false; } return true; } // Check if there is any space for writing new data bool CCircBuffer::IsReadyForWrite(void) { unLastErrorCode = CBUFFER_ERRCODE_OK; // Checks if there is any way of writing the data // Increment the test pointer UINT32 unCheckWritePtr = unWritePtr; unCheckWritePtr++; if( unCheckWritePtr &gt;= unElems ) unCheckWritePtr = 0; // Restart if( unCheckWritePtr == unReadPtr ) { // No way to write data unLastErrorCode = CBUFFER_ERRCODE_NO_DATA; return false; } return true; } // Get the last known value for the read pointer UINT32 CCircBuffer::GetLastReadPtr(void) { unLastErrorCode = CBUFFER_ERRCODE_OK; return unReadPtr; } // Get the last known value for the write pointer UINT32 CCircBuffer::GetLastWritePtr(void) { unLastErrorCode = CBUFFER_ERRCODE_OK; return unWritePtr; } //---------- ! Circular Buffer Class - End ------------ Second the solution for more than two threads: //---------- ! Threading Queue Class - Start ------------ // Class defined completely in the header file ThreadQueue.h // High performance queue... #ifndef CTHREADQUEUE_H #define CTHREADQUEUE_H #if _MSC_VER &gt; 1000 #pragma warning (disable: 4786) #pragma warning (disable: 4748) #pragma warning (disable: 4103) #endif /* _MSC_VER &gt; 1000 */ #if ! defined( QUEUE_DEFAULT_NUMBER_OF_ITEMS ) #define QUEUE_DEFAULT_NUMBER_OF_ITEMS (2048) #endif class CThreadQueue { private: // Don't allow these sorts of things inline CThreadQueue( const CThreadQueue&amp; ) {}; inline CThreadQueue&amp; operator = ( const CThreadQueue&amp; ){ return( *this ); }; protected: // What we want to protect CRITICAL_SECTION m_AddCriticalSection; CRITICAL_SECTION m_GetCriticalSection; void ** m_Items; SIZE_T m_AddIndex; SIZE_T m_GetIndex; SIZE_T m_Size; HANDLE m_Heap; inline void m_GrowBy( SIZE_T number_of_new_items ); public: inline CThreadQueue( SIZE_T initial_size = QUEUE_DEFAULT_NUMBER_OF_ITEMS ); inline ~CThreadQueue(); inline BOOL Add( SIZE_T new_item ) { return( Add( (void *) new_item ) ); }; inline BOOL Add( void * new_item ); inline void Empty( void ) { ::EnterCriticalSection( &amp;m_AddCriticalSection ); ::EnterCriticalSection( &amp;m_GetCriticalSection ); m_AddIndex = 0; m_GetIndex = 0; ::LeaveCriticalSection( &amp;m_GetCriticalSection ); ::LeaveCriticalSection( &amp;m_AddCriticalSection ); }; inline BOOL Get( SIZE_T&amp; item ) { return( Get( reinterpret_cast&lt; void *&amp; &gt;( item ) ) ); }; inline BOOL Get( void * &amp; item ); inline SIZE_T GetLength( void ) const; inline SIZE_T GetMaximumLength( void ) const { return( m_Size ); }; }; inline CThreadQueue::CThreadQueue( SIZE_T initial_size ) { m_AddIndex = 0; m_GetIndex = 0; m_Items = NULL; if ( initial_size == 0 ) { initial_size = 1; } m_Heap = ::HeapCreate( HEAP_NO_SERIALIZE, ( ( ( 2 * initial_size * sizeof( void * ) ) &lt; 65536 ) ? 65536 : (2 * initial_size * sizeof( void * ) ) ), 0 ); m_Items = (void **) ::HeapAlloc( m_Heap, HEAP_NO_SERIALIZE, initial_size * sizeof( void * ) ); m_Size = ( m_Items == NULL ) ? 0 : initial_size; ::InitializeCriticalSection( &amp;m_AddCriticalSection ); ::InitializeCriticalSection( &amp;m_GetCriticalSection ); } inline CThreadQueue::~CThreadQueue() { m_AddIndex = 0; m_GetIndex = 0; m_Size = 0; if ( m_Items != NULL ) { ::HeapFree( m_Heap, HEAP_NO_SERIALIZE, m_Items ); m_Items = NULL; } ::HeapDestroy( m_Heap ); m_Heap = NULL; ::DeleteCriticalSection( &amp;m_AddCriticalSection ); ::DeleteCriticalSection( &amp;m_GetCriticalSection ); } inline BOOL CThreadQueue::Add( void * item ) { // Block other threads from entering Add(); ::EnterCriticalSection( &amp;m_AddCriticalSection ); // Add the item m_Items[ m_AddIndex ] = item; // Make sure m_AddIndex is never invalid SIZE_T new_add_index = ( ( m_AddIndex + 1 ) &gt;= m_Size ) ? 0 : m_AddIndex + 1; // Check to see if the queue is full. We need to grow. // Stop anyone from getting from the queue ::EnterCriticalSection( &amp;m_GetCriticalSection ); if ( new_add_index == m_GetIndex ) { m_AddIndex = new_add_index; // One last double-check. if ( m_AddIndex == m_GetIndex ) { m_GrowBy( m_Size ); } } else { m_AddIndex = new_add_index; } ::LeaveCriticalSection( &amp;m_GetCriticalSection ); // Let other threads call Add() now. ::LeaveCriticalSection( &amp;m_AddCriticalSection ); return( TRUE ); } inline BOOL CThreadQueue::Get( void * &amp; item ) { // Prevent other threads from entering Get() ::EnterCriticalSection( &amp;m_GetCriticalSection ); if ( m_GetIndex == m_AddIndex ) { // Let's check to see if our queue has grown too big // If it has, then shrink it if ( m_Size &gt; QUEUE_DEFAULT_NUMBER_OF_ITEMS ) { // too big... if ( ::TryEnterCriticalSection( &amp;m_AddCriticalSection ) != 0 ) { // Now, no one can add to the queue if ( m_GetIndex == m_AddIndex ) { // See if we can just shrink it... It is safe to use HeapReAlloc() because the queue is empty void * return_value = (void *) ::HeapReAlloc( m_Heap, HEAP_NO_SERIALIZE, m_Items, QUEUE_DEFAULT_NUMBER_OF_ITEMS * sizeof( void * ) ); if ( return_value != NULL ) { m_Items = (void **) return_value; } else { // Looks like we'll have to do it the hard way ::HeapFree( m_Heap, HEAP_NO_SERIALIZE, m_Items ); m_Items = (void **) ::HeapAlloc( m_Heap, HEAP_NO_SERIALIZE, QUEUE_DEFAULT_NUMBER_OF_ITEMS * sizeof( void * ) ); } m_Size = ( m_Items == NULL ) ? 0 : QUEUE_DEFAULT_NUMBER_OF_ITEMS; m_AddIndex = 0; m_GetIndex = 0; } else { // m_GetIndex != m_AddIndex, this means that someone added // to the queue between the time we checked m_Size for being // too big and the time we entered the add critical section. // If this happened then we are too busy to shrink } // Let people add to the queue once again ::LeaveCriticalSection( &amp;m_AddCriticalSection ); } } // Let other threads call Get() now, we are empty ::LeaveCriticalSection( &amp;m_GetCriticalSection ); return( FALSE ); } item = m_Items[ m_GetIndex ]; // Make sure m_GetIndex is never invalid m_GetIndex = ( ( m_GetIndex + 1 ) &gt;= m_Size ) ? 0 : m_GetIndex + 1; // Let other threads call Get() now ::LeaveCriticalSection( &amp;m_GetCriticalSection ); return( TRUE ); } inline SIZE_T CThreadQueue::GetLength( void ) const { // This is a very expensive process! // No one can call Add() or Get() while we're computing this SIZE_T number_of_items_in_the_queue = 0; ::EnterCriticalSection( const_cast&lt; CRITICAL_SECTION * &gt;( &amp;m_AddCriticalSection ) ); ::EnterCriticalSection( const_cast&lt; CRITICAL_SECTION * &gt;( &amp;m_GetCriticalSection ) ); number_of_items_in_the_queue = ( m_AddIndex &gt;= m_GetIndex ) ? ( m_AddIndex - m_GetIndex ) : ( ( m_AddIndex + m_Size ) - m_GetIndex ); ::LeaveCriticalSection( const_cast&lt; CRITICAL_SECTION * &gt;( &amp;m_GetCriticalSection ) ); ::LeaveCriticalSection( const_cast&lt; CRITICAL_SECTION * &gt;( &amp;m_AddCriticalSection ) ); return( number_of_items_in_the_queue ); } inline void CThreadQueue::m_GrowBy( SIZE_T number_of_new_items ) { // Prevent other threads from calling Get(). // We don't need to enter the AddCriticalSection because // m_GrowBy() is only called from Add(); void * * new_array = NULL; void * * pointer_to_free = NULL; SIZE_T new_size = m_Size + number_of_new_items; // Prevent other threads from getting ::EnterCriticalSection( &amp;m_GetCriticalSection ); new_array = (void **) ::HeapAlloc( m_Heap, HEAP_NO_SERIALIZE, new_size * sizeof( void * ) ); // Now copy all of the old items from the old queue to the new one. // Get the entries from the get-index to the end of the array ::CopyMemory( new_array, &amp;m_Items[ m_GetIndex ], ( m_Size - m_GetIndex ) * sizeof( void * ) ); // Get the entries from the beginning of the array to the add-index ::CopyMemory( &amp;new_array[ m_Size - m_GetIndex ], m_Items, m_AddIndex * sizeof( void * ) ); m_AddIndex = m_Size; m_GetIndex = 0; m_Size = new_size; pointer_to_free = m_Items; m_Items = new_array; ::LeaveCriticalSection( &amp;m_GetCriticalSection ); ::HeapFree( m_Heap, HEAP_NO_SERIALIZE, pointer_to_free ); } #endif /* ! defined( CTHREADQUEUE_H ) */ //---------- ! Threading Queue Class - End ------------ Good luck Federico
GeneralRe: Better solutions - Missing Concurrency Management Pin
federico.strati14-Sep-10 4:57
memberfederico.strati14-Sep-10 4:57 
GeneralMy vote of 1 Pin
federico.strati14-Sep-10 2:02
memberfederico.strati14-Sep-10 2:02 
GeneralThis will not "solve" teh problem Pin
Emilio Garavaglia7-Sep-10 21:18
memberEmilio Garavaglia7-Sep-10 21:18 
GeneralRe: This will not "solve" teh problem Pin
Blake Miller9-Sep-10 6:43
memberBlake Miller9-Sep-10 6:43 
GeneralVery Good Pin
Sushant Joshi7-Sep-10 21:18
memberSushant Joshi7-Sep-10 21:18 
GeneralRe: Very Good Pin
Luis Vital10-Sep-10 18:32
memberLuis Vital10-Sep-10 18:32 
Generalyou missing the locks here Pin
wangjinhu7-Sep-10 16:49
memberwangjinhu7-Sep-10 16:49 
GeneralRe: you missing the locks here Pin
Luis Vital10-Sep-10 18:28
memberLuis Vital10-Sep-10 18:28 
GeneralRe: you missing the locks here Pin
federico.strati14-Sep-10 2:03
memberfederico.strati14-Sep-10 2:03 
GeneralRe: you missing the locks here Pin
federico.strati14-Sep-10 2:11
memberfederico.strati14-Sep-10 2:11 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Terms of Use | Mobile
Web02 | 2.8.150804.3 | Last Updated 7 Sep 2010
Article Copyright 2010 by Luis Vital
Everything else Copyright © CodeProject, 1999-2015
Layout: fixed | fluid