Click here to Skip to main content
15,868,016 members
Articles / Web Development / HTML

Start Class As Thread

Rate me:
Please Sign up or sign in to vote.
4.51/5 (22 votes)
26 Aug 2014CPOL14 min read 47.8K   820   54   28
Start a class as a thread, made simple

Introduction

This article describes how to start a class as a separate thread. The project is an MFC dialog project. It shows how to start a simple C function as a separate thread. That function in turn instantiates a class and starts it running. In this project, there are two paths of communications between the main application and the thread. First is a common memory area. Data passed between the main app and the thread uses this memory area. The second path of communications is via events that are created and under control of the main application. The thread detects those events and takes action based on them. All this is easier than at first appears.

Background

Starting a separate thread is much easier than I anticipated. The difficulties arose when attempting to use a class as a thread. The simplest article I found is here: Code Project Article But the translation from running a simple free standing C function as a thread, to running a class is difficult. After many forum posts, I discovered a very simple way to do that. I have yet to find an example that discusses or presents that concept, therefore, this article.

Environment

Windows 7, Visual Studio 2008, MFC Dialog, C++

Cautionary Note

The eventual purpose of this concept / method is to create the code that can be used by a non-window application.  That main_app will use this concept to start a thread that will handle TCP/IP operations.  The use of MFC provides the ability to display a dialog with controls that drive the thread and displays that show the performance and response of the thread.  MFC is not the final environment, just the development and test vehicle. As this project continues, the thread will perform the high level control of a TCP/IP class.

This project and dialog will be expanded until I have a full up server application that can connect with a client and send data. Fake data generated by the dialog, but data just the same. All the code that comprises the thread and TCP/IP code is kept in a directory separate from the solution for easy reuse.

Again

This is a test vehicle only, not production code. Since first writing this I have fleshed out the class to manage the TCP/IP code, create a second class to maintain the low level TCP connection and send the data. I have also create a second thread to handle the events needed for non-blocking TCP/IP with overlapped I/O. I am about done with testing the events and will soon add code to send data.

Further Warning

This example does not attempt to deal with the intricacies of multi-threading.  The coder, you,  must be wary of multiple opportunities for errors and problems such as which task is allowed to read or write the shared variables and when.  To address them in the article would make it overly complex.  I strive to do things one major step at a time and this was a major step for me.

Raison-d’etre

To the experienced Windows and Visual Studio coder this is all trivial stuff.  As a neophyte in this arena, I found that gathering the information presented here and organizing it in my brain and on paper was a formidable task.  Hence, this article.

Prerequisites

The reader should be able to create simple MFC dialogs with buttons and text controls, and to create classes.

Using the Code

Build the application and start it. The dialog shown here is after the events have been created, two events triggered, and the Timeout expired twice.

dialog image

Button Get Event Counts can be clicked at any time. It shows how many times the thread has detected each of the events. Click button Create Events to create the events. The handles are displayed below the button. There is no need to display them, but I like it.

Click Create Simple Thread to start the class running as a separate thread. The text below the button shows the status of the thread creation. This thread cannot be started until the events have been created. There is a time out value of five seconds that shows up in the event counts. It will be explained later.

Click Event: Start and Event: Stop at will to trigger those events. Clicking button Get Event Counts will show the number of times each event has been detected by the thread.

Button Event: Exit tells the thread to exit and it will stop detecting events. The thread may be restarted as desired. Click Done to exit the app. (Done is a much better label than OK.)

Common Structure.h

This include file declares a bunch of constants and the common structure used to pass data between the main app and the thread. The constants are omitted here and the structure shown. First is the array of events as required by WaitForMultipleObjects(). It is a simple array of event handles. Referring to the dialog image above, this array holds the values: 276, 284, and 288. The MSDN description is easy to follow. Take a few minutes to read it carefully.

The code to populate that array is presented shortly. Following the array is a set of counters, one for each of the three explicitly declared events and two more for additional events that can be detected. These values are incremented by the thread as each event is detected, and read by the main app to show the user. More on these variables shortly.

C++
struct st_common_data
{
    HANDLE event_array[ EVENT_ARRAY_SIZE ];
    unsigned int    count_of_start_events;
    unsigned int    count_of_stop_events;
    unsigned int    count_of_exit_events;
    unsigned int    count_of_timeout_events;
    unsigned int    count_of_unexpected_return_value;
};

C_TCP_Thread.h

This is the class that is started as a thread. It will eventually handle TCP/IP communications hence the name. All comments and such have been removed here leaving the essence. There is not much needed for this demo.

C++
#include "Common_Structure.h"
unsigned int __stdcall  Start_As_Thread( void * p_void_pointer );
class C_TCP_Thread
{
public:
    C_TCP_Thread( void * ptr_common_data );
    ~C_TCP_Thread(void);
    void Main_TCP_Thread( );
private:
    st_common_data * mp_common_data;
};

First up is Start_As_Thread(). It is declared outside the class scope making it a free standing function. It returns an unsigned int. The __stdcall is an instruction to the compiler specifying how this function is to be called. It is required by _beginthreadex(). That function is to be given one void pointer. A pointer to the common structure will be passed as that void pointer. More on that shortly.

The constructor will get that same pointer when the class is instantiated. There is no default constructor. The class must be created with the pointer.

Method Main_TCP_Thread was given that name to be similar to the Main() that used to start all simple programs. It runs for the life of the thread.

In the private section is a pointer to the common structure. The value for this pointer arrives via Start_As_Thread().

C_TCP_Thread.cpp

This is the class that is run as a separate threaed. It is presented one section at a time beginning with that free standing C function: Start_As_Thread().

C++
unsigned int __stdcall  Start_As_Thread( void * p_void_pointer )
  {
    C_TCP_Thread *     mp_C_TCP_Thread;
    mp_C_TCP_Thread =  new C_TCP_Thread( p_void_pointer );
    mp_C_TCP_Thread->Main_TCP_Thread();
    delete mp_C_TCP_Thread;
    _endthreadex( 0 );
    return 0;
  }

Observe that it does not contain the phrase C_TCP_Thread:: It is declared and defined outside the class scope. This is what makes the thread easy to start.

The first two lines declare a pointer to the class then instantiate it. The constructor gets the void pointer passed in via _beginthreadex(); This is the address of the common memory structure. Both the main app and the thread have that address.

Method Main_TCP_Thread() is called. It runs for the life of the thread. When it returns, the class is immediately deleted. This helps prevent memory leaks. The thread ends itself with endthreadex(0);

There are two interesting points here. What value should be returned? A short session with Google discovered no advice. Does it matter?

Putting _endthreadex(0) here simplifies the main app code. The thread kills itself and the main app is relieved of that chore. Here is the rub: In the debugger, the thread ends on that statement so return 0; never executes. According to MSDN, the thread should return a value. The compiler complains without that return 0; Maybe, technically speaking, this should be done differently. It does leave the thread handle of the main app dangling so the programmer must deal with that, or pointedly ignore it. That handle to the thread is captured in the main app when starting the thread and it can be used as an emergency method to kill the thread. I elect to just ignore it.

Class Constructor

 

C++
	C_TCP_Thread::C_TCP_Thread( void * ptr_common_data )
{
    mp_common_data = ( st_common_data * ) ptr_common_data;
}

The only thing it does is to cast the void pointer back to a pointer to the common data and save it.

Main_TCP_Thread()

Here is the main method of the thread. This is the method that does it all. It runs until the thread has completed its job. When it exits, the thread is done.

C++
 void C_TCP_Thread::Main_TCP_Thread( )
{
    bool done = false;
    DWORD event_triggered  = 0;
    
    while( !done )
    {       // This is the key Windows API call.
            // Wait here until any of the events are triggered,
            // to include the timeout event.
        event_triggered = WaitForMultipleObjects(
            EVENT_ARRAY_SIZE,
            mp_common_data->event_array,
            WAIT_FOR_ANY_EVENT,
            WAIT_TIMEOUT_VALUE );

            //  This only does enough to show that each event is detected 
            //  separate from the others.
            //  Note that these are only starter/demo events.
            //  When code is added for TCP/IP operations those events
            //  will be added along with an FSM to manage the TCP/IP
            //  operations.  (Finite State Machine)
        switch( event_triggered )
        {
                // Check out these constants in Common_Structure.h.
        case EVENT_RETURN_START_RUNNING:
            {
                mp_common_data->count_of_start_events ++;
                break;
            }
        case EVENT_RETURN_STOP_RUNNING:
            {
                mp_common_data->count_of_stop_events ++;
                break;
            }
        case EVENT_RETURN_THREAD_EXIT:
            {
                mp_common_data->count_of_exit_events ++;
                done = true;
                break;
            }
        case WAIT_TIMEOUT:
            {
                mp_common_data->count_of_timeout_events ++;
                break;
            }
        default:
            {
                mp_common_data->count_of_unexpected_return_value ++;
                break;
            }
        }

    } // end of: while( !done )
}

The code presumes that the pointer has been set and that the events have been created. It is a simple while loop that runs until the end thread event is detected.

The key to this method is the call to WaitForMultipleObjects(). Rather than call this API with some unhelpful numbers like: 3, 0, and 5000 as arguments, there are rather self-explanatory constants in the call. WAIT_FOR_ANY_EVENT is a BOOL set to FALSE meaning that any of the events being triggered will cause the call to return and the code to continue/resume running. When set to TRUE, all the events must be triggered before the code will continue. The return value of WaitForMultipleObjects() is a specially encoded value. This is a good place to show a bit more of Common_Structure.h.

C++
const DWORD EVENT_RETURN_START_RUNNING = WAIT_OBJECT_0;
const DWORD EVENT_RETURN_STOP_RUNNING  = EVENT_RETURN_START_RUNNING + 1;
const DWORD EVENT_RETURN_THREAD_EXIT   = EVENT_RETURN_STOP_RUNNING + 1;

WAIT_OBJECT_0 is a constant defined by Microsoft. When that value is returned, it means that the first event in the array was detected. A well named constant is declared and given that value. For each following event, the value is incremented by one. Hence that format of defining these constants. This means that the programmer must keep careful track of those events.

We should not presume that WAIT_OBJECT_0 has a value of zero, or that whatever value it does have, will never change. So it cannot be used as the index into the event array. Ingenious programmers can devise methods to avoid a list of constants, but having well named constants for each array carries significant benefits.

Also from Common_Structure.h

C++
const unsigned int EVENT_START_RUNNING = 0;
const unsigned int EVENT_STOP_RUNNING  = EVENT_START_RUNNING + 1;
const unsigned int EVENT_THREAD_EXIT   = EVENT_STOP_RUNNING + 1;

const unsigned int EVENT_ARRAY_SIZE    = EVENT_THREAD_EXIT + 1;

These are the constants used to index into the array of event handles. The coordination of these two sets of constants keep the main app and the thread using the event handles in a synchronized manner. When reading the code, those constants make it quite easy to determine exactly what the code is doing. When I add more code for the asynchronous TCP/IP operations, this will become much more complex. Keeping things simple now will make that task easier.

Hopefully, the operation of the switch statement is self-evident. As each event is detected, it is counted. It does nothing useful, but does demonstrate how these concept work together to make a functional thread. If you need more explanation, please post a comment.

Back to the code...

The placement of the call to WaitForMultipleObjects() is important, as is the fact that each event is created in a reset condition. The thread starts then immediately waits for events from the main app. It is effectively started in a suspended state. Or more correctly, a blocked state. When I write the TCP/IP handler code, I may want the thread to wait for a bit before opening the port to listen for a client. I may also want the ability to suspend TCP operations for some period. The events in this demo project will help with that.

OnBnClickedBtnCreateEvents()

Moving to the dialog, we now look at some of the button code that controls operations.

C++
void CStart_New_Thread_2Dlg::OnBnClickedBtnCreateEvents()
{
    const unsigned int STRING_LENGTH = 16;
    char  event_number[ STRING_LENGTH ];
    LPSECURITY_ATTRIBUTES NO_SECURITY_ATTRIBUTES = NULL;

    const bool  EVENT_TYPE_MANUAL_RESET = TRUE;
    const bool  EVENT_TYPE_AUTO_RESET   = FALSE;

    const bool  INITIALIZE_AS_TRIGGERED = TRUE;
    const bool  INITIALIZE_AS_RESET     = FALSE;
    LPCTSTR     NO_EVENT_NAME           = NULL;

    m_common_data.event_array[ EVENT_START_RUNNING ] = 
        CreateEvent(  NO_SECURITY_ATTRIBUTES,
                      EVENT_TYPE_AUTO_RESET,
                      INITIALIZE_AS_RESET,
                      NO_EVENT_NAME );

    m_common_data.event_array[ EVENT_STOP_RUNNING ] = 
        CreateEvent(  NO_SECURITY_ATTRIBUTES,
                      EVENT_TYPE_AUTO_RESET,
                      INITIALIZE_AS_RESET,
                      NO_EVENT_NAME );

    m_common_data.event_array[ EVENT_THREAD_EXIT ] = 
        CreateEvent(  NO_SECURITY_ATTRIBUTES,
                      EVENT_TYPE_AUTO_RESET,
                      INITIALIZE_AS_RESET,
                      NO_EVENT_NAME );

    sprintf_s( event_number, 
               STRING_LENGTH, 
              "%d", 
              m_common_data.event_array[ EVENT_START_RUNNING ] );
    m_start_event_handle.SetWindowTextA( event_number );

    sprintf_s( event_number, 
               STRING_LENGTH, 
              "%d", 
              m_common_data.event_array[ EVENT_STOP_RUNNING ] );
    m_stop_event_handle.SetWindowTextA( event_number );

    sprintf_s( event_number, 
               STRING_LENGTH, 
              "%d", 
              m_common_data.event_array[ EVENT_THREAD_EXIT ] );
    m_exit_event_handle.SetWindowTextA( event_number );
}

The purpose of this button is evident from the name. Bear in mind, creating the events is not the same as triggering/setting them. This just says: “Hey operating system, I need an event, please create it and give me a handle to it.” That is done three times and each handle is put in the event array using the constants mentioned earlier. Once we have those handles, then we can set the events and detect them. Note that auto reset is used. That means that the event is automatically reset, or rearmed if you prefer. Do read the MSDN page about this function and about events.

It almost goes without mention that the well named constants make the code rather easy to read and self-explanatory. The last few lines of code display the handles in the dialog. There is no need for this, I just like seeing them.

Set Events

While on the subject of events, here is the code to set an event.

C++
void CStart_New_Thread_2Dlg::OnBnClickedBtnEventExit()
{
    BOOL status = SetEvent( m_common_data.event_array[ EVENT_THREAD_EXIT ]); 
}	

Now that was much easier than I expected. There are only three events, hence, three buttons, and the other two look almost exactly like this one. Only the index into the array changes.

Create the Thread

Now we are ready to create the thread, the whole purpose of the project and article. The text continues after the code block.

C++
	void CStart_New_Thread_2Dlg::OnBnClicked_Create_Simple_Thread()
{
    const int DISPLAY_MAX = 50;
    char  t_string[ DISPLAY_MAX ];

	  // Do not start the thread until the events have been created.
    if( m_common_data.event_array[ EVENT_START_RUNNING ] == 0 )
    {
        m_thread_start_status.SetWindowTextA( "Create Events First" );
        return;
    }

    // These few lines start the free standing procedure as a thread.
    // That function instantiates the class and calls its main method.
    void *         NO_SECURITY_ATTRIBUTES = NULL;
    const unsigned USE_DEFAULT_STACK_SIZE = 0;
    const unsigned CREATE_IN_RUN_STATE    = 0;

    uintptr_t thread_pointer;
    thread_pointer = _beginthreadex( NO_SECURITY_ATTRIBUTES,
                                     USE_DEFAULT_STACK_SIZE,
                                     &Start_As_Thread,
                                     &m_common_data,
                                     CREATE_IN_RUN_STATE,
                                     &m_thread_address);
    if( thread_pointer == 0 )
    {
       m_return_value  = _get_errno( &m_thread_start_error );
       if( m_return_value == 0 )
       {
           sprintf_s( t_string, DISPLAY_MAX, "Create Fail, error %d", m_thread_start_error  );
       }
       else
       {
           sprintf_s( t_string, DISPLAY_MAX,    
           "Create Fail, get_errno fail, returned %d", m_return_value  );
       }
    }
    else
    {
        sprintf_s( t_string, DISPLAY_MAX, "Thread Created" );
    }
    
    m_thread_start_status.SetWindowTextA( t_string );
}

The event array is initialized to all zeroes in OnInitDialog(); I forgot to create the events before launching the thread and wondered why I was getting thousands of counts in variable:

mp_common_data->count_of_unexpected_return_value

It showed up when I clicked button Get Event Counts. So I added some code to tell me that and refuse to start the thread until the events were created. I have had at least one purist complain that there should be only one exit point in a function. I hold that when a quick and clean exit is needed, a return is better than shrouding the remainder of the code in if blocks.

Following that are a few, hopefully, self explanatory constants followed by the code that starts the thread. The third argument is the purpose of this entire article. Remember that function Start_As_Thread() is a free standing function, i.e. is not part of a class. Because of that, its address can be put directly into this call. I find this much simpler than the method used in the article referenced at the top of this article.

Coding Strategy

Communications between this main app and its thread is very simple. The main app writes to the array events, one time only. The thread reads that array repeatedly, but never writes to it. The thread writes to the counters that note the number of times an event has been detected. It never reads data from them. (Yeah, Yeah. Technically speaking, it must read it to get the value to increment, but it never uses that value within the code so effectively it only writes to the value.)

When you expand this class to do more things, here is my advice regarding this common area. The main app can write but never read one set of variables. The thread reads them but never writes. The reverse will be true for the remaining variable in the common area in that the thread writes but never reads while the main app only reads. Each common variable is used to communicate in one direction only, never the opposite direction.

And it will also help if the common structure is declared in two parts. All the items written by the main app are first, followed by all the items written by the thread, or vice-versa of course. It will also be good to name them with a prefix denoting the writer. Then when reading the code, it will be easy to see which is which.

This is certainly not comprehensive advice for interprocess communications. But when used, will be helpful. That topic merits, and has received, much discussion.

Update

Caution

Comments on this article have expressed concern about the lack of synchronization and control of the common data area. This update addresses those issues

In addition to the events just discussed this project has two data paths to communicate between the main app and the thread.

It is important to note that this project is simplistic. The data path from main app to thread consists of three integers, the handles to the events. They are written one time before the thread starts and never written again. Given that limitation there is no need for any type of mutex.

The data path from thread to main app is five integers, written by the thread and read by the main app. Again, the write/read cycle is one way. The thread write simple scalars one at a time, the main app reads the values. The main app makes no decisions and calculates no values based on those integers. They are display only. Further, in this example, on a lightly or otherwise idle computer, the thread will always have completed its update before the user can get the mouse over to the Get Event Counts button.

The novice is strongly advised to engage in some serious research and reading on the topic of mutexes, semaphores, and other methods of controlling and sharing common resources.

I think this is all that is needed to start a class as a thread. I hope it works well for you.

Points of Interest

After hesitating for a long time, starting a separate thread is much easier than anticipated. Very often, things are much simpler that they appear. Using this tool well and effectively will take significant care and practice. But if you have ever thought you might need to start a thread, give it a try and add it to your tool kit.

History

  • V01: December 2013

License

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


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

Comments and Discussions

 
GeneralMy vote of 3 Pin
Andy Bantly23-Aug-14 10:18
Andy Bantly23-Aug-14 10:18 
GeneralRe: My vote of 3 Pin
bkelly1323-Aug-14 15:38
bkelly1323-Aug-14 15:38 
GeneralRe: My vote of 3 Pin
Andy Bantly23-Aug-14 17:16
Andy Bantly23-Aug-14 17:16 
GeneralRe: My vote of 3 Pin
bkelly1324-Aug-14 6:31
bkelly1324-Aug-14 6:31 
GeneralMy vote of 2 Pin
Albert Holguin27-Dec-13 7:44
professionalAlbert Holguin27-Dec-13 7:44 
GeneralRe: My vote of 2 Pin
bkelly1327-Dec-13 14:01
bkelly1327-Dec-13 14:01 
GeneralRe: My vote of 2 Pin
Albert Holguin28-Dec-13 10:56
professionalAlbert Holguin28-Dec-13 10:56 
Generalmutex response Pin
bkelly1328-Dec-13 6:52
bkelly1328-Dec-13 6:52 
GeneralRe: mutex response Pin
Albert Holguin28-Dec-13 10:53
professionalAlbert Holguin28-Dec-13 10:53 
SuggestionAn alternative method, that keeps to the concept of encapsulation. Pin
CoreyCooper23-Dec-13 23:45
CoreyCooper23-Dec-13 23:45 
GeneralRe: An alternative method, that keeps to the concept of encapsulation. Pin
bkelly1324-Dec-13 11:44
bkelly1324-Dec-13 11:44 
GeneralRe: An alternative method, that keeps to the concept of encapsulation. Pin
CoreyCooper26-Dec-13 12:54
CoreyCooper26-Dec-13 12:54 
GeneralRe: An alternative method, that keeps to the concept of encapsulation. Pin
bkelly1327-Dec-13 15:31
bkelly1327-Dec-13 15:31 
GeneralRe: An alternative method, that keeps to the concept of encapsulation. Pin
CoreyCooper28-Dec-13 7:07
CoreyCooper28-Dec-13 7:07 
GeneralRe: An alternative method, that keeps to the concept of encapsulation. Pin
bkelly1328-Dec-13 8:24
bkelly1328-Dec-13 8:24 
GeneralRe: An alternative method, that keeps to the concept of encapsulation. Pin
Albert Holguin27-Dec-13 7:29
professionalAlbert Holguin27-Dec-13 7:29 
QuestionStart class as a thread Pin
geoyar23-Dec-13 8:19
professionalgeoyar23-Dec-13 8:19 
AnswerAn advantage to starting a class as a thread Pin
bkelly1324-Dec-13 11:25
bkelly1324-Dec-13 11:25 
GeneralRe: An advantage to starting a class as a thread Pin
geoyar24-Dec-13 12:01
professionalgeoyar24-Dec-13 12:01 
GeneralRe: An advantage to starting a class as a thread Pin
bkelly1325-Dec-13 7:09
bkelly1325-Dec-13 7:09 
GeneralRe: An advantage to starting a class as a thread Pin
geoyar25-Dec-13 10:21
professionalgeoyar25-Dec-13 10:21 
Again, thank for reply.

What I disagree with you is:

1. IMHO we never 'run' any class. We instantiate the class and run the member functions of the instance. If the class has static member functions, we run them without any instance of the class.

2. There never was a problem to pass the instance of the class to a worker thread: use

C++
_begin_thread(void( __cdecl *start_address )( void * ),
              unsigned stack_size,
              void *arglist )

In C++ 11 the constructor of the thread class accepts worker functions with arguments.
Once the worker function started you are free to call any member function of the instance, no need to pass a pointer to the member function to the worker thread.

3. Also there never was a problem to instantiate any class inside any function as a local variable or static variable, inside or outside the worker thread.

I think all this is in use for long time.
The best explanation of threads in C++ 11 I read on http://www.stroustrup.com/C++11FAQ.html

Regarding the pointers to the class member functions: You can define the pointer to the member function in any place and different members will have different addresses, if the definition is right. But you must dereference this pointer with the instance of the class to execute the member function Here is an example from Internet:
include <iostream>
using namespace std;

class X {
public:
  int a;
  void f(int b) {
    cout << "The value of b is "<< b << endl;
  }
};

int main() {

  // declare pointer to data member
  int X::*ptiptr = &X::a;

  // declare a pointer to member function
  void (X::* ptfptr) (int) = &X::f;

  // create an object of class type X
  X xobject;

  // initialize data member
  xobject.*ptiptr = 10;

  cout << "The value of a is " << xobject.*ptiptr << endl;

  // call member function
  (xobject.*ptfptr) (20);
}


Because you need the instance, IMHO, there is no need to pass the pointer if you know what you want to call.

So I understand your idea as suggestion to write some function to instantiate the class inside it and call an appropriate member function.

Personally, I prefer ( and I did) to write the class with all static data members and member functions if there is only one instance of the class to be used. Or, for many instances, I would use _begin_thread or C++ 11 thread with additional parameter (index) for the task to be performed. Of course, if it is easy to pass result to the consumer thread, the instantiation inside the worker function is preferable.
geoyar

GeneralRe: An advantage to starting a class as a thread Pin
CoreyCooper26-Dec-13 13:10
CoreyCooper26-Dec-13 13:10 
GeneralRe: An advantage to starting a class as a thread Pin
geoyar26-Dec-13 16:54
professionalgeoyar26-Dec-13 16:54 
QuestionMy vote of 5: Good idea and good article Pin
kanalbrummer23-Dec-13 3:19
kanalbrummer23-Dec-13 3:19 
AnswerRe: My vote of 5: Good idea and good article Pin
bkelly1328-Dec-13 7:15
bkelly1328-Dec-13 7:15 

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.