Click here to Skip to main content
15,886,422 members
Articles / Programming Languages / C
Article

Avoiding Data Loss in Thread Communication

Rate me:
Please Sign up or sign in to vote.
3.60/5 (5 votes)
7 Sep 2010CPOL5 min read 25.6K   13   11
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)


Written By
Software Developer
Portugal Portugal
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
GeneralBetter solutions - Missing Concurrency Management Pin
federico.strati14-Sep-10 2:57
federico.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
federico.strati14-Sep-10 4:57 
GeneralMy vote of 1 Pin
federico.strati14-Sep-10 2:02
federico.strati14-Sep-10 2:02 
GeneralThis will not "solve" teh problem Pin
Emilio Garavaglia7-Sep-10 21:18
Emilio Garavaglia7-Sep-10 21:18 
GeneralRe: This will not "solve" teh problem Pin
Blake Miller9-Sep-10 6:43
Blake Miller9-Sep-10 6:43 
GeneralVery Good Pin
Sushant Joshi7-Sep-10 21:18
Sushant Joshi7-Sep-10 21:18 
GeneralRe: Very Good Pin
Luis Vital10-Sep-10 18:32
Luis Vital10-Sep-10 18:32 
Generalyou missing the locks here Pin
wangjinhu7-Sep-10 16:49
wangjinhu7-Sep-10 16:49 
GeneralRe: you missing the locks here Pin
Luis Vital10-Sep-10 18:28
Luis Vital10-Sep-10 18:28 
GeneralRe: you missing the locks here Pin
federico.strati14-Sep-10 2:03
federico.strati14-Sep-10 2:03 
GeneralRe: you missing the locks here Pin
federico.strati14-Sep-10 2:11
federico.strati14-Sep-10 2:11 

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

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