Simple Thread: Part II

Simple Thread: Part II

Passing thread-safe data between two threads.

Abstract

This article is a continuation of Simple Thread: Part I where you learned how to start, pause, resume, and stop a thread in MFC. Part I illustrated techniques of decoupling threading code from the MFC UI and used PostMessage to update a progress bar in the UI. Part I also demonstrated how to signal a thread to exit and shut down cleanly. As much as Part I demonstrated with regard to threading, it did not share any data between threads nor did it discuss thread safety or illustrate synchronization techniques. This article will build on the previous application and share data between threads by extending the StartStop example used in Part I. Similar to Part I, the worker thread is going to be a simple loop that updates a progress bar; however, in Part II a couple of string buffers will be shared between threads and the UI thread will read one of the buffers and use it to add items to a list control.

Note: If you haven't already done so, please read Simple Thread: Part I before reading this article. This article assumes some basic familiarity with MFC, how to create projects, add resources, and so forth.

Threading Overview

Before you get into the details of synchronizing data, take a moment to learn what threading is and why synchronization is important.

Processes and Threads

On the most basic level, when a user starts up an application (.exe), the OS creates a process space for the application, loads the EXE and any required DLLs into memory, and creates a primary thread of execution. The program then starts executing in main or winmain, depending on whether the program is a console or Windows application.

50 Cent Tour of the Windows Thread Scheduler

As you know, a process must have at least one thread that gets called on by the OS and executed. Without going into much detail, the OS thread scheduler switches between threads on a round robin basis. If there are ten applications running on a system and each has a single thread, the scheduler will run thread 1 for a bit, then run thread 2, and so on until the threads in all ten applications get run for a little bit. Then the cycle repeats (and repeats). By the way, the little bit the thread gets to run is called a time slice and the scheduler is in charge of what thread runs and for how long.

In reality, things are a more complicated than that and threads are allowed to have different priorities, but for threads with the same priority, that's essentially how it works. This scheduling of threads is called pre-emptive multitasking. With pre-emptive multitasking, the OS scheduler always controls which thread runs and how long a time slice it gets.

You may wonder what happens to the thread when a time slice has completed. The scheduler puts the thread to sleep and switches to another thread. You may have heard the term context switch? If you have, this is what occurs when the scheduler switches between threads.

Whether you have a single processor machine, a dual core machine, or a multiprocessor machine, the scheduler still operates on the round robin basis and switches between threads. The difference is that, on dual core or multi-proc machines, the scheduler can simultaneously run multiple threads depending on the number of cores and/or processors available. Threads on multicore/multiproc machines actually run in parallel (as compared to a single proc machine where they only seem to run in parallel).

An important concept to take away is that, when a thread is scheduled and running, lines of program code are being executed. When it's not scheduled or is sleeping, no program code is being executed.

Pre-Emptive Multitasking

I've mentioned the phrase pre-emptive multitasking a couple of times. Well, what is it? In a nutshell, a pre-emptive multitasking OS is one that remains in control of thread scheduling and threads. This type of OS has absolute power, unlike the older Win3.1 type of OS that allowed the application to decide when it was finished executing a chunk of code. With pre-emptive multitasking, the OS decides when and how long a thread executes. In other words, it can 'yank the rug out from under' the thread whenever it sees fit. Because of this, programmers should never make assumptions about how long a thread will get to run.

What Does Sleep() Do?

Speaking of sleeping, look at what happens when a program calls the Sleep() API. When a sleep statement is encountered during execution, the OS starts tracking the start time of the sleep and what thread it belongs to. Next, it forces the thread to give up the remainder of its time slice. The scheduler then context switches to another thread. When it is the sleeping thread's turn again in the round robin, the scheduler first checks to see whether the sleep period has expired and, if not, it simply skips over the thread and runs the next thread in the round robin. If the sleep period has expired, the scheduler starts executing the next line after the Sleep() statement.

Why Is Thread Synchronization Important?

You've learned about threads and the OS scheduler, but what is this talk about thread synchronization? Thread synchronization is necessary whenever two threads access a common resource. For example, say you have two threads accessing a shared string. One thread is writing to the thread while the other thread is reading from the thread. Because you can't rely on how long any thread will get to execute based on the thread scheduler, you can never be sure that the writing thread has finished writing before it is pre-empted so the reading thread can read. If the thread hasn't finished writing before the reading thread is executed, you have a race condition or corrupted data. Thread synchronization allows only one thread to access shared data. Threads are typically synchronized using a critical section, mutex, or other synchronization primitives such as semaphores or events. These primitives are sometimes referred to as synchronization objects.

How Critical Sections Function

Programmers new to multi-threading frequently have a common misunderstanding on how thread synchronization objects function. It is often thought that that a critical section protects blocks or chunks of code or even somehow protects an actual resource. This isn't quite how they operate—they simply operate as gatekeepers that prevent the code within threads from executing.

In a way, critical sections operate kind of like Sleep(), in that the OS keeps track of some data about the critical section. When you first initialize a critical (CS) section with a call to InitializeCriticalSection, you register the CS so the OS knows to begin tracking this variable.

Critical sections differ from the Sleep() API in that with sleep the thread doesn't get scheduled for some time period whereas only one thread can 'enter' a critical section at a time. Any thread that tries to 'enter' a critical section that has been 'entered' by another thread will be put to sleep (in other words, not scheduled) by the OS. Another way to say 'entered' is referred to as obtaining a lock.

So now, back to the new programmer misunderstanding. Remember I said that it is sometimes thought that a critical section protects blocks of code? It is not blocks of code that are protected, but rather that only one thread gets to execute any code past the call to EnterCriticalSection. Other threads will be forced to sleep when trying to call EnterCriticalSection—the OS will simply put the other threads to sleep until the first thread calls LeaveCriticalSection and unlocks or releases the CS. What occurs within a thread between the EnterCriticalSection call and the LeaveCriticalSection calls is completely unknown to the OS. The OS really doesn't care what occurs after the call to EnterCriticalSection; all it knows is that any other thread isn't allowed past a call to EnterCriticalSection and will get put to sleep. When thread A calls EnterCriticalSection and successfully obtains a lock on the CS, the OS keeps track of which thread has obtained the CS lock. If another thread, B, tries to gain access to the CS with a call to EnterCriticalSection, the OS will not allow more than one thread to access a critical section so the EnterCriticalSection call in thread B never returns. In fact, until thread A releases the CS lock with a call to LeaveCriticalSection, the OS scheduler will not schedule thread B.

From the point of view of the second thread trying to obtain a lock, this thread doesn't know it's being put to sleep; it just appears as though the EnterCriticalSection function never returns.

Simple Thread: Part II

What About a Mutex?

Although the method of obtaining a lock for a mutex is syntactically different than a critical section, functionally a mutex works the same as a critical section. That is, only one thread can obtain a lock on a mutex at a time. Unlike critical sections, a mutex can be used to synchronize threads in different processes whereas a critical section can only be used for threads in the same process.

The code in this article will perform synchronization using a critical section because you are only synchronizing threads within the same process.

Protecting Shared Data: The Bottom Line

To put it simply, only one thread can access a shared resource at any one time (if any thread is writing to the resource). Shared data is protected by using a critical section (or some other sync object) to prevent other threads from accessing the resource.

The Basic Project

Unlike Part I, I am not going to walk you through setting up the MFC project. Instead, you'll start with the Part I project StartStop and modify it to use the string buffers. The first modification is to rename the CProgressMgr class to CLogMgr. The naming of this class seemed to make sense in Part I because the purpose of the code was to manipulate a thread and simulate thread progress. Because now you are simulating passing log data, you will rename the class to CLogMgr.

Modifying the UI

As mentioned earlier, the shared string application is going to take an input string from the user and pass it to the worker thread. The worker thread is going to copy it over to an output string. The worker thread then will notify the UI. The UI will read the output string and use it to populate a list control. The modifications to the UI will consist of adding a list control and a couple of new buttons and an edit box. One button will update the input string without synchronization and the other will use a critical section to synchronize the input string access. As in Part I, you are going to use DDX variables to connect up the MFC controls. Rather than take the reader through the steps to add the new controls and wire them up, I am just going to skip this detail and focus on the threading.

After making the changes, the UI should look like Figure 1.

[Thread1.jpg]

Figure 1

Along with the UI control additions, the OnIncProgress message and handler has been renamed to OnLogEntry to better reflect the log operation of the sample. The Stdafx.h and LogMgr.h files have been updated to reflect these changes as well.

Wiring Up the New Button Handlers

The UI has two new buttons:

  • Add w/o Sync button: Used to change the log string in a non thread-safe manner
  • Add w/Sync button: Used to change the log entry string in a thread safe manner (using a critical section)

The code for the new button handlers is as follows:

void CStartStopDlg::OnBnClickedAddWOSync()
{
   EnableAddControls( FALSE );

   UpdateData( TRUE );
   m_LogMgr.AddLogEntryStringNoSync( m_sEditAdd );

   EnableAddControls( TRUE );
}

void CStartStopDlg::OnBnClickedAddWSync()
{
   EnableAddControls( FALSE );

   UpdateData( TRUE );
   m_LogMgr.AddLogEntryStringWSync( m_sEditAdd );

   EnableAddControls( TRUE );
}

void CStartStopDlg::EnableAddControls( BOOL bEnable )
{
   m_btnAddWOSync.EnableWindow( bEnable );
   m_btnAddWSync.EnableWindow( bEnable );
   m_ctrlEditAdd.EnableWindow( bEnable );
}

CLogMgr Modifications

As mentioned earlier, the UI will allow the user to set the 'input' log entry while the secondary thread is running. In the secondary thread, for each loop iteration, the input string's buffer is copied over to the output string buffer and the UI is notified that a log entry item is available. The UI then calls a method to read the shared output string so it can insert a new log entry into the list control.

Critical Section and Buffer Declarations

You need to define the input and output buffers shared between threads and the critical sections that protect these buffers.

Declarations

Add the following to the private field section of LogMgr.h.

// cs used to guard the input buffer
CRITICAL_SECTION  m_csInput;
// cs used to guard the output buffer
CRITICAL_SECTION  m_csOutput;

// input buffer (set by the UI thread)
TCHAR m_szLogEntryInput[ MAX_PATH ];
// output buffer (read by the UI thread)
TCHAR m_szLogEntryOutput[ MAX_PATH ];

Constructor/Destructor Changes

Both critical sections must be initialized in the constructor and deleted in the destructor. Make the following changes.

// ctor
CLogMgr( )
   : m_hWnd( NULL )
   , m_hThread( NULL )
   , m_hShutdownEvent( ::CreateEvent( NULL, TRUE, FALSE, NULL ) )
{
   ::InitializeCriticalSection( &m_csInput );
   ::InitializeCriticalSection( &m_csOutput );
}

// dtor
~CLogMgr( )
{
   ShutdownThread( );
   ::CloseHandle( m_hShutdownEvent );

   ::DeleteCriticalSection( &m_csInput );
   ::DeleteCriticalSection( &m_csOutput );

Define String Buffer Accessor Methods

The String buffer accessor methods provide access to the input and output string buffers and allow you to encapsulate the synchronization code within the CLogMgr class. Using this approach, the UI thread or secondary thread need not worry about synchronization because it's performed within these methods. Nice clean interfaces—that's what we like.

DISCLAIMER: The code shown below manually copies strings character by character in several of the methods and even goes as far as putting a delay after each character. Of course, real production code shouldn't do this, but you are trying to demonstrate a race condition (in other words, memory corruption) when writing to the string buffer without using synchronization. If this approach bothers you, consider this: All you are doing is slowing down the string copying by inserting a delay between each character copy and the only difference between the synchronized method and non thread safe method is that the synchronized method enters a critical section before copying the string. While this method of copying is a bit contrived, you are using the same copying code in the non-synchronized and synchronized cases. You'll see that in the non-synchronized case the code will be corrupted.
//+------------------------------------------------------
// Non thread safe method that overwrites the input buffer
// This method is called by the UI thread (and it's evil)
//+------------------------------------------------------
void AddLogEntryStringNoSync( LPCTSTR szLogEntry )
{
   // Manually copy the string chars (including terminating null)
   size_t length = _tcslen( szLogEntry );
   for( UINT uIndex = 0; uIndex < length + 1; uIndex++ )

   {
      m_szLogEntryInput[ uIndex ] = szLogEntry[ uIndex ];
      Sleep( 50);
   }
}

//+------------------------------------------------------
// Thread safe method that overwrites the input buffer
// This method is called by the UI thread (and it's good)
//+------------------------------------------------------
void AddLogEntryStringWSync( LPCTSTR szLogEntry )
{
   // Lock the input buffer
   ::EnterCriticalSection( &m_csInput );

   // Copy the string into the shared string buffer
   size_t length = _tcslen( szLogEntry );

   // Manually copy the string chars (including terminating null)
   for( UINT uIndex = 0; uIndex < length + 1; uIndex++ )
   {
      m_szLogEntryInput[ uIndex ] = szLogEntry[ uIndex ];
      Sleep( 0 );
   }

   // Unlock the input buffer
   ::LeaveCriticalSection( &m_csInput );
}

//+------------------------------------------------------
// Thread safe method that gets the output buffer text
// This method is called by the UI thread
//+------------------------------------------------------
void GetLogEntryStringWSync( LPTSTR szLogEntry, size_t size )
{
   // Lock the output buffer
   ::EnterCriticalSection( &m_csOutput );

   // Copy the string into the temp string
   _tcsncpy( szLogEntry, m_szLogEntryOutput, size );

   // Unlock the output buffer
   ::LeaveCriticalSection( &m_csOutput );
}

//+------------------------------------------------------
// Transfers string from input buffer to output buffer.
// Uses critical sections to protect limit access to
// the buffers to only one thread at a time.
// This method is called only by the secondary thread
//+------------------------------------------------------
void TransferLogEntryInputToOutput( )
{
   // Lock the input and output buffers
   ::EnterCriticalSection( &m_csInput );
   ::EnterCriticalSection( &m_csOutput );

   // Manually copy the string chars
   for( UINT uIndex = 0; uIndex < MAX_PATH; uIndex++ )
   {
      m_szLogEntryOutput[ uIndex ] = m_szLogEntryInput[ uIndex ];
      Sleep( 1 );
   }

   // Unlock the input and output buffers
   ::LeaveCriticalSection( &m_csOutput );
   ::LeaveCriticalSection( &m_csInput );
}

Simple Thread: Part II

Running the Application

Non thread safe operation

  1. Run the application.
  2. Press the start button to start the secondary (worker) thread.
  3. Press the 'Add w/o Sync' button to change the log entry text.
  4. Once you see the text change in the list control, press the 'Pause' button.
  5. Scroll up the list control to display the entries where the string has change from the original string.

You should end up with something like Figure 2.

[Thread3.jpg]

Figure 2

Observations

Notice how the string immediately following the last 'Original...' entry is a combination of the original string and the new string? This is because while the UI thread is writing to the string in an unsafe manner, the secondary thread is happily reading the string. Because you are changing the string without synchronization, you end up with a race condition or memory corruption. When you look at the code, other than writing to the string buffer without synchronization, you went out of your way to manually copy the string with a for loop to reliably produce the corruption. You did this because you wanted the reader to experience the corruption problem first hand. Without taking these extra steps to reliably produce the corruption, the reader may gain a false sense of security by thinking "well, it didn't corrupt on my machine." Rest assured that corruption will occur when you least expect it and it's different depending on the hardware configuration, CPU load, and so forth. Without synchronization for shared resources, the program may run fine on a developer's machine, but fail on a production server.

Tip: Whenever possible, always write and debug multi-threaded code on a dual proc or dual core development machine. Writing threading code on a single processor machine may appear to work properly but fail when executed on a different machine. Better to find out issues during the debugging stage rather than during deployment.

Thread safe Operation

  1. Run the application.
  2. Press the start button to start the secondary (worker) thread.
  3. Press the 'Add w/Sync' button to change the log entry text.
  4. Once you see the text change in the list control, press the 'Pause' button.

Figure 3 illustrates the synchronized (thread safe) log entry change.

[Thread5.jpg]

Figure 3

Observations

This time, you'll notice that the entry is either the old string or the new string (and nothing in between). This is because the string writes and reads were synchronized with the critical section. As you learned earlier, only one thread can enter a critical section as a time, so a thread is either reading or writing but not at the same time (unlike the non thread safe approach).

Simple Thread: Part III

Part I showed you how to start, pause, resume, and stop a thread and update an MFC control. Part II (this article) went on to share a couple of string buffers between threads and show the effects of non thread safe access and then proper synchronization using critical sections. Part III will continue the threading journey by using a technique called RAII (Resource Acquisition Is Initialization) using a sample that develops and shares thread-safe std::queue between threads.



Downloads

Comments

  • There are no comments yet. Be the first to comment!

Leave a Comment
  • Your email address will not be published. All fields are required.

Top White Papers and Webcasts

  • Agile methodologies give development and test teams the ability to build software at a faster rate than ever before. Combining DevOps with hybrid cloud architectures give teams not just the principles, but also the technology necessary to achieve their goals. By combining hybrid cloud and DevOps: IT departments maintain control, visibility, and security Dev/test teams remain agile and collaborative Organizational barriers are broken down Innovation and automation can thrive Download this white paper to …

  • Live Event Date: November 13, 2014 @ 2:00 p.m. ET / 11:00 a.m. PT APIs can be a great source of competitive advantage. The practice of exposing backend services as APIs has become pervasive, however their use varies widely across companies and industries. Some companies leverage APIs to create internal, operational and development efficiencies, while others use them to drive ancillary revenue channels. Many companies successfully support both public and private programs from the same API by varying levels …

Most Popular Programming Stories

More for Developers

Latest Developer Headlines

RSS Feeds