수안이의 컴퓨터 연구실

  • Mainpage
  • About Me
  • Tags
  • Metapage
  • Notice
  • Location
  • Keywords
  • Guestbook
  • Admin
  • Write an Article
  • Total | 1694992
  • Today | 737
  • Yesterday | 606

9 Articles, Search for 'Thread'

  1. 2007/07/26 IOCP Thread Pooling in C#
  2. 2007/05/17 쓰레드 풀 (Thread Pooling) 작성 (1)
  3. 2007/05/15 WaitForSingleObject() 에 의한 Thread 동기화
  4. 2007/05/15 Thread (쓰레드) 란 ? (4)
  5. 2007/05/14 pthread API 레퍼런스 (2)
  6. 2007/05/11 쓰레드와 시그널
  7. 2007/05/10 리눅스 커널의 이해(3): 리눅스 디바이스 작성시 동기화 문제
  8. 2007/03/23 TLS(Thread Local Storage)
  9. 2007/02/05 C# 스레드 사용 (1)
Programming/Network Programming2007/07/26 14:07

IOCP Thread Pooling in C#

출처 : http://www.devarticles.com/c/a/C-Sharp/IOCP-Thread-Pooling-in-C-sharp-Part-I/

IOCP Thread Pooling in C# - Part I
(Page 1 of 3 )

This is the first part of William's two part series on thread pooling in C#. By importing a dll file for IOCP thread support.

When building server based applications in C#, it is important to have the ability to create thread pools.  Thread pools allow our server to queue and perform work in the most efficient and scalable way possible.  Without thread pooling we are left with two options. 

The first option is to perform all of the work on a single thread.  The second option is to spawn a thread every time some piece of work needs to be done.  For this article, work is defined as an event that requires the processing of code.  Work may or may not be associated with data, and it is our job to process all of the work our server receives in the most efficient and fastest way possible.

As a general rule, if you can accomplish all of the work required with a single thread, then only use a single thread.   Having multiple threads performing work at the same time does not necessarily mean our application is getting more work done, or getting work done faster. This is true for many reasons. 

For example, if you spawn multiple threads which attempt to access the same resource bound to a synchronization object, like a monitor object, these threads will serialize and fall in line waiting for the resource to become available.  As each thread tries to access the resource, it has the potential to block, and wait for the thread that owns the resource to release the resource. 

At that point, these waiting threads are put to sleep, and not getting any work done.  In fact, these waiting threads have caused more work for the operating system to perform.  Now the operating system must task another thread to perform work, and then determine which thread, waiting for the resource, may access the resource next, once it becomes available. 

If the threads that need to perform work are sleeping, because they are waiting for the resource to become available, we have actually created a performance problem.  In this case it would be more efficient to queue up this work and have a single thread process the queue. 

Threads that start waiting for a resource before other threads, are not guaranteed to be given the resource first.  In diagram A, thread 1 requests access to the resource before thread 2, and thread 2 requests access to the resource before thread 3.  The operating system however decides to give the resource to thread 1 first, then thread 3, and then thread 2.  This scenario causes work to be performed in an undetermined order.  The possible issues are endless when dealing with multi-threaded applications.


If work received can be performed independent of each other, we could always spawn a thread for processing that piece of work.  The problem here is that an operating system like Windows has severe performance problems when a large number of threads are created or running at the same time, waiting to have access to the CPU. 

The Windows operating system needs to manage all of these threads, and compared to the UNIX operating system, it just doesn’t hold up.  If large amounts of work are issued to the server, this model will most likely cause the Windows operating system to become overloaded.  System performance will degrade drastically.
<>

This article is a case study comparing thread performance between Windows NT and Solaris.

http://www.usenix.org/publications/libr ··· tta.html

In the .NET framework, the “System.Threading” namespace has a ThreadPool class.  Unfortunately, it is a static class and therefore our server can only have a single thread pool.  This isn’t the only issue.  The ThreadPool class does not allow us to set the concurrency level of the thread pool. 

The concurrency level is the most important setting when configuring a thread pool.  The concurrency level defines how many threads in the pool may be in an “active state” at the same time.  If we set this parameter correctly, we will have the most efficient, performance enhanced thread pool for the work being processed.

Imagine we have a thread pool with 4 threads and a concurrency level of 1. Then, three pieces of work are queued up for processing in the pool. Since the concurrency level for the thread pool is 1, only a single thread from the pool is activated and given work from the queue.  Even though there are two pieces of work queued up, no other threads are activated.  This is because the concurrency level is set to 1.  If the concurrency level was set to 2, then another thread would have been activated immediately and given work from the queue.  In diagram B we have thread 1 running and all of the other threads sleeping with two pieces of work queued.

So the question exists, why have more than 1 thread in the pool if the concurrency level is set to 1?  If thread 1 in diagram B ever goes to sleep before it completes its work, another thread from the pool will be activated.  When thread 1 goes to sleep, there are 0 threads “active” in the pool and it is ok to activate a new thread based on the concurrency level.  In diagram C, we now have thread 1 sleeping and thread 4 running with one piece of work queued.

Eventually, thread 1 will wake up, and it is possible for thread 4 to still be active.  We have 2 threads active in the pool, even though the concurrency level is set to 1.  In diagram D, we now have thread 1 and thread 4 running and one piece of work still queued.

The last piece of work in the queue will need to wait until both threads return to a sleeping state.  This is because the concurrency level is set to 1.  As we can see, even though the concurrency level restricts the number of active threads in the pool at any given time, we could have more active threads then the concurrency level allows.  It all depends on the state of the threads in the pool and how fast the threads can complete the work they are processing.

A good rule of thumb is to set the concurrency level to match the number of CPU’s in the system.  If the machine our server is running on only has one CPU, then only one thread can be executing at any given time. It will require a task swap to have another thread get CPU time.  We want to reduce the number of active threads at any given time to maximize performance.  This also leads to scalability.  As the number of CPU’s increase, we can increase the concurrency level because there is a CPU to execute that thread.  This is a general rule and is always a good starting point for configuring our thread pools.

The bottom line is, if the CPU is available, and there is work to perform, activate a thread.  If the CPU is not available, do not activate a thread.  One other thing, we need to be careful that we don’t cause a situation where the threads in the pool are constantly being put to sleep for long periods of time during the processing of work.  This may cause all of the threads in the pool to constantly be in an active state, defeating the efficiency of the pool and the performance of the server.

The remaining scope of this article will show you how to add IOCP thread pools to your C# server based applications.  How to configure the thread pools for your specific application will not be covered.  It is suggested to use the general rules as discussed.

This Win32 API call is used to create an IOCP thread pool.  The first argument will always be set to INVALID_HANDLE_VALUE, which is 0xFFFFFFFF.  This tells the operating system this IOCP thread pool is not linked to a device.  The second argument will always be set to 0. There is no existing IOCP thread pool because we are creating this for the first time.  The third argument will always be null.  IOCP Thread Pooling in C# - Part I - The Article
(Page 2 of 3 )

System Requirements

A basic understanding of C# is required to follow through the examples and the classes.  Basic concepts of type, properties, threading, synchronization, and delegates are required.

Defining the Problem

IOCP thread support has not been made available to C# developers through the “System.Threading” namespace.  We need to access the Win32 API calls from the Kernel32.dll.  This requires us to write unsafe code.  This is really not a problem, but something that needs to be discussed.  Let’s take a look at the Win32 API calls we need to implement an IOCP thread pool.

[DllImport("Kernel32", CharSet=CharSet.Auto)]
private unsafe static extern UInt32 CreateIoCompletionPort(UInt32 hFile, UInt32 hExistingCompletionPort, UInt32* puiCompletionKey, UInt32 uiNumberOfConcurrentThreads);

We do not require a key because we have not associated this IOCP thread pool with a device.  The last argument is the important argument.  Here we define the concurrency level of the thread pool.  If we pass a 0 for this argument the operating system will set the concurrency level to match the number of CPU’s in the machine. 

This option gives us our best chance to be scalable and take advantage of the number of CPU’s present in the machine.  This API call will return a handle to the newly created IOCP thread pool.  If the API call fails, it will return null.

[DllImport("Kernel32", CharSet=CharSet.Auto)]
private unsafe static extern Boolean CloseHandle(UInt32 hObject);

This Win32 API call is used to close our thread pool.  The only argument is the handle to the IOCP thread pool.  This API call will return TRUE or FALSE if the handle can not be closed.

[DllImport("Kernel32", CharSet=CharSet.Auto)]
private unsafe static extern Boolean PostQueuedCompletionStatus(UInt32 hCompletionPort, UInt32 uiSizeOfArgument, UInt32* puiUserArg, OVERLAPPED* pOverlapped);

This Win32 API call is used to post work in the IOCP thread pool queue. Other threads in our application will make this Win32 API call.  The first argument is the handle to the IOCP thread pool.  The second argument is the size of the data we are posting to the queue.  The third argument is a value or a reference to an object or data structure we are posting to the queue.  The last argument will always be null. The following diagram shows how the data is associated with the posted work.

In diagram E, we have two threads actively processing posted work and one piece of work on the queue waiting for its data to be processed.  The thing to note here is that each piece of work was given a reference to its specific data.  I am calling this variable pData to help describe what is happening in the IOCP thread pool.  The actual name or structure of this variable is undocumented.

When we make this API call in a C++ application, we can pass the address of any object in memory we wish, as in diagram E.  In C#, we don’t have the same luxury because of the managed heap.  The managed heap is a contiguous region of address space that contains all of the memory allocated for reference variables.  The heap maintains a pointer that indicates where the next object is to be allocated, and all allocations are contiguous from that point.  This is much different from the C-runtime heap. 

The C-runtime heap uses a link list of data structures to reference available memory blocks.  For the C-runtime heap to allocate memory, it must walk through the link list until a large enough block of free memory is found.  Then the free block of memory must be resized, and the link list adjusted. 

If objects are allocated consecutively in a C++ application, those objects could be allocated anywhere on the heap.  This can never happen with the managed heap.  Objects that are allocated consecutively in a C# application will always be allocated consecutively on the managed heap.  The catch is that the managed heap must be compacted to guarantee the heap does not run out of memory.  That is the job of garbage collection.

For more information on garbage collection, try these links:

http://msdn.microsoft.com/library/default.asp?url=/library/en-us/cpguide/html/cpconautomaticmemorymanagement.asp

http://msdn.microsoft.com/msdnmag/issues/1100/GCI/default.aspx

http://msdn.microsoft.com/msdnmag/issues/1200/GCI2/default.aspx

In diagram F, we have allocated four objects on the managed heap.  Imagine that the managed heap has allocated memory for these objects at address FDEO, FDDO, FDCO, and FDBO.  This would mean the value of pClass1 is FDEO, the value of pClass2 is FDDO, the value of pClass3 is FDCO, and the value of pClass4 is FDBO. 

MyClass pClass1 = new MyClass();
MyClass pClass2 = new MyClass();
MyClass pClass3 = new MyClass();
MyClass pClass4 = new MyClass();


Now we write the following code.

pClass2 = null;

Diagram G shows what happens to the managed heap after garbage collection takes place and the managed heap is compacted.

The Class 2 object has been removed from the managed heap and the Class3 and Class 4 objects have been moved.  Now the value of pClass3 is FDDO and the value of pClass4 is FDCO.  The value that the pointer points to has changed.  The garbage collection process changes the values of all reference variables to make sure they are pointing to the correct objects after the managed heap is compacted.

So what does this mean for our IOCP thread pool implementation?  If we pass the reference of a managed object as the data for the work, there is a chance the reference is no longer valid when a thread in the pool is chosen to work on the data.

In diagram H, we have passed a reference to the Class 3 object as the data for the work posted to the IOCP thread pool.  This object is at address FDCO.  Before the work is given to thread 1, the Class 2 object is marked for deletion.  Then the garbage collection process runs, and the managed heap is compacted.  Now in diagram I, the work has been given to thread 1 for processing.  The value of pData is still FDCO, but Class 3 is no longer at address FDCO, it is at address FDDO.  The thread will perform the work, but using Class 4 instead of Class 3.

The garbage collection process can not change the value of pData, as it does with other variables, because this variable is not a managed variable.  It is a variable owned by the IOCP thread pool and exists outside the scope of the CLR.  The garbage collector has no knowledge of this variable or access to this variable.  The variable is set during the unsafe call to PostQueuedCompletionStatus.

Unfortunately, pinning the objects we want to pass as the data for the work posted to the IOCP thread pool is not a possible solution.  Pinning provides the ability to prevent an object from being moved on the manage heap during the garbage collection process.  We can not pin these objects because there is no way to pin an object in one thread and unpin the object in a different thread.  To pin an object, we need to use the fixed keyword.  This keyword can only be used in the context of a single method. Here is a quick example of pinning.

Int32 iArray = new Int32[5] {12, 34, 56, 78, 90};
unsafe
{
  fixed (Int32* piArray = iArray)
  {
   // Do Something
  }
}

The safest thing we can do is pass a value to the IOCP thread pool.  This value could be the index from a managed array, containing a reference to an object on the managed heap.   If the garbage collection process does compact the heap, the index values of the array will not change. In diagram J and K, we can see one way to properly pass data for the work posted to the IOCP thread pool.  After the garbage collection process compacts the heap, the values of pData change, but the index positions to the pData variables do not change.

[DllImport("Kernel32", CharSet=CharSet.Auto)]
private static extern Boolean GetQueuedCompletionStatus(UInt32 hCompletionPort, UInt32* pSizeOfArgument, UInt32* puiUserArg, OVERLAPPED** ppOverlapped, UInt32 uiMilliseconds);

The final Win32 API call is used to add threads to the IOCP thread pool. Any thread that makes this Win32 API call will become part of the IOCP thread pool.  This is a blocking call and the method will return when the IOCP thread pool chooses the thread to perform work. 

The first argument is the handle to the IOCP thread pool.  The second argument is the size of the data associated with the work.  This value was provided when the work was posted.  The third argument is the data value or data reference associated with the work.  This value was provided when the work was posted.  The forth argument is the address to a pointer of type OVERLAPPED. 

This address is returned after the call.  The last argument is the time in milliseconds the thread should wait to be activated to perform work. We will always pass INFINITE or 0xFFFFFFFF.

These are the Win32 API calls we need to add IOCP thread pool support to our C# server based applications.  We need to encapsulate these Win32 API calls using .NET threads and minimize the sections of unsafe code.  We need to prevent the application developer from passing a reference variable into the IOCP thread pool, by restricting them to passing only integer values.

IOCP Thread Pooling in C# - Part I - What to Expect in Part 2
(Page 3 of 3 )

In part II of this article, we will build a class that encapsulates a single IOCP thread pool.  The application developer will be able to instantiate as many thread pools as he wishes. 

During construction, the application developer will be able to: set the concurrency level of the thread pool, set the minimum and maximum number of threads in the pool, and will be able to provide a method to be called when work posted to the thread pool needs to be processed. The application developer will also be able to post work with data into the IOCP thread pool.

IOCP Thread Pooling in C# - Part II
(Page 1 of 3 )

In part 2, William will continue to explain how the create a class that will handle threads using a IOCP Thread Pool.

Defining the Solution

We will build a class that encapsulates a single IOCP thread pool.  The application developer will be able to instantiate as many thread pools as he wishes.  During construction, the application developer will be able to: set the concurrency level of the thread pool, set the minimum and maximum number of threads in the pool, and will be able to provide a method to be called when work posted to the thread pool needs to be processed.  The application developer will also be able to post work with data into the IOCP thread pool.

Component Design and Coding

Start by adding a new class to your C# project.  Remove all of the code provided by the Visual Studio .NET wizard.  Then add the following namespaces.  The System.Runtime.InteropServices namespace is required to access the Win32 API methods from the Kernel32 DLL. 

using System;
using System.Threading;
using System.Runtime.InteropServices;

Don’t forget to change the project properties to allow unsafe code blocks. This can be done by opening the project properties and selecting the Configuration Properties.  Under the Build / Code Generation section you will see “Allow unsafe code blocks”.  Set this to true.

Next add the namespace.   You will notice that I have defined a two level namespace.  This is great when you are building a class library with many different classes.

namespace Continuum.Threading
{

The PostQueuedCompletionStatus and GetQueuedCompletionStats Win32 API methods both require a pointer to the Win32 OVERLAPPED structure. Because this structure will be used by the unsafe Win32 API call, we need to make sure the structure is aligned exactly the same way it would be in our C++ applications.  This can be accomplished by using the StructLayout attribute.  By setting the attribute to “LayoutKind.Sequential”, the structure will be aligned based on the same rules as the C++ compiler.

The structure requires members that are pointers.  The only way to add pointers to this structure is to use the unsafe keyword.  We can still use the FCL types when defining the structure.  This is very important because we can make sure the structure is identical to the C++ version.

// Structures
  //==========================================
  /// <summary> This is the WIN32 OVERLAPPED structure </summary>
  [StructLayout(LayoutKind.Sequential, CharSet=CharSet.Auto)]
  public unsafe struct OVERLAPPED
  {
   UInt32* ulpInternal;
   UInt32* ulpInternalHigh;
   Int32   lOffset;
   Int32   lOffsetHigh;
   UInt32  hEvent;
  }

Now it is time to define the IOCP thread pool class.  I am using the keyword sealed in the definition of this class.  The sealed keyword tells the compiler that this class can not be inherited.  If you know there is no reason for a class to be inherited, use the sealed keyword.  Certain run-time optimizations are enabled for the class when the sealed keyword is used.

// Classes
  //============================================
  /// <summary> This class provides the ability to create a thread pool to manage work.  The
  ///           class abstracts the Win32 IOCompletionPort API so it requires the use of
  ///           unmanaged code.  Unfortunately the .NET framework does not provide this functionality </summary>
  public sealed class IOCPThreadPool
  {

The first section of the IOCP thread pool class is the Win32 function prototypes.  These are the same ones described earlier.

  // Win32 Function Prototypes
   /// <summary> Win32Func: Create an IO Completion Port Thread Pool </summary>
   [DllImport("Kernel32", CharSet=CharSet.Auto)]
  private unsafe static extern UInt32 CreateIoCompletionPort(UInt32 hFile, UInt32 hExistingCompletionPort, UInt32* puiCompletionKey, UInt32 uiNumberOfConcurrentThreads);

   /// <summary> Win32Func: Closes an IO Completion Port Thread Pool </summary>
   [DllImport("Kernel32", CharSet=CharSet.Auto)]
   private unsafe static extern Boolean CloseHandle(UInt32 hObject);

   /// <summary> Win32Func: Posts a context based event into an IO Completion Port Thread Pool </summary>
   [DllImport("Kernel32", CharSet=CharSet.Auto)]
  private unsafe static extern Boolean PostQueuedCompletionStatus(UInt32 hCompletionPort, UInt32 uiSizeOfArgument, UInt32* puiUserArg, OVERLAPPED* pOverlapped);

   /// <summary> Win32Func: Waits on a context based event from an IO Completion Port Thread Pool.
   ///           All threads in the pool wait in this Win32 Function </summary>
   [DllImport("Kernel32", CharSet=CharSet.Auto)]
  private unsafe static extern Boolean GetQueuedCompletionStatus(UInt32 hCompletionPort, UInt32* pSizeOfArgument, UInt32* puiUserArg, OVERLAPPED** ppOverlapped, UInt32 uiMilliseconds);

The next section is the constants section.  Here we need to define the Win32 constants required for the Win32 API calls we are going to make later.

// Constants
   /// <summary> SimTypeConst: This represents the Win32 Invalid Handle Value Macro </summary>
   private const UInt32 INVALID_HANDLE_VALUE = 0xffffffff;

   /// <summary> SimTypeConst: This represents the Win32 INFINITE Macro </summary>
   private const UInt32 INIFINITE = 0xffffffff;

   /// <summary> SimTypeConst: This tells the IOCP Function to shutdown </summary>
   private const Int32 SHUTDOWN_IOCPTHREAD = 0x7fffffff;

The delegate function type section is where we define any delegate functions.  We need one delegate function type to define the signature of the function we will call when work needs to be processed.

  // Delegate Function Types
   /// <summary> DelType: This is the type of user function to be supplied for the thread pool </summary>
   public delegate void USER_FUNCTION(Int32 iValue);

These private properties are required to maintain the application developer’s settings.  The most interesting property is the GetUserFunction property.  This property contains a reference to a method supplied by the application developer.  We will use this property to call the application developers method.

  // Private Properties
   private UInt32 m_hHandle;
     /// <summary> SimType: Contains the IO Completion Port Thread Pool handle for this instance </summary>
     private UInt32 GetHandle { get { return m_hHandle; } set { m_hHandle = value; } }

   private Int32 m_uiMaxConcurrency;
     /// <summary> SimType: The maximum number of threads that may be running at the same time </summary>
     private Int32 GetMaxConcurrency { get { return m_uiMaxConcurrency; } set { m_uiMaxConcurrency = value; } }

   private Int32 m_iMinThreadsInPool;
     /// <summary> SimType: The minimal number of threads the thread pool maintains </summary>
     private Int32 GetMinThreadsInPool { get { return m_iMinThreadsInPool; } set { m_iMinThreadsInPool = value; } }

   private Int32 m_iMaxThreadsInPool;
     /// <summary> SimType: The maximum number of threads the thread pool maintains </summary>
     private Int32 GetMaxThreadsInPool { get { return m_iMaxThreadsInPool; } set { m_iMaxThreadsInPool = value; } }

   private Object m_pCriticalSection;
     /// <summary> RefType: A serialization object to protect the class state </summary>
     private Object GetCriticalSection { get { return m_pCriticalSection; } set { m_pCriticalSection = value; } }

   private USER_FUNCTION m_pfnUserFunction;
     /// <summary> DelType: A reference to a user specified function to be call by the thread pool </summary>
     private USER_FUNCTION GetUserFunction { get { return m_pfnUserFunction; } set { m_pfnUserFunction = value; } }
  
   private Boolean m_bDisposeFlag;
     /// <summary> SimType: Flag to indicate if the class is disposing </summary>
     private Boolean IsDisposed { get { return m_bDisposeFlag; } set { m_bDisposeFlag = value; } }
 
These public properties are used to determine if new threads need to be added to the thread pool.  These properties also provide statistical data about the thread pool.  Here we use the Interlocked class to provide serialization when we increment or decrement these properties.  This is the least expensive way to perform serialization.

  // Public Properties
   private Int32 m_iCurThreadsInPool;
     /// <summary> SimType: The current number of threads in the thread pool </summary>
     public Int32 GetCurThreadsInPool { get { return m_iCurThreadsInPool; } set { m_iCurThreadsInPool = value; } }
     /// <summary> SimType: Increment current number of threads in the thread pool </summary>
     private Int32 IncCurThreadsInPool() { return Interlocked.Increment(ref m_iCurThreadsInPool); }
     /// <summary> SimType: Decrement current number of threads in the thread pool </summary>
     private Int32 DecCurThreadsInPool() { return Interlocked.Decrement(ref m_iCurThreadsInPool); }
   private Int32 m_iActThreadsInPool;
     /// <summary> SimType: The current number of active threads in the thread pool </summary>
     public Int32 GetActThreadsInPool { get { return m_iActThreadsInPool; } set { m_iActThreadsInPool = value; } }
     /// <summary> SimType: Increment current number of active threads in the thread pool </summary>
     private Int32 IncActThreadsInPool() { return Interlocked.Increment(ref m_iActThreadsInPool); }
     /// <summary> SimType: Decrement current number of active threads in the thread pool </summary>
     private Int32 DecActThreadsInPool() { return Interlocked.Decrement(ref m_iActThreadsInPool); }
   private Int32 m_iCurWorkInPool;
     /// <summary> SimType: The current number of Work posted in the thread pool </summary>
     public Int32 GetCurWorkInPool { get { return m_iCurWorkInPool; } set { m_iCurWorkInPool = value; } }
     /// <summary> SimType: Increment current number of Work posted in the thread pool </summary>
     private Int32 IncCurWorkInPool() { return Interlocked.Increment(ref m_iCurWorkInPool); }
     /// <summary> SimType: Decrement current number of Work posted in the thread pool </summary>
     private Int32 DecCurWorkInPool() { return Interlocked.Decrement(ref m_iCurWorkInPool); }

The constructor method does several things.  The class state is initialized and then the IOCP thread pool is created with a call to the CreateIoCompletionPort method.  Notice the method call is within the scope of the unsafe keyword.  This is required because we are passing pointers into the Win32 API call. 

The last thing we do is create the minimal number of threads specified by the application developer.  Notice we use the .NET threading classes to create the threads.  We do not need to use the unsafe CreateThread method.  One might think we need to because these threads will be calling the GetQueuedCompletionStatus Win32 API method.

   // Constructor, Finalize, and Dispose
   //***********************************************
   /// <summary> Constructor </summary>
   /// <param name = "iMaxConcurrency"> SimType: Max number of running threads allowed </param>
   /// <param name = "iMinThreadsInPool"> SimType: Min number of threads in the pool </param>
   /// <param name = "iMaxThreadsInPool"> SimType: Max number of threads in the pool </param>
   /// <param name = "pfnUserFunction"> DelType: Reference to a function to call to perform work </param>
   /// <exception cref = "Exception"> Unhandled Exception </exception>
  public IOCPThreadPool(Int32 iMaxConcurrency, Int32 iMinThreadsInPool, Int32 iMaxThreadsInPool, USER_FUNCTION pfnUserFunction)
   {
     try
     {
       // Set initial class state
       GetMaxConcurrency   = iMaxConcurrency;
       GetMinThreadsInPool = iMinThreadsInPool;
       GetMaxThreadsInPool = iMaxThreadsInPool;
       GetUserFunction     = pfnUserFunction;
       // Init the thread counters
       GetCurThreadsInPool = 0;
       GetActThreadsInPool = 0;
       GetCurWorkInPool    = 0;
       // Initialize the Monitor Object
       GetCriticalSection = new Object();
       // Set the disposing flag to false
       IsDisposed = false;
       unsafe
       {
         // Create an IO Completion Port for Thread Pool use
         GetHandle = CreateIoCompletionPort(INVALID_HANDLE_VALUE, 0, null, (UInt32) GetMaxConcurrency);
       }
       // Test to make sure the IO Completion Port was created
       if (GetHandle == 0)
         throw new Exception("Unable To Create IO Completion Port");
       // Allocate and start the Minimum number of threads specified
       Int32 iStartingCount = GetCurThreadsInPool;
       ThreadStart tsThread = new ThreadStart(IOCPFunction);
       for (Int32 iThread = 0; iThread < GetMinThreadsInPool; ++iThread)
       {
         // Create a thread and start it
         Thread thThread = new Thread(tsThread);
         thThread.Name = "IOCP " + thThread.GetHashCode();
         thThread.Start();
         // Increment the thread pool count
         IncCurThreadsInPool();
       }
     }
     catch
     {
       throw new Exception("Unhandled Exception");
     }
   }

The finalize method is only required to guarantee the IOCP thread pool handle is closed.  As a general rule, if a class allocates a resource outside the scope of the .NET framework, a finalize method is required else do not add a finalize method.  A finalize method will cause the garbage collection process to spend more time trying to release the memory for the object.

   //***********************************************
   /// <summary> Finalize called by the GC </summary>
   ~IOCPThreadPool()
   {
     if (!IsDisposed)
       Dispose();
   }

The dispose method will not return until all of the threads in the pool have been terminated.  We can’t use the Abort method to kill the threads in the pool because any thread blocked, via the call to the GetQueuedCompletionStatus Win32 API method, will not respond to the Abort message. 

The GetQueuedCompletionStatus Win32 API method will cause the thread to run outside the scope of the CLR and the .NET framework will lose access to the thread.  So what we do is post work into the IOCP thread pool.  We pass the SHUTDOWN_IOCPTHREAD data when we post the work.  This will tell the thread to terminate.  Then, we wait in a spin lock, until all of the threads have terminated.  The last thing is to close the IOCP thread pool.

   //**********************************************
   /// <summary> Called when the object will be shutdown.  This
   ///           function will wait for all of the work to be completed
   ///           inside the queue before completing </summary>
   public void Dispose()
   {
     try
     {
       // Flag that we are disposing this object
       IsDisposed = true;
       // Get the current number of threads in the pool
       Int32 iCurThreadsInPool = GetCurThreadsInPool;
       // Shutdown all thread in the pool
       for (Int32 iThread = 0; iThread < iCurThreadsInPool; ++iThread)
       {
         unsafe
         {
           bool bret = PostQueuedCompletionStatus(GetHandle, 4, (UInt32*) SHUTDOWN_IOCPTHREAD, null);
         }
       }
       // Wait here until all the threads are gone
       while (GetCurThreadsInPool != 0) Thread.Sleep(100);
       unsafe
       {
         // Close the IOCP Handle
         CloseHandle(GetHandle);
       }
     }
     catch
     {
     }
   }

The only private method is the IOCPFunction method.  This method is spawned as a thread and is made part of the IOCP thread pool by calling the GetQueuedCompletionStatus Win32 API method.  When the GetQueuedCompletionStatus Win32 API method returns, we check to make sure we are not being asked to shutdown the thread.  The third argument is the data associated with the posted work.  If the data is not SHUTDOWN_IOCPTHREAD, then real work has been posted into the IOCP thread pool and this thread has been chosen to process the work. 

The application developer’s supplied user function is called since the application developer is the only one who knows what needs to be done. Once that is complete, the method checks if a new thread should be added to the pool.  This is done by reviewing the number of active threads in the pool.

   // Private Methods
   //*******************************************
   /// <summary> IOCP Worker Function that calls the specified user function </summary>
   private void IOCPFunction()
   {
     UInt32 uiNumberOfBytes;
     Int32  iValue;
     try
     {
       while (true)
       {
         unsafe
         {
           OVERLAPPED* pOv;
           // Wait for an event
           GetQueuedCompletionStatus(GetHandle, &uiNumberOfBytes, (UInt32*) &iValue, &pOv, INIFINITE);
         }
         // Decrement the number of events in queue
         DecCurWorkInPool();
         // Was this thread told to shutdown
         if (iValue == SHUTDOWN_IOCPTHREAD)
           break;
         // Increment the number of active threads
         IncActThreadsInPool();
         try
         {
           // Call the user function
           GetUserFunction(iValue);
         }
         catch
         {
         }
         // Get a lock
         Monitor.Enter(GetCriticalSection);
         try
         {
           // If we have less than max threads currently in the pool
           if (GetCurThreadsInPool < GetMaxThreadsInPool)
           {
             // Should we add a new thread to the pool
             if (GetActThreadsInPool == GetCurThreadsInPool)
             {
               if (IsDisposed == false)
               {
                 // Create a thread and start it
                 ThreadStart tsThread = new ThreadStart(IOCPFunction);
                 Thread thThread = new Thread(tsThread);
                 thThread.Name = "IOCP " + thThread.GetHashCode();
                 thThread.Start();
                 // Increment the thread pool count
                 IncCurThreadsInPool();
               }
             }
           }
         }
         catch
         {
         }
         // Relase the lock
         Monitor.Exit(GetCriticalSection);
        // Increment the number of active threads
         DecActThreadsInPool();
       }
     }
     catch
     {
     }
     // Decrement the thread pool count
     DecCurThreadsInPool();
   }

The last two public methods are the PostEvent methods.  The first method takes an integer as an argument and the second version takes no argument at all.  The integer is the data the application developer wishes to pass with the work posted into the IOCP thread pool.  In the PostQueuedCompletionStatus Win32 API call, we can see that the third argument is where we pass the data value.   Since this value is always an integer we set the size of the data to four, as seen in the second argument.  Like in the IOCPFunction, we check to see if we need to add a new thread to the pool.

   // Public Methods
   //******************************************
   /// <summary> IOCP Worker Function that calls the specified user function </summary>
   /// <param name="iValue"> SimType: A value to be passed with the event </param>
   /// <exception cref = "Exception"> Unhandled Exception </exception>
   public void PostEvent(Int32 iValue)
   {
     try
     {
       // Only add work if we are not disposing
       if (IsDisposed == false)
       {
         unsafe
         {
           // Post an event into the IOCP Thread Pool
           PostQueuedCompletionStatus(GetHandle, 4, (UInt32*) iValue, null);
         }
         // Increment the number of item of work
         IncCurWorkInPool();
         // Get a lock
         Monitor.Enter(GetCriticalSection);
         try
         {
           // If we have less than max threads currently in the pool
           if (GetCurThreadsInPool < GetMaxThreadsInPool)
           {
             // Should we add a new thread to the pool
             if (GetActThreadsInPool == GetCurThreadsInPool)
             {
               if (IsDisposed == false)
               {
                 // Create a thread and start it
                 ThreadStart tsThread = new ThreadStart(IOCPFunction);
                 Thread thThread = new Thread(tsThread);
                 thThread.Name = "IOCP " + thThread.GetHashCode();
                 thThread.Start();
                 // Increment the thread pool count
                 IncCurThreadsInPool();
               }
             }
           }
         }
         catch
         {
         }
         // Release the lock
         Monitor.Exit(GetCriticalSection);
       }
     }
     catch (Exception e)
     {
       throw e;
     }
     catch
     {
       throw new Exception("Unhandled Exception");
     }
   } 
   //*****************************************
   /// <summary> IOCP Worker Function that calls the specified user function </summary>
   /// <exception cref = "Exception"> Unhandled Exception </exception>
   public void PostEvent()
   {
     try
     {
       // Only add work if we are not disposing
       if (IsDisposed == false)
       {
         unsafe
         {
           // Post an event into the IOCP Thread Pool
           PostQueuedCompletionStatus(GetHandle, 0, null, null);
         }
         // Increment the number of item of work
         IncCurWorkInPool();
         // Get a lock
         Monitor.Enter(GetCriticalSection);
         try
         {
           // If we have less than max threads currently in the pool
           if (GetCurThreadsInPool < GetMaxThreadsInPool)
           {
             // Should we add a new thread to the pool
             if (GetActThreadsInPool == GetCurThreadsInPool)
             {
               if (IsDisposed == false)
               {
                 // Create a thread and start it
                 ThreadStart tsThread = new ThreadStart(IOCPFunction);
                 Thread thThread = new Thread(tsThread);
                 thThread.Name = "IOCP " + thThread.GetHashCode();
                 thThread.Start();
                 // Increment the thread pool count
                 IncCurThreadsInPool();
               }
             }
           }
         }
         catch
         {
         }
         // Release the lock
         Monitor.Exit(GetCriticalSection);
       }
     }
     catch (Exception e)
     {
       throw e;
     }
     catch
     {
       throw new Exception("Unhandled Exception");
     }
   }
  }
}

We have now completed the implementation of the IOCP thread pool class.  Now it is time to test it.

IOCP Thread Pooling in C# - Part II - The Sample Application
(Page 2 of 3 )

Start by adding a new class to your C# project.  Remove all of the code provided by the Visual Studio .NET wizard.  Then add all of the following code.  In Main, an IOCP thread pool is created, and a single piece of work is posted to the IOCP thread pool.  We pass the data value of 10 along with the posted work. 

The main thread is then put to sleep. This gives the IOCP thread function time to wake up to process the work posted.  The last thing in main is to dispose the IOCP thread pool.  The IOCP thread function displays the value of the data passed into the IOCP thread pool.

using System;
using System.Threading;  // Included for the Thread.Sleep call
using Continuum.Threading;
namespace Sample
{
  //============================================
  /// <summary> Sample class for the threading class </summary>
  public class UtilThreadingSample
  {
   //*******************************************
   /// <summary> Test Method </summary>
   static void Main()
   {
     // Create the MSSQL IOCP Thread Pool
     IOCPThreadPool pThreadPool = new IOCPThreadPool(0, 5, 10, new IOCPThreadPool.USER_FUNCTION(IOCPThreadFunction));
     pThreadPool.PostEvent(10);
     Thread.Sleep(100);
     pThreadPool.Dispose();
   }
   //*****************************************
   /// <summary> Function to be called by the IOCP thread pool.  Called when
   ///           a command is posted for processing by the SocketManager </summary>
   /// <param name="iValue"> The value provided by the thread posting the event </param>
   static public void IOCPThreadFunction(Int32 iValue)
   {
     try
     {
       Console.WriteLine("Value: {0}", iValue);
     }
     catch (Exception pException)
     {
       Console.WriteLine(pException.Message);
     }
   }
  }
}

This is what you should see when you run the sample application.  On your own change the main function to call the PostEvent method several times and see how the IOCP thread pool performs.


"Network Programming" 카테고리의 다른 글
  • Socket Programming in C# (0)2007/07/27
  • Network Programming in C# (0)2007/07/27
  • IOCP Thread Pooling in C# (0)2007/07/26
  • UDP 프로그래밍의 기초 (0)2007/05/14
  • ICMP 프로그래밍 (0)2007/05/14
2007/07/26 14:07 2007/07/26 14:07
Posted by webdizen
Tags C#, IOCP, Pooling, Thread
No Trackback No Comment

Trackback URL : http://www.webdizen.net/blog/trackback/3095

Leave your greetings.

[로그인][오픈아이디란?]

Programming/MFC2007/05/17 17:48

쓰레드 풀 (Thread Pooling) 작성

쓰레드 풀은 연결/종료가 자주일어나는 웹서버와 같은 바쁜 서버에게 있어서 효율적인 클라이언트 연결 처리를 위해서 사용하는 프로그래밍 기법이다. 이번에는 쓰레드풀을 이용한 어플리케이션 제작방법에 대해서 알아보도록 하겠다.


1절. Thread Pooling
1.1절. Thread Pooling 이란
1.1.1절. Thread Pool의 구현방식
1.1.2절. 구현 프로세스
1.2절. 예제
2절. 결론

--------------------------------------------------------------------------------

1절. Thread Pooling
1.1절. Thread Pooling 이란
pool 의 사전적인 뜻을 찾아보면 연못, 저수지, 수영장 풀 등 "무엇을 담아놓는" 의 뜻을 가진다. 이대로 해석하자면 Thread Pooling 이란 쓰레드를 담아 놓는 용기(메모리가 될것이다) 를 뜻하며, 프로그래밍 측면에서 해석하자면, "미리 쓰레드를 할당시켜 놓는기법" 을 뜻한다.

그렇다면 쓰레드를 미리 할당시켜 놓는 이유에 대해서 생각해보자, 지금까지 이 사이트에서 다루었던 쓰레드프로그래밍 기법은 기본적으로 fork 방식과 매우 비슷하며, 쓰레드를 생성시켜야 될 필요가 있을때 pthread_create(3)등의 함수를 이용하여 새로운 작업쓰레드를 생성시키는 방식을 사용했다. 보통 쓰레드프로그래밍은 네트웍 프로그래밍시 주로 사용됨으로 accept(2) 로 연결을 기다리다가 연결이 만들어지면 accept 에서 넘어온 소켓 지시자를 인자로 하는 쓰레드를 생성했다.

이러한 방식 - 요청이 있을때 쓰레드를 생성시키는 - 의 쓰레드 프로그래밍기법은 대부분의 작업을 처리하기에 충분히 효율적이며, 빠르긴하지만 클라이언트로 부터의 연결과 종료가 매우 바쁘게 일어나는 서버의 경우, 계속적으로 쓰레드를 생성하고 종료해야 하는 비용을 무시할수 없게 된다. 쓰레드가 비록 fork()에 비해서 생성과 소멸시에 훨씬 적은 비용을 소모한다고는 하지만, 이건 어디까지나 상대적인 것으로 실상은 꽤 많은 시간과 비용을 소비하는 작업이다. 특히 Linux 에서의 Pthread 의 경우 clone(2)를 이용한 구현임으로 더욱더 많은 비용을 소비하게 된다.

Thread Pooling 은 이러한 반복적인 쓰레드의 생성/소멸에 의한 비효율적인 측면을 없애고자 하는 목적으로 만들어진 프로그래밍 기법이다.


--------------------------------------------------------------------------------

1.1.1절. Thread Pool의 구현방식
개념적으로 보자면 Thread Pool 을 구성하는건 매우 간단하다. 생성하고자 하는 크기만큼 ptread_create() 함수를 돌리면 되기 때문이다.

하지만 이건 어디까지나 개념적인 것으로 대부분의 경우 각각의 쓰레드를 스케쥴링 해주어야 함으로, 때에 따라서는 구현을 위해서 매우 복잡한 프로그래밍 기법을 동원해야 할때도 있다. 간단히 웹 서버를 Thread Pool 로 구현한다고 가정을 해보자 - 보통 웹서버는 HTTP 의 특성상 연결과/종료가 빈번하게 일어 남으로 쓰레드풀을 사용할경우 많은 이익을 얻을수 있다 -, 만약 100 개의 Thread 를 미리 생성시켰고, 각각의 Thread 는 하나의 클라이언트 연결을 처리한다고 가정했을때, main 쓰레드는 accept(2) 를 통해서 클라이언트를 받아들였을때, accept() 로 만들어진 소켓 지정번호를 미리 만들어진 100 개의 쓰레드중 "놀고" 있는 쓰레드에게 넘겨주어야 할것이다. 그러기 위해서는 main 쓰레드에서 각각의 쓰레드 상태를 유지해서 적당한 쓰레드에게 파일지정자를 넘겨줘야 할것이다.

그나마 위의 경우는 하나의 쓰레드가 하나의 연결을 처리함으로 어렵지 않게 구현하겠지만, 만약 100개의 쓰레드가 있고, 거기에 각각의 쓰레드가 10개 씩의 클라이언트 연결을 처리하도록 구성한다면, 거기에다가 적당한 로드밸런싱 기능 까지 포함시키고자 한다면, 구현이 꽤 복잡해 질수도 있다.


사용자 삽입 이미지

그림 1. Thread Pool 구성도



위는 Thread Pool 의 대략적인 구현상태를 그림? 으로 나타낸 것이다. Thread Pool 에 들어있는 각각의 쓰레드를 관리하기 위해서는 필수적으로 각각의 쓰레드의 상태를 가지고 있는 Schedul 자료구조 를 가지고 있어야한다. 그래야만 MAIN THREAD 에서 쓰레드 상태를 확인해서 적당한 쓰레드로 작업분배가 가능할것이기 때문이다. - 실제 Linux 커널도 각각의 task 의 스케쥴링을 위해서 task 구조체를 유지한다. -


1.1.2절. 구현 프로세스
이제 구현방식에 대한 밑그림이 나왔으니, 실제로 구현을 위한 프로세스를 만들어 보도록 하자. 프로세스는 슈도코드로 구성을 하도록 하겠다. 네트웍 서버 작성을 기준으로 하겠다.



구현은 구현하는 프로그래머가 상황에 따라서 선택하기 나름이긴 하지만 보통은 위의 방법을 기본으로 해서, 약간의 변경을 가하는 정도가 될것이다. 위의 슈도코드를 보면 main 쓰레드에서 accept 를 받으면 휴식상태에 있는 쓰레드를 깨운다고 되어있는데, 이때 깨우기 위해서는 쓰레드 조건변수를 사용하면 될것이다.

그렇다면 스케쥴관련 자료구조는 어떻게 구현하는게 쉬운방법인지 생각해보도록 하자. 구현하는 방법은 프로그래머 맘이겠지만, 필자가 구현하고자 한다면 multimap 을 이용해서 구현할것이다. 이 자료구조는 아마 다음과 같을것이다.



멀티맵의 key 는 쓰레드의 활성화 여부로 1 혹은 0이 된다. 그리고 value 는 해당 쓰레드 정보가 될것이다. 이렇게 멀티맵으로 만든이유는 간단하다. 멀티맵은 정렬연관 컨테이너 임으로 key 를 기준으로 자동적으로 정렬이 될것이다. 만약 첫번째 쓰레드가 처리중(1)로 변경되었다면 이 원소는 multimap 의 가장 뒤로 정렬이 될것이다. 그럼으로 우리는 클라이언트의 수가 총연결가능한 클라이언트수(Thread Pool 에 생성된 쓰레드수) 를 초과하지 않는한 phinfo.begin() 으로 가져온 쓰레드는 휴식상태(0) 이라는걸 믿을수 있게 된다. 다시 말해서 복잡해서 쓰레드상태가 0인지 1인지 처음부터 검사할 필요가 없다는 뜻이다.  
  1 2 3 4 5 6 7    99 100  : 쓰레드 번호
+-+-+-+-+-+-+-+---+-+-+
|0|0|0|0|0|0|0|...|0|0|
+-+-+-+-+-+-+-+---+-+-+

--> 연결이 들어왔다면
1 2 3 4 5 6 7    99 100  : 쓰레드 번호
+-+-+-+-+-+-+-+---+-+-+
|1|0|0|0|0|0|0|...|0|0|
+-+-+-+-+-+-+-+---+-+-+
|                   |  
+----------->-------+
가장 뒤로 자동으로 sort 됨

--> Sort 후
2 3 4 5 6 7 8   100 1 : 쓰레드 번호
+-+-+-+-+-+-+-+---+-+-+
|0|0|0|0|0|0|0|...|0|1|
+-+-+-+-+-+-+-+---+-+-+

--> 클라이언트가 99개가 접속해 있을경우
+-+-+-+-+-+-+-+---+-+-+
|0|1|1|1|1|1|1|...|1|1|
+-+-+-+-+-+-+-+---+-+-+

그럼으로 begin() 을 사용하게 될경우
언제나 휴식상태에 있는 쓰레드를 가져올수 있음
                               

사실 multimap 을 쓴다면 굳이 "현재 연결된 클라이언트 수" 를 유지하기 위해서 별도의 변수를 둘 필요가 없을것이다. multimap 에서 제공하는 count() 를 이용해서 key 가 "1" 인 요소의 수를 구하면 되기 때문이다. 만약 multimp 의 begin() 값이 1 이라면 MAX 클라이언트가 가득찼다는걸 의미할것이다.

물론 multimap 의 경우 기본적으로 key 값의 수정은 허용하지 않기 때문에 0 을 1로 변경할경우 실제로는 0 을 가지는 요소를 삭제하고, 1을 가지는 새로운 요소를 삽입하는 방식을 취해야 할것이다. 마찬가지로 클라이언트가 종료해서 1을 0으로 변경할때에도 삭제/인서트를 해야할것이다. Value(값) 는 그대로 복사해서 삭제/인서트를 해야 한다.

이 방법이 번거롭다면, 그냥 배열을 쓰거나 혹은 다른 어떤 자료구조를 쓰더라도 전혀 관계없기는 하다. 그건 자기의 기호에 맞게 선택해서 사용하면 될문제이다.


1.2절. 예제
지금까지 Thread POOL 의 구현방법에 대해서 알아봤으니, 간단하게 구현해 보도록 하겠다. 이 코드는 지극히 기능구현에만 신경쓴 코드이다. 에러처리와 몇군데 뮤텍스잠금처리는 각자의 재량에 맡기겠다.

예제 : pool_echo.cc


이 프로그램은 2개의 인자를 받아들이며, 클라이언트의 입력을 되돌려주는 일을한다 (echo 서버). 첫번째 인자는 서비스할 PORT 번호이고, 두번째 인자는 쓰레드 생성갯수이다. 프로그램은 인자의 정보를 이용해서 PORT 를 열고 클라이언트를 받아들인다. 클라이언트가 연결하면, Thread Pool 에 남는 공간이 있는지를 확인하고, 남는 공간이 있다면 클라이언트와 통신하게 된다.

단지 쓰레드를 미리 생성시키고 나서, 이것을 스케쥴링하기 위한 코드가 몇줄 추가되었을 뿐 특별히 복잡한 코드는 아닐거라고 생각된다.


--------------------------------------------------------------------------------

2절. 결론
이상 간단한 쓰레드 풀의 작성요령에 대해서 알아보았다. 위에서 설명했듯이 쓰레드 풀이란 개념적인 요소에 가까움으로 어떻게 구현할지는 상황에 따라서 매우 달라지게 되며, 위의 예제는 그러한 여러가지 상황중 가장 기본적인 상황을 예로 해서 만들어진 것이다. 어쨋든 위의 예제를 충분히 이해한다면 다른 상황으로의 응용역시 별 어려움없을 것이라고 생각된다.

쓰레드 풀은 보통 매우 효율적인 성능을 보장해주는 어플리케이션의 작성을 위해서 사용되어짐으로, 가능한한 빠른 쓰레드간 전환이 가능하도록 고민해서 코딩을 해야 한다. 위의 경우 쓰레드간 전환을 위해서 multimap 을 사용하고 있는데, accept 가 들어왔을경우 해당 클라이언트에 대한 쓰레드 할당은 매우 빠르다고 볼수 있을것이다. 그러나 종료할경우에는 multimap 의 첫번째 원소부터 마지막번 원소까지 search 해야 한다. 이것은 매우 비효율적임으로 개선할 여지가 있다. 가장 간단하게 생각할수 있는 것은 multimap 의 key 값이 1인 원소내에서만 검색하는 것이다. 우리는 쓰레드 풀의 크기와 현재 연결된 클라이언트의 수를 알고 있음으로, multimap 의 몇번째 요소부터 key 값이 1인지를 계산해 낼수 있기 때문이다. 이렇게 할경우 약간의 시간단축효과를 기대할수 있을것이다.    1 2 3 4 5 6 7 8 9 0 1 2 3 4 5
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|0|0|0|0|0|0|0|0|1|1|1|1|1|1|1|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
                 |             |
                 +-------------+
                     15 - 8
               

이 시간단축효과는 연결된 클라이언트의 수가 전체 POOL 사이즈에 비례해서 작을 수록 커질것이다.

나머지 방법은 각자 고민을 해보기 바란다. 아마 전혀 다른 자료구조를 사용할수도 있을것이다.


출처 : http://joinc.co.kr/modules.php?name=new ··· 3Dnested
"MFC" 카테고리의 다른 글
  • 쓰레드 풀 (Thread Pooling) 작성 (1)2007/05/17
  • 시간 중심의 코드 성능 향상을 위한 팁 (0)2007/05/17
  • 상태바위에 ProgressBar 올리기 #2 (0)2007/05/17
  • 상태바위에 ProgressBar 올리기 #1 (0)2007/05/17
  • 다이얼로그상의 특정 컨트롤의 색상 변경 (0)2007/05/17
2007/05/17 17:48 2007/05/17 17:48
Posted by webdizen
Tags Thread, Thread Pooling
No Trackback 1 Comment

Trackback URL : http://www.webdizen.net/blog/trackback/2984

Leave your greetings.

  1. ronie.kang

    ^^ 어디에 적용을 해야 할지가 참 중요한것 같아요

    2007/09/14 19:30 [ Permalink : Modify/Delete : Reply ]
[로그인][오픈아이디란?]

Programming/MFC2007/05/15 09:38

WaitForSingleObject() 에 의한 Thread 동기화

May 15 2003 11:47 Written by sinalove (codenuri.com member)


스레드를 동기화하는 방법에는 여러가지 방법이 있습니다.
저의 경우 예전에 전역플레그를 사용한 동기화 방법을 많이 사용했습니다.
그런데 이경우 시스템의 부하가 많이 발생하는 경우가 많습니다.
이러한 문제점을 해결하기 위해 WaitForSingleObject() 함수에 의한
동기화 방법에 대해 알아보겠습니다.
아주 간단하면서도 효과적이고 시스템의 부하도 적은 방법이니
알아두시면 유용하게 쓰실 기회가 반드시 생길겁니다.
엥 ??? 이미 다 알고 있으신 분은 걍 복습한다 생각하세용. ^^

1. 먼저 스레드의 이벤트 핸들을 정의합니다.
이 이벤트핸들은 스레드에서 이 핸들에 이벤트가 발생할경우 WaitForSingleObject()를
통과하도록 하기위해서 필요합니다.
헤더에 다음과 같이 정의합니다.



m_hEvnet 는 이벤트핸들이고, m_hThread 는 스레드 핸들입니다.

2. 이제 스레드함수를 만듭니다.

자 이스레드는 먼저 Wait 라는 메시지박스를 출력하고 이벤트가 발생하길 기다리다가
이벤트가 발생하면 End 라는 메시지박스를 출력하고 스레드를 종료할겁니다.

3. 자 이제 이벤트와 관련된 부분을 만듭시다.

이코드는 이벤트를 만들어 m_hEvent 에 붙이고 이벤트를 활성화하는 코드입니다.
적당한데 넣어주면 되겠지요.

4. 나머지는 스레드를 시작하는 부분과 스레드 시작후 이벤트를 발생시키는 부분입니다.
스레드의 시작은 다 아시겠지만

해주면 되겠고, 이벤트의 발생은



해주면 됩니다.
"MFC" 카테고리의 다른 글
  • IPC (Inter Process Communication) (0)2007/05/15
  • CFile 클래스를 이용한 파일 I/O (0)2007/05/15
  • WaitForSingleObject() 에 의한 Thread 동기화 (0)2007/05/15
  • 응용 프로그램의 INI 파일 바꾸기 (0)2007/05/15
  • Thread (쓰레드) 란 ? (3)2007/05/15
2007/05/15 09:38 2007/05/15 09:38
Posted by webdizen
Tags Thread, WaitForSingleObject
No Trackback No Comment

Trackback URL : http://www.webdizen.net/blog/trackback/2955

Leave your greetings.

[로그인][오픈아이디란?]

Programming/MFC2007/05/15 09:24

Thread (쓰레드) 란 ?

Nov 19 2002 16:20 Written by www.anygate.com


1. 쓰레드란?

용어가 그렇게 중요하지 않을 수도 있다는 생각을 가끔한다.
쓰레드란 표현도 그러한 용어중에 하나이다.
쓰레드라는 용어의 개념을 인지하기 전에 우린 이미 쓰레드를 사용하고 있으니 말이다.
이 글을 보는 사람은 누구나 Hellow World! 라는 전혀 쓸모없어 보이는
(프로그래밍에 첫 발을 디디는 사람들에겐 전설적인 프로그램이지만 지금은 냉정해질 필요가 있다.),
그러나 실행 가능한 프로그램을 작성해 보았을 것이다. 거기에도 쓰레드는 존재한다.
컴파일된 프로그램은 명령코드로 변환되어 디스크에 저장된다.
그리고 프로그램이 실행되면 운영체제에 의해 메모리에 올려지고 각각의 명령코드는 순서대로 실행된다.
이 정도의 지식만 있다면 현실 세계의 '실타래'를 전산용어인
쓰레드의 의미로 유추하는 일이 그리 어렵진 않을 것이다.
쓰레드란 바로 코드의 실행 흐름을 뜻하는 것이다.


2. 프로세스와 쓰레드

프로세스란 실행중인 프로그램을 의미한다.
좀 더 근사하게 말하면 실행중인 프로그램의 인스턴스(intance) 이다.
언뜻 프로세스와 쓰레드가 같은 개념으로 이해될 소지도 있지만 분명 차이는 있다.
초보자에게 가장 분명한 차이는 프로세스가 코드를 실행하지 않는다는 것이다.
앞서 프로그램이 실행되면 운영체제가 메모리에 실행코드를 올린다고 했는데
그 외에도 필요한 데이터와 운영체제가 그 프로그램을 위해 할당해 주는
시스템 자원(파일, 통신포트 같은)등도 함께 올려지며
이런 것들이 올려진 메모리의 주소영역을 갖고 있는것이 프로세스이다.
또한 결정적으로 쓰레드를 소유하는 것도 프로세스이다.
그래야 하나의 프로그램을 실행하는데 필요한 완벽한 준비가 될게 아닌가?
모든 것을 프로세스가 소유하므로 당연히 프로그램의 실행종료는
프로세스의 종료를 뜻하며 종료된 프로세스는 모든 자원을 운영체제에 되돌려 준다.

프로세스는 적어도 하나 이상의 쓰레드를 갖는다.
'적어도' 라는 말은 '반드시'라는 말과 같다.
반드시 필요한 하나의 쓰레드 (primary thread)를 메인 쓰레드 혹은 최우선 쓰레드라고도 하는데
용어야 뭐 대수겠는가? 어차피 응용 프로그램의 실행을 위한 기본적인 쓰레드를 말하는 것인데.

프로세스가 생성되면 메인 쓰레드는 프로세스가 할당한 메모리 영역에서 실행되며
프로세스에 할당된 시스템 리소스를 사용하게 된다.
쓰레드는 그 자체가 또 다른 쓰레드를 생성해 낼 수 있는데
결국 하나의 프로세스가 여러개의 쓰레드를 소유한 셈이며
이를 멀티 쓰레드(multi-thread)라고 말한다.
생성 가능한 쓰레드의 수는 몇 개일까?
프로세스가 할당받은 메모리의 크기가 허용하는 한 무한하다.


3. 윈도우즈와 멀티태스킹(multi-tasking)

윈도우즈는 멀티태스킹이 지원된다.
태스크(task)라는 말은 프로세스와 같은 개념으로 봐도 된다.
즉 여러개의 응용 프로그램이 동시에 실행되며 처리된다는 얘기이다.
주의할 점은 멀티태스킹이 단지 여러개의 프로그램을 실행만 시킨다는 뜻은 아니라는 것이다.
이것은 모니터로 윈도우즈를 지켜보고 있을때 쉽게 오해될 수 있다.
여러개가 실행되었지만 결국 사용하는 프로그램은 오로지 하나 아닌가?
사실 문서작성기에 열심히 타이핑하고 있는 사용자는 윈도우즈가
전적으로 문서작성기에만 신경을 곤두 세우고 있으리라 착각하기 쉽다.
그러나 윈도우즈는 비활성화되어 있는 다른 프로그램에도 똑같은 양의 관심을 가지고 있다.
공평한 배신자(?)인 셈인데 이는 윈도우즈 입장에서보면 배신이 아니고
여러개의 프로그램을 동시에 이용할 수 없는 사용자의 한계일 뿐이다.

두 개의 문서작성기에 동시에 입력할 수 있는가?
이것이 가능하다면 윈도우즈는 기꺼이 처리해 줄 것이다.

그럼 어떻게 윈도우즈가 여러개의 프로그램을 동시에 실행해 줄 수 있는가?
바로 시간을 쪼개 쓰는 것이다.
점잖게 '시분할'이라고 표현하는데 아주 짧은 시간동안에 윈도우즈는
다수의 프로그램 각각이 가진 실행코드를 돌아가며 조금씩 처리해 주는 방식이다.
그 시간은 굉장히 짧은 것이어서 사용자는 이를 눈치채지 못한다.
물론 실행된 프로그램이 많아진다면 이른바 과부하가 걸려 실행이 느려지기 시작하지만...

위에서 여러개의 프로그램이란 말을 여러개의 프로세스라는 말로 바꾸어 다시 설명한다면
윈도우즈는 (정확히는 윈도우즈의 커널) 모든 프로세스가 CPU를
공평하게 나누어 쓸 수 있도록 관리한다.
커널에 의해 각각의 프로세스는 굉장히 짧은 시간동안 CPU를 독차지하며
그동안 다른 프로세스는 순서가 올 때까지 대기하게 된다.
따라서 엄격히 말하면 현재 실행중이 아니거나 CPU를 사용하기 위해 기다리는 동안은
프로세스라고 할 수 없다. 어떤 책에는 프로세스가 기계의 현재 상태를 의미한다고 나와 있기도 하다.

참고로 윈도우즈는 이벤트 메시지 핸들 방식을 취한다.
즉 어떤 사건이 발생하면 메시지가 발생하고 사건이 발생된
윈도우(응용 프로그램)에 메시지가 보내져서 처리된다.
따라서 시분할이 적용되더라도 메시지를 가장 많이 받는 프로그램이
CPU를 상대적으로 많이 점유하는 형태가 된다.
그렇더라도 여전히 윈도우즈는 놀고있는 백수 프로그램에도
처리해야할 메시지가 있는가를 꼬박꼬박 돌아가며 확인한다.


4. 멀티 쓰레드와 쓰레드 함수

넓게 본다면 윈도우즈가 하나의 프로그램이고 윈도우즈에서 실행되는
여러개의 응용 프로그램들은 윈도우즈가 가진 다수의 쓰레드라고 볼 수 있다.
이 말은 사실이고 이것을 하나의 프로그램에 적용해 보면 쓰레드의 개념이 보다 확실해 진다.
대개 한 프로그램에서 동시에 여러가지 작업을 처리하는 경우는 드물지만
반드시 동시에 처리하지 않으면 안되는 작업이 있을 수 있다.
단일 쓰레드에선 동시 작업이 있을 수 없다.
하나의 코드가 실행되기 위해서는 그 전 코드의 실행이 끝나야만 하기 때문이다.
예를 들어 슈팅게임에서 총알을 발사했을때 프로그램된 일정량의 총알 궤적이 그려지는 작업이
끝나기 전에는 2번째 총알을 발사할 수 없게 된다면 얼마나 답답할 것인가?
반대로 총알을 그리는 작업이 쓰레드를 사용하여 메인 프로그램과는 상관없이
실행된다면 메인 프로그램은 따발로 총알을 갈겨댈 수 있을 것이다.(표현이 거칠군...)

자! 그럼 쓰레드는 어떻게 작성하는 것인가?

쓰레드는 일반 함수의 형태를 띤다.
그리고 그 형태는 정해져 있다.
다음이 쓰레드 함수의 원형인데 리턴형과 인자를 빼곤 당연히 함수명은 여러분 마음대로 정한다.



이 함수가 실행되면 쓰레드는 시작되며 함수실행이 끝나면 쓰레드도 끝난다.
이 함수도 값을 리턴하지만 프로그램에서 쓰레드 함수의 실행명령은
다음 명령의 실행에 아무런 영향을 미치지 않는다.
즉 쓰레드의 시작과 동시에 쓰레드가 종료되지 않았더라도
쓰레드 함수를 호출한 이후의 명령은 물 흐르듯이 순서대로 실행된다는 것이다.
메인 프로그램이 실행되는 동안 쓰레드는 배후에서 일을 하게 된다.
(그래야 동시작업의 의미가 있잖은가?)

쓰레드 함수의 리턴값은 실행의 성공여부를 알리는데 사용하며
보통 정상적으로 종료되었을때 '0'을, 아니면 '0'이 아닌 값을 리턴하게 된다.
(물론 프로그래머가 해줄 일이다.)

함수의 인자는 32bit 크기의 값이며 쓰레드 작업에 필요한 어떠한 형태의 정보(변수)든
이 인자를 통해 메인 프로그램으로부터 전달받을 수 있다.
이 인자의 사용에 대해서는 조금 후에 다시 언급하겠다.

이제 이 함수를 실행하는 일만 남았는데 이 함수의 직접호출은 배후작업을 지시하지 못한다.
쓰레드를 생성하기 위해선 MFC가 준비한 전역함수 AfxBeginThread(...)를 사용하게 되는데
다음이 이 함수의 원형이다. (2가지 버전이 있음)




두 버전의 쓰임새는 쓰레드의 종류에 따라 달라지는데 작업 쓰레드 (Worker thread)일 경우 전자를,
사용자 인터페이스 쓰레드 (User interface thread)일 경우엔 후자를 선택하게 된다.

쓰레드의 구분은 MFC를 사용한 프로그래밍에서 유효하며 win32 프로그래밍에선 쓰레드의 구분이 없다.
두 버전의 첫번째 인자를 보면 쉽게 구분이 갈 것이다.
여기서는 작업 쓰레드에 대해서만 다루겠다.

첫번째 인자는 위에서 만든 쓰레드 함수를 말하는데 단순히 함수명만 전달해 주면 된다.
두번째 인자는 쓰레드 함수의 인자에 사용되는 것과 동일한 32bit 값이다.
세번째 인자는 쓰레드의 우선순위이다. 보통 디폴트값을 그대로 사용하는데
이렇게 되면 쓰레드의 실행 순위는 다른 쓰레드와 동등해 진다.
작업내용에 따라 우선 순위를 특별하게 지정해야 한다면 라이브러리를 참조하기 바란다.
네번째 인자는 쓰레드에서 사용할 스택의 크기를 지정한다.
다섯번째 인자는 쓰레드 생성을 제어하는 플래그로 디폴트값처럼 '0'이 지정되면
쓰레드는 생성즉시 실행되며 CREATE_SUSPENDED를 지정하면
다른 쓰레드가 중지되기 전까지 실행을 지연시키게 된다.
마지막 인자는 보안속성을 나타내는 SECUTRITY_ATTRIBUTES 구조체를 가리키는데
보통 NULL을 사용한다.

두 버전 모두 생성된 쓰레드 객체의 포인터(CWinThread*)를 반환한다.
이 포인터를 사용하여 SuspendThread()나 ResumeThread()를 호출하면
쓰레드의 실행을 중지시킬 수도 있고 중지된 쓰레드를 다시 실행시킬 수도 있다.
CWinThread 클래스의 멤버들은 라이브러리를 참조하라.


5. 쓰레드 함수 사용예제

① 작업 쓰레드 함수의 작성



쓰레드 함수 내에서 맨 마지막 코드인 "return 0;"이 실행되면 쓰레드는 끝난다.
이 방법말고 쓰레드 내부에서 쓰레드를 종료하는 방법으로
AfxEndThread( UINT nExitCode )를 사용할 수도 있다.
만약 쓰레드 외부에서 쓰레드를 종료시키려면 API 함수인 TerminateThread(...)를 사용한다.
그러나 전역변수를 사용하여 쓰레드 내부에서 종료조건을 판단하는 루틴을 둔다면
쓰레드 내부에서 얼마든지 같은 효과를 볼 수 있다.
이 방법에 대해선 조금 후에 설명하겠다.

② 작업 쓰레드 함수의 실행



한가지 주의할 점은 쓰레드 함수가 C함수이므로 클래스의 멤버함수가 될 수 없다는 것이다.


6. 데이터의 전달

쓰레드 함수가 작업을 하기 위해 메인 프로그램의 데이터가 필요할 수 있다.
이 데이터는 윈도우 객체일 수도 있고 기타 구조체나 단순한 변수일 수도 있는데
그럴경우 쓰레드 함수의 LPVOID형 인자를 통해 전달해 주고
쓰레드 함수 내에서는 형변환을 사용하여 이를 취급할 수 있다.
그러나 전달해 주어야할 데이터가 많아진다면 어떻게 할 것인가?
가장 간단한 방법은 여러개의 변수를 묶은 구조체를 정의하고
그 구조체의 포인터를 전달하는 것이다.
또 다른 방법은 전역변수를 사용하는 것이다.
전역변수의 사용은 쓰레드 함수 외부, 즉 메인 프로그램에서 쓰레드를 제어하는데도 효과적이다.

예를들어 메인 프로그램에서 사용자의 입력을 받아 쓰레드에 해당 작업을 지시하는 방법을 보자.
메인 프로그램에서는 메뉴 또는 대화상자를 통해 사용자로부터
"작업1", "작업2", "작업3" 등의 명령을 처리하는 핸들러를 작성할 수 있다.
이때 그 명령처리 함수에서는 단지 전역변수에 작업의 종류만을 넣어주면
쓰레드에서 곧바로 실행할 수 있다.

다음은 쓰레드 함수에서의 처리방법이다.



이것은 아주 간단한 예에 불과하지만 효과적이다.
모든 작업 쓰레드는 모든 전역변수를 사용할 수 있기 때문이다.
"MFC" 카테고리의 다른 글
  • WaitForSingleObject() 에 의한 Thread 동기화 (0)2007/05/15
  • 응용 프로그램의 INI 파일 바꾸기 (0)2007/05/15
  • Thread (쓰레드) 란 ? (3)2007/05/15
  • Using User-Interface Threads (0)2007/04/29
  • Worker Threads (0)2007/04/29
2007/05/15 09:24 2007/05/15 09:24
Posted by webdizen
Tags Thread, 쓰레드
No Trackback 4 Comments

Trackback URL : http://www.webdizen.net/blog/trackback/2951

Leave your greetings.

  1. 김석경

    잘 보고 갑니다...개념 정리에 도움이 되었어요.

    2008/12/30 03:42 [ Permalink : Modify/Delete : Reply ]
    • webdizen

      도움이 되었다니, 기분이 좋아지네요.

      2009/01/12 22:29 [ Permalink : Modify/Delete ]
  2. ham

    잘보고 갑니다. 담아갈게요~

    2009/10/09 17:47 [ Permalink : Modify/Delete : Reply ]
  3. 장진혁

    학교 과제물중 쓰레드에 대하여 알아오라고 하였는데
    MFC까지 배우지 않아서 잘 모르겠지만 위쪽 설명만으로도 개념이 정리가 되네요...
    좋은 정보 감사합니다. (__)

    2010/03/17 11:41 [ Permalink : Modify/Delete : Reply ]
[로그인][오픈아이디란?]

Programming/UNIX/Linux C2007/05/14 09:29

pthread API 레퍼런스

출처 : http://teamblog.joinc.co.kr/yundream

pthread API의 레퍼런스 문서이다. 비록 모든 API들에 대한 레퍼런스를 담고 있지않지만, 자주 사용되는 주요 API 들에 대한 레퍼런스에 대한 설명과 예제까지를 포함하고 있다. 몇몇 빠진 API 들은 추후 보강하도록 할것이다.

1절. 소개
2절. 기본 쓰레드 함수
2.1절. pthread_create
2.2절. pthread_join
2.3절. pthread_detach
2.4절. pthread_exit
2.5절. pthread_cleanup_push
2.6절. pthread_cleanup_pop
2.7절. pthread_self
3절. 쓰레드 동기화 함수
3.1절. pthread_mutex_init
3.2절. pthread_mutex_destory
3.3절. pthread_mutex_lock
3.4절. pthread_mutex_unlock
3.5절. pthread_cond_init
3.6절. pthread_cond_signal
3.7절. pthread_cond_boradcast
3.8절. pthread_cond_wait
3.9절. pthread_cond_timewait
3.10절. pthread_cond_destroy
3.11절. 예제코드
4절. Thread Attribute 함수
4.1절. pthread_attr_init
4.2절. pthread_attr_distroy
4.3절. pthread_attr_getscope
4.4절. pthread_attr_setscope
4.5절. pthread_attr_getdetachstate
4.6절. pthread_attr_setdetachstate

--------------------------------------------------------------------------------

1절. 소개
이 문서는 pthread 레퍼런스 문서이다. pthread 에서 제공하는 모든 함수의 레퍼런스를 제공하고 있지는 않지만, 자주 쓰일만한 대부분의 함수들은 정리되어 있음으로 참고할만한 가치가 있을것이다.

이 문서에 빠진 내용들은 계속 추가해 나갈 예정이다.


--------------------------------------------------------------------------------

2절. 기본 쓰레드 함수
주로 쓰레드 생성과 종료에 관련된 가장 기본적인 함수들이다.


--------------------------------------------------------------------------------

2.1절. pthread_create

                       

쓰레드 생성을 위해서 사용한다. 첫번째 아규먼트인 thread 는 쓰레드가 성공적으로 생성되었을때 생성된 쓰레드를 식별하기 위해서 사용되는 쓰레드 식별자이다. 두번째 아규먼트인 attr 은 쓰레드 특성을 지정하기 위해서 사용하며, 기본 쓰레드 특성을 이용하고자 할경우에 NULL 을 사용한다. 3번째 아규먼트인 start_routine는 분기시켜서 실행할 쓰레드 함수이며, 4번째 아규먼는인 arg는 쓰레드 함수의 인자이다.

성공적으로 생성될경우 0을 리턴한다.

예제 : pthread_create.cc

                       

실행된 쓰레드에 대해서는 pthread_join 등의 함수를 이용해서 쓰레드 종료때까지 기다려줘야 한다. ptherad_join 은 일종의 fork 의 wait 와 비슷하게 작동하며, 쓰레드자원을 해제 시켜준다.


--------------------------------------------------------------------------------

2.2절. pthread_join

                       

첫번째 아규먼트 th는 기다릴(join)할 쓰레드 식별자이며, 두번째 아규먼트 thread_return은 쓰레드의 리턴(return) 값이다. thread_return 이 NULL 이 아닐경우 해다 포인터로 쓰레드 리턴 값을 받아올수 있다.

pthread_joinc.c




--------------------------------------------------------------------------------

2.3절. pthread_detach

                       

detach 는 "떼어내다" 라는 뜻을 가지며 main 쓰레드에서 pthread_create 를 이용해 생성된 쓰레드를 분리시킨다. 이 함수는 식별번호th인 쓰레드를 detach 시키는데, detach 되었을경우 해당(detach 된) 쓰레드가 종료될경우 pthread_joinc 을 호출하지 않더라도 즉시 모든 자원이 해제(free) 된다.

여기에서는 pthread_create 호출후 detach 하는 방법을 설명하고 있는데, pthread_create 호출시에 쓰레드가 detach 되도록 할수도 있다. 이에 대한 내용은 pthread_attr_setdetachstate 를 다루면서 설명하도록 하겠다.

예제 : pthread_detach.c

                       

위의 쏘쓰 코드에서 detach 시켰을때와 그렇지 않았을때의 메모리 상황을 비교해보기 바란다. detatach 를 했을경우 프로세스의 메모리 사용율과 detache 를 주석 처리했을경우의 메모리 사용율의 변화를 서로 비교해보면 되는데, detach 를 사용하지 않았을경우 t_function 이 종료가 되더라도 자원이 해제되지 않음을 볼수 있을것이다. 테스트는 간단한 스크립트를 이용하도록 한다.
[root@localhost test]# while [ 1 ]; do ps -aux | grep pthread | grep -v grep | grep -v vim; sleep 1; done
root      2668  0.0  0.1  1436  292 pts/8    S    18:37   0:00 ./pthread_detach
root      2668  0.0  0.1  1436  292 pts/8    S    18:37   0:00 ./pthread_detach
                       

위의 ps 내용에서 5번째 필드의 변화를 확인하면 된다.


--------------------------------------------------------------------------------

2.4절. pthread_exit

                       

pthread_exit 는 현재 실행중인 쓰레드를 종료시키고자 할때 사용한다. 만약 pthread_cleanup_push 가 정의되어 있다면, pthread_exit 가 호출될경우 cleanup handler 가 호출된다. 보통 이 cleanup handler 은 메모리를 정리하는 등의 일을 하게 된다.

예제 : pthread_exit.c




--------------------------------------------------------------------------------

2.5절. pthread_cleanup_push



이것은 cleanup handlers 를 인스톨하기 위해서 사용된다. pthread_exit(3) 가 호출되어서 쓰레드가 종료될때 pthread_cleanup_push 에 의해서 인스톨된 함수가 호출된다. routine이 쓰레드가 종료될때 호출되는 함수이다. arg는 아규먼트이다.

cleanup handlers 는 주로 자원을 되돌려주거나, mutex 잠금등의 해제를 위한 용도로 사용된다. 만약 mutex 영역에서 pthread_exit 가 호출되어 버릴경우 다른쓰레드에서 영원히 block 될수 있기 때문이다. 또한 malloc 으로 할당받은 메모리, 열린 파일지정자를 닫기 위해서도 사용한다.

예제 : pthread_cleanup.c




--------------------------------------------------------------------------------

2.6절. pthread_cleanup_pop
pthread_cleanup_push 와 함께 사용되며, install 된 cleanup handler 을 제거하기 위해서 사용된다.

                       

만약 execute 가 0 이라면, pthread_cleanup_push 에 의해 인스톨된 cleanup handler 를 (실행시키지 않고)삭제만 시킨다. 0 이 아닌 숫자라면 cleanup handler 을 실행시키고 삭제 된다. 사용예제는 2.5절을 참고하라.

그리고 pthread_cleanup_push 와 pthread_cleanup_pop 은 반드시 같은 함수내의 같은 레벨의 블럭에서 한쌍으로 사용해야 한다.


--------------------------------------------------------------------------------

2.7절. pthread_self

                       

pthread_self를 호출하는 현재 쓰래드의 쓰레드식별자를 되돌려준다.

예제 : pthread_self.c




--------------------------------------------------------------------------------

3절. 쓰레드 동기화 함수
쓰레드 동기화와 관련된 함수들이다.


--------------------------------------------------------------------------------

3.1절. pthread_mutex_init

                       

mutex 는 여러개의 쓰레드가 공유하는 데이타를 보호하기 위해서 사용되는 도구로써, 보호하고자 하는 데이타를 다루는 코드영역을 단지 한번에 하나의 쓰레드만 실행가능 하도록 하는 방법으로 공유되는 데이타를 보호한다. 이러한 코드영역(하나의 쓰레드만 점유가능한)을 critical section 이라고 하며, mutex 관련 API 를 이용해서 관리할수 있다.

pthread_mutex_init 는 mutex 객체를 초기화 시키기 위해서 사용한다. 첫번째 인자로 주어지는 mutex 객체 mutex를 초기화시키며, 두번째 인자인 attr 를 이용해서 mutex 특성을 변경할수 있다. 기본 mutex 특성을 이용하기 원한다면 NULL 을 사용하면 된다.

mutex 특성(종류) 에는 "fast", "recurisev", "error checking" 의 종류가 있으며, 기본으로 "fast" 가 사용된다.





--------------------------------------------------------------------------------

3.2절. pthread_mutex_destory

                       

인자로 주어진 뮤텍스 객체 mutex 를 제거하기 위해서 사용된다. mutex 는 pthread_mutex_init()함수를 이용해서 생성된 뮤텍스 객체이다.

pthread_mutex_destory 를 이용해서 제대로 mutex 를 삭제하려면 이 mutex 는 반드시 unlock 상태이여야 한다.


--------------------------------------------------------------------------------

3.3절. pthread_mutex_lock

                       

pthread_mutex_lock 는 critcal section 에 들어가기 위해서 mutex lock 을 요청한다. 만약 이미 다른 쓰레드에서 mutex lock 를 얻어서 사용하고 있다면 다른 쓰레드에서 mutex lock(뮤텍스 잠금) 을 해제할때까지(사용할수 있을때까지) 블럭 된다.

만약 다른 어떤 쓰레드에서도 mutex lock 을 사용하고 있지 않다면, 즉시 mutex lock 을 얻을수 있게 되고 critcal section 에 진입하게 된다. critcal section 에서의 모든 작업을 마쳐서 사용하고 있는 mutex lock 이 더이상 필요 없다면 pthread_mutex_unlock 를 호출해서 mtuex lock 를 되돌려준다.


--------------------------------------------------------------------------------

3.4절. pthread_mutex_unlock

                       

critical section 에서의 모든 작업을 마치고 mutex lock 을 돌려주기 위해서 사용한다. pthread_mutex_unlock 를 이용해서 mutex lock 를 되돌려주면 다른 쓰레드에서 mutex lock 를 얻을수 있는 상태가 된다.


--------------------------------------------------------------------------------

3.5절. pthread_cond_init

                       

pthread_cond_init는 조견변수 (condition variable)cond를 초기화하기 위해서 사용한다. attr 를 이용해서 조건변수의 특성을 변경할수 있으며, NULL 을 줄경우 기본특성으로 초기화된다.

조건변수 cond는 상수 PTHREAD_COND_INITIALIZER 을 이용해서도 초기화 할수 있다. 즉 다음과 같은 2가지 초기화 방법이 존재한다.
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
or
pthread_cond_init(&cond, NULL);
                       




--------------------------------------------------------------------------------

3.6절. pthread_cond_signal

                       

조건변수 cond에 시그날을 보낸다. 시그날을 보낼경우 cond에서 기다리는(wait) 쓰레드가 있다면 쓰레드를 깨우게 된다(봉쇄가 풀림). 만약 조건변수 cond를 기다리는 쓰레드가 없다면, 아무런 일도 일어나지 않게되며, 여러개의 쓰레드가 기다리고 있다면 그중 하나의 쓰레드에게만 전달된다. 이때 어떤 쓰레드에게 신호가 전달될지는 알수 없다.


--------------------------------------------------------------------------------

3.7절. pthread_cond_boradcast

                       

조건변수 cond에서 기다리는(wait) 모든 쓰레드에게 신호를 보내서, 깨운다는 점을 제외하고는 pthread_cond_signal과 동일하게 작동한다.


--------------------------------------------------------------------------------

3.8절. pthread_cond_wait
                       

조건변수 cond를 통해서 신호가 전달될때까지 블럭된다. 만약 신호가 전달되지 않는다면 영원히 블럭될수도 있다. pthread_cond_wait는 블럭되기 전에 mutex 잠금을 자동으로 되돌려준다.


--------------------------------------------------------------------------------

3.9절. pthread_cond_timewait

                       

조건변수 cond를 통해서 신호가 전달될때까지 블럭되며 자동으로 mutex을 돌려주는 점에서는 pthread_cond_wait와 동일하다. 그러나 시간체크가 가능해서 abstime시간동안 신호가 도착하지 않는다면 error 를 발생하면서 리턴한다. 이때 리턴값은 ETIMEDOUT 이다. errno 가 세팅되는게 아닌, 리턴값으로 에러가 넘어오는것에 주의해야 한다.

또한 pthread_cond_timedwait함수는 다른 signal 에 의해서 interrupted 될수 있으며 이때 EINTR 을 리턴한다. 이 함수를 쓸때는 interrupted 상황에 대한 처리를 해주어야 한다.


--------------------------------------------------------------------------------

3.10절. pthread_cond_destroy

                       

pthread_cond_init를 통해서 생성한 조건변수cond에 대한 자원을 해제한다. destroy 함수를 호출하기 전에 어떤 쓰레드도 cond에서의 시그널을 기다리지 않는걸 확인해야 한다. 만약 cond 시그널을 기다리는 쓰레드가 존재한다면 이 함수는 실패하고 EBUSY 를 리턴한다.


--------------------------------------------------------------------------------

3.11절. 예제코드
이번장에서 설명한 쓰레드 동기화 관련 함수의 이해를 돕기 위해서 간단한 예제를 준비했다. 설명은 주석으로 대신한다.

예제 : pthrad_sync_api.c
                       



위의 예제는 ping&pong 프로그램으로 ping 쓰레드와 pong 쓰레드가 각각 번갈아가면서 "ping", "pong" 을 날리는 프로그램이다. 2개의 영역에 걸쳐서 크리티컬섹션이 지정되어 있으며 각 크리티컬섹션안에는 쓰레드 동기화를 위해서 ptread_cond_signal 이 쓰여지고 있다.

위의 코드는 기본적으로 pong 쓰레드가 먼저 시그널을 대기하고 있다가 그 후 ping 쓰레드가 진입해서 "ping"을 날리고 시그널을 발생시키면 "pong" 메시지를 발생시키도록 되어 있다. 그렇다면 while 문에 있는 크리티컬 섹션에 반드시 pong 쓰레드가 먼저 진입할수 있도록 만들어줘야 할것이다. 그래서 위의 코드에서는 pong 쓰레드를 먼저 생성시켰다. 그러나 이것만으로는 충분하지 않다. 예를들어서 pong 쓰레드에서 크리티컬섹션에 들어가기 위해서 어떤 부가적인 작업이 있다고 했을때(메모리초기화, 기타 다른 함수 호출과 같은, 위에서는 sleep 으로 대신했다), 우리가 의도했던 바와는 다르게 ping 가 먼저 크리티컬섹션에 진입할수도 있다. 이럴경우 2개의 쓰레드는 교착상태에 빠지게 된다.

ping 쓰레드가 크리티컬섹션에 먼저 진입했을경우 ping 쓰레드는 "ping" 출력시키고 시그널을 발생시킬 것이고 pong 쓰레드가 "pong"를 출력시키고 시그널을 발생시킬때까지 시그널대기 하게 된다. ping 쓰레드가 시그널대기 하게 되면, 크리티컬섹션에 대한 뮤텍스 잠금이 해제됨으로 뒤늦게 크리티컬섹셔네 진입을 시도하던 pong 가 크리티컬섹션에 진입하고 ping 쓰레드에서부터 신호가 있는지 기다리게 될것이다. 그러나 ping 쓰레드는 이미 신호를 날려버렸음으로, pong 쓰레드는 결코 도착하지 않을 신호를 기다리며 영원히 시그널대기 하게 될것이다. 이런식으로 2개의 쓰레드는 교착상태에 빠져 버린다.

이 문제는 쓰레드간 동기화를 이용해서 해결할수 있으며, 위 코드에서는 mutex 잠금과, 조건변수를 이용해서 해결하고 있다. 물론 쓰레드간 동기화를 위해서 사용할수 있는 원시?적인 방법으로 sleep 나 usleep 같은 함수를 호출하는 방법도 있긴 하지만, ping 쓰레드에서 크리티컬 섹션에 진입하기전 1초 정도 sleep 을 주는 식으로 사용가능하지만 추천할만하진 않다. (간혹 간단하게 사용할수는 으며, 가장 확실한 방법을 제공해 주기도 한다)


--------------------------------------------------------------------------------

4절. Thread Attribute 함수
4.1절. pthread_attr_init


                       

pthread_attr_init는 thread attribute 객체인 attr을 디폴트 값으로 초기화 시킨다.

성공할경우 0을 돌려주고 실패할경우 -1 을 되돌려준다.


--------------------------------------------------------------------------------

4.2절. pthread_attr_distroy

                       

pthread_attr_init에 의해 생성된 thread attribute 객체인 attr을 제거한다. 제거된 attr 을 다시 사용하기 위해서는 pthread_attr_init를 이용해서 다시 초기화 해주어야 한다.


--------------------------------------------------------------------------------

4.3절. pthread_attr_getscope

                       

쓰레드가 어떤 영역(scope)에서 다루어지고 있는지를 얻어오기 위해서 사용된다. PTHREAD_SCOPE_SYSTEM과 PTHREAD_SCOPE_PROCESS 의 2가지 영역중에 선택할수 있다. SYSTEM 영역 쓰레드는 user 모드 쓰레드라고 불리우며, PROCESS 쓰레드는 커널모드 쓰레드라고 불리운다. 리눅스의 경우 유저모드 쓰레드인데, 즉 커널에서 쓰레드를 스케쥴링하는 방식이 아닌 쓰레드 라이브러리를 통해서 쓰레드를 스케쥴링 하는 방식을 사용한다.


                       

위 프로그램을 컴파일한후 Linux 에서 실행시키면 "user mode thread"를 출력하고 솔라리스 상에서 실행시키면 "kernel mode thread"를 출력한다.


--------------------------------------------------------------------------------

4.4절. pthread_attr_setscope

                       

쓰레드가 어떤 영역(scope)에서 작동하게 할것인지 결정하기 위해서 사용한다. 리눅스의 경우 Kernel mode 쓰레드를 지원하지 않음으로 오직 PTHREAD_SCOPE_SYSTEM 만을 선택할수 있다. 반면 솔라리스는 유저모드와 커널모드중 선택이 가능하다.

pthread_attr_setscope.c

                       

위코드에서 쓰레드가 커널 모드에서 작동하도록 지정을 했다. 리눅스에서 실행시킬경우에는 비록 커널모드로 지정을 했다고 하더라도 유저모드 쓰레드로 작동하게 된다. 솔라리스의 경우에는 setscope 로 지정한대로 커널모드에서 작동하게 된다.


--------------------------------------------------------------------------------

4.5절. pthread_attr_getdetachstate

                       

쓰레드가 join 가능한 상태(PTHREAD_CREATE_JOINABLE) 인지 detached 상태인지 (PTHREAD_CREATE_DETACHED) 인지를 알아낸다. 알아낸 값은 아규먼트 detachstate 에 저장된다.

기본은 PTHREAD_CREATE_JOINABLE 이며, pthread_detach를 이용해서 생성된 쓰레드를 detach 상태로 만들었을경우 또는 pthread_attr_setdetachstate함수를 이용해서 쓰레드를 detache 상태로 변경시켰을경우 PTHREAD_CREATE_DETACHED 상태가 된다.

예제 : pthread_attr_getdetachstate.c

                       

위의 프로그램을 실행시키면 분명 "Join able"를 출력할것이다.


--------------------------------------------------------------------------------

4.6절. pthread_attr_setdetachstate

                       

쓰레드의 상태를 PTHREAD_CREATE_JOINABLE 혹은 PTHREAD_CREATE_DETACHED 상태로 변경시키기 위해서 사용된다. 아래와 같은 방법으로 사용하면 된다.

"UNIX/Linux C" 카테고리의 다른 글
  • random 값 얻어오기 (0)2007/05/14
  • openssl 을 통한 데이타 암호화 (0)2007/05/14
  • pthread API 레퍼런스 (3)2007/05/14
  • 안전한 프로그래밍 (0)2007/05/11
  • 쓰레드와 시그널 (0)2007/05/11
2007/05/14 09:29 2007/05/14 09:29
Posted by webdizen
Tags API, pthread, Thread, 레퍼런스
1 Trackback 2 Comments

Trackback URL : http://www.webdizen.net/blog/trackback/2931

  1. Pthread 채널  Delete

    2007/05/30 10:32 Tracked from

    Pthread와 관련된 내용들을 다루는 채널입니다.트랙백 주소는 http://teamblog.joinc.co.kr/Cpthread/trackback/1 입니다.카테고리 페이지로 바로가기

Leave your greetings.

  1. yundream

    쓰레드 관련 글을 찾다가 들어왔습니다.
    예전에 제가 작성한 글이군요.
    쓰레드와 관련되어서 몇개 참고할만한 글들이 더 있어서 트랙백겁니다.

    쓰레드와 관련된 많은 정보교환 부탁드립니다. 그럼 오늘도 좋은 하루 되세요.

    2007/05/30 10:34 [ Permalink : Modify/Delete : Reply ]
  2. webdizen

    안녕하세요.
    저도 joinc가 아닌 다른 곳에서 정보를 얻었는지라...
    지금보니 출처 표시를 미처 하지 못했었네요. 죄송합니다.

    해당 주소로 출처 표시 하겠습니다.

    2007/05/31 10:41 [ Permalink : Modify/Delete : Reply ]
[로그인][오픈아이디란?]

Programming/UNIX/Linux C2007/05/11 10:15

쓰레드와 시그널

그렇잖아도 애매모호한 쓰레드에 헷갈리는 시그널을 사용하고자 하면 여러가지 애로사항이 꽃피게 된다. 각 쓰레드별로 시그널이 전달되거나 전달되지 않도록 설정할 수 있어야 하기 때문인데, 개념적으로는 간단하지만 막상 적용하려면 그 과정이 머리에 그려지지 않기 때문이다.


차례
1. 쓰레드에서의 시그널 사용
1.1. 시그널을 특정 쓰레드로 보내기
1.1.1. 간단 예제
1.2. 쓰레드간 시그널 전송
1.2.1. 다른 쓰레드로 시그널 전송
1.2.2. 시그널 받기
1.2.3. 예제
1.2.4. 시그널을 이용한 쓰레드 작동 제어
1.3. 운영체제별 차이점


1. 쓰레드에서의 시그널 사용
쓰레드에서의 시그널 사용은 시그널에 대한 기본적인 이해만 가지고 있다면 약간의 응용으로 충분히 해결할 수 있는 문제이긴 하지만 범 유닉스적으로 응용하고자 한다면(특히 리눅스가 포함된다면) 운영체제간 신경써줘야할 문제가 있다. 이번장에서는 쓰레드에서의 시그널을 이용하는 방법과 운영체제가 다름으로 인해 발생할 수 있는 문제들에 대해서 알아보도록 하겠다.


--------------------------------------------------------------------------------

1.1. 시그널을 특정 쓰레드로 보내기
쓰레드에서 시그널은 서로 공유된다는걸 알고 있을 것이다. 문제는 공유된다는 점인데 만약 프로세스에 시그널을 보낼 경우 해당 프로세스에서 생성된 모든 쓰레드에 시그널이 전달이 되게 된다. 이것은 우리가 원하는게 아니다.

우리가 원하는 것은 특정 쓰레드에서만 시그널을 받도록 하는 것이다. 이러한 작업을 위해서 우리는 시그널 마스크를 사용한다. 시그널 마스크는 말그대로 특정 시그널에 대해서 마스크를 씌우는 것으로 해당 쓰레드에서 특정 시그널에 대해서 마스크를 씌우면 마스킹된 시그널은 해당 쓰레드로 전달되지 않는다. 이 시그널을 받기를 원하는 쓰레드에서는 이 시그널에 대한 마스크를 제거시킨다. 그러면 블럭되어 있는 시그널은 마스크가 제거된 쓰레드로 전달될 것이다. 일종의 필터기다.

사용자 삽입 이미지
그림 1. 시그널 마스크의 작동원리
 

위의 그림은 시그널 마스크의 작동원리를 보여준다. 메인 쓰레드에서는 SIGINT와 SIGUSR2에 대해서 시그널 마스크를 설치한다. 그리고 쓰레드 1에서는 SIGINT에 대한 마스크를 제거하고, 쓰레드 2에서는 SIGUSR2에 대한 마스크를 제거한다. 이렇게 되면 SIGINT가 메인 쓰레드에 도착했을 때 마스크 때문에 메인 쓰레드에는 도착하지 못하고 쓰레드 1로 전달될 것이다. SIGUSR2가 도착했을 경우 메인 쓰레드와 쓰레드 1에서는 마스크 때문에 전달되지 못하고 쓰레드 2로 시그널이 전달된다. 1.1.1절에서는 위의 작동원리 대로 구현된 예제 코드를 다루고 있다.

이러한 쓰레드별 시그널 마스킹을 위해서 pthread는 pthread_sigmask(3)라는 함수를 제공한다.

                       

이 함수는 현재 쓰레드에 시그널newmask와 how 를 이용해서 시그널 마스크를 만든다. how는 SIG_BLOCK, SIG_UNBLOCK, SIG_SETMASK중 하나를 선택할 수 있다. SIG_BLOCK는 현재 설정된 시그널 마스크에 newmask를 추가하며 SIG_UNBLOCK는 현재 설정된 시그널 마스크에서 newmask를 제거하고 SIG_SETMASK는 newmask로 현재 시그널 마스크를 설정한다.


--------------------------------------------------------------------------------

1.1.1. 간단 예제
그럼 pthread_mask(3)를 이용한 간다한 예제를 만들어 보도록 하겠다. 코드는 여러분이 시그널과 쓰레드에 관한 최소한의 지식을 가지고 있다는 가정하에 작성될 것이며, 설명은 주석으로 대신하도록 하겠다.

예제 : th_signal.c

                               

위 프로그램을 실행시킨뒤 kill명령으로 SIGINT와 SIGUSR2 시그널을 PID로 보내보면 해당 쓰레드로 시그널이 전달되고 시그널 핸들러가 실행되는걸 확인할 수 있을 것이다.

위의 그림은 시그널 마스크의 작동원리를 보여준다. 메인 쓰레드에서는 SIGINT와 SIGUSR2에 대해서 시그널 마스크를 설치한다. 그리고 쓰레드 1에서는 SIGINT에 대한 마스크를 제거하고, 쓰레드 2에서는 SIGUSR2에 대한 마스크를 제거한다. 이렇게 되면 SIGINT가 메인 쓰레드에 도착했을 때 마스크 때문에 메인 쓰레드에는 도착하지 못하고 쓰레드 1로 전달될 것이다. SIGUSR2가 도착했을 경우 메인 쓰레드와 쓰레드 1에서는 마스크 때문에 전달되지 못하고 쓰레드 2로 시그널이 전달된다. 1.1.1절에서는 위의 작동원리 대로 구현된 예제 코드를 다루고 있다.

이러한 쓰레드별 시그널 마스킹을 위해서 pthread는 pthread_sigmask(3)라는 함수를 제공한다.

                       

이 함수는 현재 쓰레드에 시그널newmask와 how 를 이용해서 시그널 마스크를 만든다. how는 SIG_BLOCK, SIG_UNBLOCK, SIG_SETMASK중 하나를 선택할 수 있다. SIG_BLOCK는 현재 설정된 시그널 마스크에 newmask를 추가하며 SIG_UNBLOCK는 현재 설정된 시그널 마스크에서 newmask를 제거하고 SIG_SETMASK는 newmask로 현재 시그널 마스크를 설정한다.


--------------------------------------------------------------------------------

1.1.1. 간단 예제
그럼 pthread_mask(3)를 이용한 간다한 예제를 만들어 보도록 하겠다. 코드는 여러분이 시그널과 쓰레드에 관한 최소한의 지식을 가지고 있다는 가정하에 작성될 것이며, 설명은 주석으로 대신하도록 하겠다.

예제 : th_signal.c

                               

위 프로그램을 실행시킨뒤 kill명령으로 SIGINT와 SIGUSR2 시그널을 PID로 보내보면 해당 쓰레드로 시그널이 전달되고 시그널 핸들러가 실행되는걸 확인할 수 있을 것이다.

위의 그림은 시그널 마스크의 작동원리를 보여준다. 메인 쓰레드에서는 SIGINT와 SIGUSR2에 대해서 시그널 마스크를 설치한다. 그리고 쓰레드 1에서는 SIGINT에 대한 마스크를 제거하고, 쓰레드 2에서는 SIGUSR2에 대한 마스크를 제거한다. 이렇게 되면 SIGINT가 메인 쓰레드에 도착했을 때 마스크 때문에 메인 쓰레드에는 도착하지 못하고 쓰레드 1로 전달될 것이다. SIGUSR2가 도착했을 경우 메인 쓰레드와 쓰레드 1에서는 마스크 때문에 전달되지 못하고 쓰레드 2로 시그널이 전달된다. 1.1.1절에서는 위의 작동원리 대로 구현된 예제 코드를 다루고 있다.

이러한 쓰레드별 시그널 마스킹을 위해서 pthread는 pthread_sigmask(3)라는 함수를 제공한다.

                       

이 함수는 현재 쓰레드에 시그널newmask와 how 를 이용해서 시그널 마스크를 만든다. how는 SIG_BLOCK, SIG_UNBLOCK, SIG_SETMASK중 하나를 선택할 수 있다. SIG_BLOCK는 현재 설정된 시그널 마스크에 newmask를 추가하며 SIG_UNBLOCK는 현재 설정된 시그널 마스크에서 newmask를 제거하고 SIG_SETMASK는 newmask로 현재 시그널 마스크를 설정한다.


--------------------------------------------------------------------------------

1.1.1. 간단 예제
그럼 pthread_mask(3)를 이용한 간다한 예제를 만들어 보도록 하겠다. 코드는 여러분이 시그널과 쓰레드에 관한 최소한의 지식을 가지고 있다는 가정하에 작성될 것이며, 설명은 주석으로 대신하도록 하겠다.

예제 : th_signal.c

                               

위 프로그램을 실행시킨뒤 kill명령으로 SIGINT와 SIGUSR2 시그널을 PID로 보내보면 해당 쓰레드로 시그널이 전달되고 시그널 핸들러가 실행되는걸 확인할 수 있을 것이다.

1.2. 쓰레드간 시그널 전송
외부의 다른 프로세스에서 시그널을 발생시키는 것 외에도 같은 프로세스에서 작동하는 쓰레드간에 시그널을 전송해야 하는 경우도 생길 것이다.

이러한 쓰레드간 시그널 전송은 여러가지 목적으로 사용할 수 있다. 일정시간마다 특정 쓰레드에 시그널을 전송하므로써 쓰레드를 깨워서 코드를 실행시키게 한다거나 네트워크 애플리케이션에서 write, read에 타임아웃을 검사하는 용도로도 사용가능 하다.

네트워크 애플리케이션에서 스레드간 시그널 전달을 통해 타임아웃을 검사한다는 생각은 좀 생소할 수도 있을것 같다. 보통은 select나 alarm을 사용할 건데, 멀티 쓰레드 프로그램의 경우 alarm(2)의 사용은 사실상 어렵다고 볼 수 있다. 여러개의 쓰레드에서 alarm(2)을 사용할 경우 단지 하나의 alarm(마지막 alarm값)만이 등록되어서 사용할 수 있기 때문이다. 그렇다면 select를 사용해야 할 건데, select대신에 전용의 시그널을 발생하는 쓰레드를 이용해서 사용할 수 있다.

read(2)를 예로 들어서 설명해 보자 read(2)를 하기전에 특정 (전역)값을 0으로 세팅하고 read를 수행한후 1로 값을 변경하도록 한다. 그리고 타임아웃 체크를 위한 쓰레드에서는 타임아웃 시간 간격으로(sleep(2)를 이용하면 된다) 이 값을 검사한다. 만약 값이 0으로 세팅되어 있는걸 확인 했는데, 다음 시간이 돌아온 뒤에도 이 값이 0이라면 read영역에서 타임아웃이 발생했다고 판단 할 수 있을 것이다. 그러면 타임아웃이 발생한 쓰레드에 시그널을 전송하도록 한다. 쓰레드에 시그널이 전송하면 인터럽트가 발생하고 read에서 빠져나오게 된다.


                       

시그널 발생시 인터럽트가 전달되게 하려면 약간의 부가적인 작업이 필요한데, 이것은 소켓 타임아웃을 참고하기 바란다.



1.2.1. 다른 쓰레드로 시그널 전송
이러한 쓰레드간 시그널 전송을 위해서 pthread_kill(3)이라는 함수가 제공된다.
                               

첫번째 인자thread는 시그널을 전달받을 쓰레드의 식별자이고 signo는 전달하고자 하는 시그널 번호이다. 보내는 쪽은 pthread_kill(3)을 이용해서 비교적 간단하게 구현이 가능하다.


--------------------------------------------------------------------------------

1.2.2. 시그널 받기
시그널을 받는 쓰레드의 경우 동기와 비동기 두가지 방식을 통해서 받을 수 있다. 동기 방식으로 받을 경우는 sigwait(3)함수를 이용해서 시그널이 전달될 때까지 블럭되면서 기다린다.

                               

이 함수는 시그널 셋set에 설정된 시그널중 하나가 전달될 때까지 호출된 영역에서 대기한다. 시그널을 받았다면 리턴되고 전달 받은 시그널 번호는 sig를 통해서 넘어온다. 시그널을 기다린다는 특징을 이용해서 쓰레드간 동기화를 위한 목적으로도 유용하게 사용할 수 있을 것이다.

두번째는 비동기적인 방식으로 코드 실행중에 시그널이 전달되면 인터럽트가 걸리고 시그널 핸들러가 수행되는 방식이다. 일반적인 시그널 사용방식과 동일하다.

1.2.3. 예제
sigwait(3)를 통해서 동기적으로 기다리는 것은 구현이 간단하므로 따로 다루지 않고 시그널 핸들러를 등록해서 비동기적으로 시그널을 기다리는 코드를 구현해 보도록 하겠다. 1.1.1절의 코드를 약간 수정했다.

예제 : thtoth_sig.c

                               

위의 코드의 경우 시그널을 받을 쓰레드를 명시해줄 수 있으므로 시그널 마스크등을 설치할 필요가 없다. SIGINT가 원하는 쓰레드로 정확하게 전달되는걸 확인할 수 있을 것이다.

1.2.4. 시그널을 이용한 쓰레드 작동 제어
쓰레드 프로그래밍을 하다보면 비동기 적으로 특정 쓰레드를 중단 시켜야 되는 경우가 발생한다. 물론 임의의 시점에서 중단된 쓰레드를 다시 작동하도록 만들어 주어야 할것이다.

다른 우회적인 몇가지 구현 방법이 있겠지만 비동기적인 처리를 위해서는 역시 시그널만한게 없는 것 같다.
                               


--------------------------------------------------------------------------------

1.3. 운영체제별 차이점
쓰레드의 작동방식은 운영체제별로 많은 차이를 보여줄 수 있으며, 차이점에 유의해서 프로그램을 작성해야 한다. 여기에서는 솔라리스와 리눅스를 비교해서 설명하도록 하겠다.

지금까지의 쓰레드와 시그널에 대해서 다루었던 내용은 솔라리스와 같이 하나의 프로세스에서 다중의 쓰레드를 관리하는 경우를 기준으로 했다. 그러나 리눅스의 경우 clone(2)를 통한 다중 프로세스형태로 쓰레드가 생성된다. 때문에 ps를 이용해서 확인할 경우 다중 쓰레드 프로세스임에도 불구하고 각각의 PID를 가지는 프로세스로 쓰레드가 생성되는걸 확인 할 수 있다.

이런 특징 때문에 리눅스 시스템에서 외부 프로세스에서 시그널을 특정 쓰레드로 보낼 경우에는 메인 쓰레드가 아닌 해당 쓰레드의 PID를 명시해 주어야 한다.




출처 : http://joinc.co.kr/modules.php?name=new ··· 3Dnested
"UNIX/Linux C" 카테고리의 다른 글
  • pthread API 레퍼런스 (3)2007/05/14
  • 안전한 프로그래밍 (0)2007/05/11
  • 쓰레드와 시그널 (0)2007/05/11
  • Zlib를 이용한 압축 프로그래밍 (0)2007/05/11
  • proc파일시스템 프로그래밍 (0)2007/05/11
2007/05/11 10:15 2007/05/11 10:15
Posted by webdizen
Tags Signal, Thread, 시그널, 쓰레드
No Trackback No Comment

Trackback URL : http://www.webdizen.net/blog/trackback/2925

Leave your greetings.

[로그인][오픈아이디란?]

Unix & Linux/Kernel2007/05/10 10:18

리눅스 커널의 이해(3): 리눅스 디바이스 작성시 동기화 문제

저자: 서민우
출처: Embedded World

[ 관련 기사 ]
♠ 리눅스 커널의 이해(1) : 커널의 일반적인 역할과 동작
♠ 리눅스 커널의 이해(2): 리눅스 커널의 동작

일반적으로 리눅스 디바이스 드라이버를 작성할 땐 여러 가지 동기화 문제를 고려해야 한다. 리눅스 디바이스 드라이버를 작성할 때 동기화 문제를 제대로 해결하지 않는다면 커널이 멈추는 등의 심각한 문제가 발생한다.

리눅스 디바이스 드라이버 내에서 동기화 문제가 발생하는 이유는 두 가지이다. 먼저 우리가 작성하는 디바이스 드라이버는 리눅스 커널의 주요한 여러 흐름(시스템 콜 영역, top half 영역, bottom half 영역) 속에서 동작한다. 다음은 nested interrupt나 process scheduling에 의해 리눅스 커널 내에서는 커널 영역간에 여러 가지 경쟁 상태가 발생할 수 있다.

따라서 우리는 리눅스 디바이스 드라이버를 작성할 때 발생할 수 있는 여러 가지 동기화 문제와 이에 대한 일반적인 해결책을 알아야 한다.

이번 기사에서는 이러한 동기화 문제와 이에 대한 해결책을 구체적으로 알아보기 전에 1) 동기화 문제란 무엇인지, 2) 디바이스 드라이버의 주요한 동작과 리눅스 커널의 흐름에서의 디바이스 드라이버의 위치, 3) nested interrupt와 process scheduling에 의한 리눅스 커널의 흐름을 구체적으로 알아보기로 한다.


동기화 문제

먼저 동기화 문제가 무엇인지 보기로 하자.

신호등이 있는 횡단보도를 생각해 보자. 보행자는 신호등에 빨간불이 들어와 있는 동안에는 횡단보도 한쪽 끝에 서 있다가 신호등에 녹색 불이 들어오면 횡단보도를 건넌다. 보행자가 횡단보도를 건너는 동안에 횡단보도를 지나려고 하는 차량은 일시 정지해 있어야 한다. 만약 보행자가 신호등의 녹색 불을 보고 횡단보도를 건너는 동안에 차량이 일시 정지해 있지 않고 횡단보도를 지나려고 할 경우 교통사고 등의 문제가 발생한다. 이러한 문제는 어느 순간에 횡단보도를 보행자와 차량이 동시에 이용하려고 하는 데서 발생한다. 즉, 보행자와 차량이 신호등에 맞추어 횡단보도를 순서대로 이용한다면 이러한 문제는 발생하지 않는다.

이처럼 동기화의 문제란 어떤 일의 순서를 지키지 않는 데서 발생하는 문제이다. 따라서 동기화란 어떤 일의 순서를 맞추는 일이다. 일반적으로 동기화의 문제는 공유영역(예를 들어, 횡단보도)을 중심으로 발생한다. 이러한 공유영역은 flag(예를 들어, 신호등)에 맞추어 순서대로 이용하여야 한다.

공유영역과 관련한 동기화의 문제는 쓰레드를 이용한 응용 프로그램, multi-tasking을 수행하는 커널 내부, 신호등을 제어하는 논리회로 등 여러 군데서 발생할 수 있다.

다음 예제를 통해 공유영역과 관련한 동기화의 문제가 어떻게 발생하는지 구체적으로 들여다 보자.

사용자 삽입 이미지



이 예제는 리눅스 쓰레드 프로그램이다. ①에서 pthread_create() 함수를 이용해 10개의 쓰레드를 생성하며, 각각의 쓰레드는 adder() 함수를 수행한다. adder() 함수에서 각각의 쓰레드는 global_counting 변수 값이 0x10000000보다 크거나 같을 때까지 변수 값을 증가시킨다. 여기서 global_counting 변수는 쓰레드 간에 공유하는 공유 변수이다. 즉, 공유 영역이다. adder() 함수 내에 있는 local_counting 변수는 각각의 쓰레드가 global_counting 변수 값을 얼마나 증가시켰는지를 보기 위한 변수이다. local_counting 변수 값은 adder() 함수에서 리턴 값으로 사용한다. 이 리턴 값을 main() 함수의 ②에서 pthread_join() 함수를 통해 전달 받은 후 main() 함수 내에 있는 sum_local_counting 변수에 더해준다. 여기서 pthread_join() 함수는 쓰레드가 종료되기를 기다리는 함수이다. main() 함수의 마지막 부분에서는 global_counting 변수 값과 sum_local_counting 변수 값을 출력해 준다.

참고로 pthread_create() 함수의 첫번째 인자는 변수의 주소 값이 넘어가지만, pthread_join() 함수의 첫번째 인자는 변수의 값이 넘어간다.

이 예제를 다음과 같이 컴파일 한다. 참고로 리눅스 상에서 쓰레드 프로그램을 컴파일 할 때는 posix thread 라이브러리를 써야 하며 따라서 컴파일 옵션에 –lpthread 가 들어가야 한다. 컴파일이 끝났으면 실행시켜 본다.


$ gcc race-condition.c -o race-condition -lpthread
$ ./race-condition
…
global counting: 0x10000000
sum of local counting: 0x5d9c9858
$ ./race-condition
…
global counting: 0x10000000
sum of local counting: 0x662979dc


두 번의 실행 결과 global_counting 변수 값은 각각 0x10000000이 나왔으나, sum_local_counting 변수 값은 각각 0x5d9c9858, 0x662979dc이 나왔다. 이 값은 몇 차례 반복해서 수행해도 같은 값이 거의 나오지 않는다. 이 두 변수의 값이 왜 다른지 [그림 1]을 보며 생각해 보자.

사용자 삽입 이미지

[그림 1] 공유영역에서의 쓰레드간 race condition


[그림 1]에서 timer interrupt에 의해 수행하는 부분은 hardware interrupt에 의해 시작하는 리눅스 커널의 일반적인 동작으로 <리눅스 커널의 이해 ②> 기사의 [그림 9]을 참조하기 바란다.

먼저 [그림 1]에서 다음과 같이 가정하자.

i) 굵은 선 부분은 adder() 함수의 ③ 부분을 나타낸다.
ii) T1과 T2는 ①에서 생성한 쓰레드 중 임의의 두 쓰레드이다.
iii) 쓰레드 T1의 A 지점은 adder() 함수의 A 지점이다.
iv) 쓰레드 T1이 A 지점을 수행할 때 tmp_counting 값은 0x10000이다.
v) 쓰레드 T1은 A 지점에서 할당 받은 time slice를 다 썼다.
vi) C 지점에서 스케쥴링시 쓰레드 T2가 선택된다.
vii) 쓰레드 T2는 E 지점에서 할당 받은 time slice를 다 썼다.
viii) 쓰레드 T2의 E 지점에서 F 지점까지 여러 번의 timer interrupt가 들어왔다.
ix) 쓰레드 T2는 F 지점에서 새로이 할당 받은 time slice를 다 썼다.
x) H 지점에서 스케쥴링시 쓰레드 T1이 다시 선택된다.

위 가정에서 viii)의 경우 쓰레드 T2의 E 지점에서 F 지점까지 timer interrupt가 여러 번 들어 오더라도 할당 받은 time slice가 남아 있으므로 중간에 스케쥴링을 수행하지 않으며, 따라서 또 다른 쓰레드를 수행하지는 않는다.

쓰레드 T1이 A 지점을 지나는 순간 global_counting 값은 가정 iv)에 의해 0x10000이다. A 지점에서 timer interrupt가 발생할 경우 가정 v)에 의해 B 부분에서 스케쥴링을 요청하고 C 부분에서 스케쥴링을 수행한다. 스케쥴링 결과 가정 vi)에 의해 쓰레드 T2가 선택되며, 따라서 C 부분에서 시작한 스케쥴링은 D 부분에서 끝난다. 즉, c 지점으로 들어가서 d 지점으로 나온다. 그러면 쓰레드 T2는 D 부분을 거쳐 E 지점으로 나와 첫 번째 을 수행한다. 이 때 쓰레드 T2의 tmp_counting 값도 0x10000이 된다. 이 후에 F 지점에 도착할 때까지 여러 번 을 수행한다. 편의상 여기서는 0x10000 번 수행한다고 가정한다. 그러면 F 지점 바로 전에 마지막으로 수행한 에서 global_counting 값은 0x20000이 된다. F 지점에서 timer interrupt가 발생할 경우 가정 ix)에 의해 G 부분에서 스케쥴링을 요청하고 H 부분에서 스케쥴링을 수행한다. 스케쥴링 결과 가정 x)에 의해 쓰레드 T1이 다시 선택된다. 따라서 H 부분에서 시작한 스케쥴링은 I 부분에서 끝난다. 그러면 쓰레드 T1은 I 부분을 거쳐 J 부분으로 나와 A 지점에서 잘린 의 나머지 부분을 수행한다. 그 결과 global_counting 값은 0x10001이 되며, 따라서 쓰레드 T2가 수행한 0x10000 번의 동작은 잃어버리게 된다.

각각의 쓰레드가 을 순서대로 접근을 했다면 이런 결과는 없었을 것이다. 즉, global_counting 값을 읽고 0x10000000보다 작을 경우 하나를 증가시키고 global_counting 값을 갱신하는 부분이 쓰레드 간에 겹치지 않았다면 중간값을 잃어버리는 일은 없었을 것이다.

일반적으로 각각의 흐름을 갖는 하나 이상의 루틴이 공유영역을 접근했을 때 동기화 문제가 발생한다. 동기화 문제는 공유영역을 순서대로 접근하면 해결된다.

이 예제에서도 하나 이상의 쓰레드가 공유영역을 접근함으로써 동기화 문제가 발생한다. 이 예제에서는 쓰레드 간에 ③ 부분과 ③ 부분, ③ 부분과 ④ 부분, ④ 부분과 ④ 부분이 겹치지 않고 순서대로 수행이 되어야 동기화 문제가 발생하지 않는다.

이 예제에서 발생한 동기화의 문제는 다음과 같이 세마포어를 이용해 문제를 해결할 수 있다. 세마포어에 대한 구체적인 설명과 사용법은 나중에 다루기로 한다. 여기서는 겹치면 안되는 부분의 처음과 마지막 부분을 세마포어로 보호해주면 된다 하는 정도로 알고 넘어가기로 한다. 다음 예제에서 음영이 들어간 부분이 추가된 부분이다. main() 함수내의 sem_init() 함수는 for 문 바로 앞에 추가한다.

사용자 삽입 이미지


여러 차례 실행하더라도 global_counting 변수 값과 sum_local_counting 변수 값이 똑같이 0x10000000이 나온다. 주의할 점은 수행시간이 많이 길어진다.

이상에서 우리는 쓰레드 프로그램에서의 동기화 문제와 그에 대한 해결책을 보았다. 이러한 동기화의 문제는 리눅스 커널에서도 발생할 수 있다. 우리가 작성하는 디바이스 드라이버는 리눅스 커널의 주요한 여러 흐름(시스템 콜 영역, top half 영역, bottom half 영역)의 부분으로 동작하며 따라서 디바이스 드라이버 내에서도 여러 가지 동기화 문제가 발생할 수 있다.


디바이스 드라이버의 주요한 동작과 리눅스 커널의 흐름에서의 디바이스 드라이버의 위치

다음은 디바이스 드라이버의 주요한 동작과 이러한 동작들이 커널의 어떤 흐름에서 이루어지는지 알아보자.

디바이스 드라이버의 주요한 동작은 크게 세가지로 나눌 수 있다.

첫번째는 [디바이스에 쓰기 동작]이다. [디바이스에 쓰기 동작]의 경우 시스템 콜을 통해서 디바이스에 쓰고자 하는 데이터를 쓴다. 이 동작을 통하여 하드 디스크나 네트워크 카드등에 데이터를 쓴다. [디바이스에 쓰기 동작]과 관련한 커널의 흐름은 다음과 같다.

* 시스템 콜 루틴 내부:
디바이스가 멈추어 있을 경우 데이터를 디바이스 버퍼에 쓰고 나간다
디바이스가 동작중일 경우 데이터를 데이터 큐에 넣고 나간다

* 하드웨어:
디바이스가 데이터를 다 보냈다 -> hardware interrupt 발생

* top half 루틴 내부:
bottom half 요청

* bottom half 루틴 내부:
데이터 큐가 비어 있으면 그냥 나간다
데이터 큐가 비어 있지 않으면 데이터를 하나 꺼내서 디바이스 버퍼에 쓰고 나간다

두 번째는 <동기적으로 디바이스로부터 읽기 동작>이다. <동기적으로 디바이스로부터 읽기 동작>은 시스템 콜을 통해서 디바이스에 읽기를 요청한다. 디바이스에 읽기를 요청하면 어느 정도 시간이 흐른 후에 디바이스 내부 버퍼에 데이터가 도착하며 디바이스는 하드웨어 인터럽트를 이용하여 CPU에게 데이터의 도착을 알린다. 그러면 CPU는 인터럽트 핸들러를 통하여 이 데이터를 읽어간다. 하드 디스크나 CDROM으로부터 데이터를 읽어가는 동작이 이에 해당한다. <동기적으로 디바이스로부터 읽기 동작>과 관련한 커널의 흐름은 다음과 같다.

* 시스템 콜 루틴 내부:
디바이스가 멈추어 있을 경우 디바이스에 데이터 읽기를 요청하고 디바이스로부터 데이터 큐에 데이터가 도착하기를 기다린다
디바이스가 동작중일 경우 디바이스의 사용이 끝나기를 기다린다 (임의의 다른 프로세스가 디바이스를 사용 중이므로)

데이터 큐에서 데이터를 꺼낸다
디바이스의 사용이 끝났음을 알린다

* 하드웨어:
디바이스에 데이터가 도착했다 -> hardware interrupt 발생

* top half 루틴 내부:
메모리 버퍼를 하나 할당해 디바이스 버퍼로부터 데이터를 읽어 들인 후 메모리 버퍼를 데이터 큐에 넣는다
bottom half 요청

* bottom half 루틴 내부:
디바이스로부터 데이터 큐에 데이터가 도착했음을 알린다

세 번째는 <비동기적으로 디바이스로부터 읽기 동작>이다. <비동기적으로 디바이스로부터 읽기 동작>은 시스템 콜을 통해서 디바이스로부터 도착한 데이터를 읽고자 한다. 이 경우 데이터는 비동기적으로 디바이스에 도착하며, 인터럽트를 통해 데이터의 도착을 CPU에게 알린다. 그러면 CPU는 인터럽트 핸들러를 통하여 이 데이터를 읽어간다. 네트워크 카드나 시리얼 디바이스에 도착한 데이터를 읽어가는 동작이 이에 해당한다. <비동기적으로 디바이스로부터 읽기 동작>과 관련한 커널의 흐름은 다음과 같다.

* 시스템 콜 루틴 내부:
데이터 큐에 데이터가 있으면 데이터를 가져간다
데이터 큐에 데이터가 없으면 디바이스로부터 데이터 큐에 데이터가 도착하기를 기다린다

* 하드웨어:
디바이스에 데이터가 도착했다 -> hardware interrupt 발생

* top half 루틴 내부:
메모리 버퍼를 하나 할당해 디바이스 버퍼로부터 데이터를 읽어 들인 후 메모리 버퍼를 데이터 큐에 넣는다
bottom half 요청

* bottom half 루틴 내부:
디바이스로부터 데이터 큐에 데이터가 도착했음을 알린다

이상 디바이스 드라이버의 주요한 동작과 리눅스 커널의 흐름에서의 디바이스 드라이버의 위치를 살펴 보았다. 지금까지 살펴본 디바이스 드라이버에 동기화 문제가 어떻게 발생할지 또 어떻게 해결해야 할 지에 대해서는 다음 기사에 자세히 다루기로 한다.


nested interrupt와 process scheduling에 의한 리눅스 커널의 흐름

사용자 삽입 이미지

[그림 2] 리눅스 커널의 기본적인 동작


[그림 2]는 각각 system call에 의한 리눅스 커널의 동작, hardware interrupt에 의한 리눅스 커널의 동작, nested interrupt에 의한 리눅스 커널의 동작을 나타낸다. 각 동작에 대한 구체적인 내용은 본지 8 월 호 <리눅스 커널의 이해 ②> 기사의 [그림 8], [그림 9], [그림 17]을 참조하기 바란다. 참고로 리눅스 커널 버전은 2.5 이후 버전이다.

[그림 3]은 리눅스 커널 내에서 프로세스 스케쥴링이 있을 수 있는 지점을 나타낸다.

먼저 프로세스 스케쥴링이 어떤 경우에 있을 수 있는지 보기로 하자.

⒜는 hardware interrupt가 발생했을 때 프로세스 스케쥴링을 수행하는 경우이다. 프로세스 스케쥴링을 기준으로 보았을 때 hardware interrupt는 크게 두 가지로 나눌 수 있는데, 첫 번째는 timer device로부터 온 경우이고, 두 번째는 timer device를 제외한 나머지 device(예를 들어 하드 디스크나 이더넷 카드)로부터 온 경우이다.

timer device로부터 interrupt가 들어왔을 때 프로세스 스케쥴링을 수행하는 경우는 두 가지로 나눌 수 있다. 먼저 timer interrupt의 interrupt handler(top half)에서 현재 프로세스의 time slice 값을 하나 감소시키고 그 결과값이 0일 때 스케쥴링을 요청한다. 다음은 timer interrupt의 bottom half에서는 여러 가지 시간과 관련한 일들을 처리하며, 이러한 일들 중에는 시간과 관련한 조건을 기다리던 프로세스를 wait queue에서 꺼내 run queue로 넣는 일도 있다. 이런 경우 wait queue에서 run queue로 들어간 프로세스가 현재 프로세스보다 우선순위가 클 경우 스케쥴링을 요청한다.

그 외의 device로부터 interrupt가 들어올 경우에는 top half 또는 bottom half에서 그 device와 관련한 어떤 조건을 기다리는(예를 들어 그 device로부터 데이터가 도착하기를 기다리는) 프로세스를 wait queue에서 꺼내 run queue로 넣는 일이 있는데, 이 때 wait queue에서 run queue로 들어간 프로세스의 우선순위가 현재 프로세스보다 우선순위가 클 경우 스케쥴링을 요청한다.

⒝는 시스템 콜 영역을 수행하는 도중에 현재 프로세스로부터 어떤 조건을 기다리던 프로세스를 wait queue에서 꺼내 run queue로 넣는 일이 있는데, 이런 경우 wait queue에서 run queue로 들어간 프로세스가 현재 프로세스보다 우선순위가 크면 스케쥴링을 요청하는 경우이다.

⒞는 시스템 콜 영역을 수행하는 도중에 현재 프로세스를 진행하기 위해 필요한 어떤 조건 을 만족하지 못해 현재 프로세스를 논리적으로 더 이상 진행하지 못할 경우, 현재 프로세스 를 wait queue로 넣고 프로세스 스케쥴링을 수행하는 경우이다. 여기서는 현재 프로세스를 wait queue로 넣음으로써 현재 프로세스를 blocking 시킨다.

여기서 주의할 점은 ⒞의 경우는 현재 프로세스를 wait queue로 넣지만, ⒜와 ⒝의 경우는 현재 프로세스가 run queue에 그대로 남아있다. ⒞와 같은 형태의 프로세스 스케쥴링을 Direct invocation이라 하고, ⒜, ⒝와 같은 형태의 프로세스 스케쥴링을 Lazy invocation이라 한다.

⒟는 시스템 콜 영역을 수행하는 도중에 nested interrupt가 들어 왔을 때 수행하는 프로세스 스케쥴링이며, 스케쥴링을 수행하는 조건은 ⒜의 경우와 같다.

⒠, ⒡는 현재 프로세스에게 도착한 시그널을 처리하는 도중에 nested interrupt가 들어 왔을 때 수행하는 프로세스 스케쥴링이며, 스케쥴링을 수행하는 조건은 ⒜의 경우와 같다.

사용자 삽입 이미지

[그림 3] 리눅스 커널에서 프로세스 스케쥴링의 시작과 끝


[그림 3]을 통해 리눅스 커널 내에서 프로세스 스케쥴링이 어디서 시작해서 어디서 끝나는지 살펴 보자. 참고로 프로세스 스케쥴링에 대한 구체적인 내용은 본지 7 월호 <리눅스 커널의 이해 ①> 기사 내용을 참조하기 바란다.

어떤 프로세스의 a 지점에서 시작한 프로세스 스케쥴링은 임의의 다른 프로세스의 b, d, f, h, j, l 지점에서 끝날 수 있다. 마찬가지로 어떤 프로세스의 c, e, g, i, k 지점에서 시작한 프로세스 스케쥴링은 임의의 다른 프로세스의 b, d, f, h, j, l 지점에서 끝날 수 있다.

사용자 삽입 이미지

[그림 4] 프로세스 스케쥴링을 통한 프로세스간 전환


[그림 4]에서 ⒜와 ⒝는 각각 a 지점에서 시작한 프로세스 스케쥴링이 d 지점에서 끝나는 경우와, g 지점에서 시작한 프로세스 스케쥴링이 f 지점에서 끝나는 경우를 나타낸다. [그림 3]의 ⒜와 ⒝의 경우처럼 a, c, e, g, i, k 지점에서 시작한 프로세스 스케쥴링이 b, d, f, h, j, l 지점에서 끝나는 프로세스간 전환의 형태는 36 가지가 있을 수 있다.

[그림 4]의 ⒜와 ⒝를 통해서 우리는 프로세스의 흐름이 어떤 프로세스의 임의의 사용자 영역(프로세스 P1의 A 영역)에서 임의의 다른 프로세스의 임의의 사용자 영역(프로세스 P2 의 B 영역)으로 옮겨가는걸 볼 수 있다. 이와 같은 방식으로 프로세스의 흐름이 프로세스 P1의 사용자 영역에서 프로세스 P2의 사용자 영역으로, 또 프로세스 P2의 사용자 영역에서 프로세스 P3의 사용자 영역으로, …, 프로세스 Pn-1의 사용자 영역에서 프로세스 Pn의 사용자 영역으로 옮겨갈 수 있다. 즉, [그림 3]의 ⒜, ⒝와 같은 방식으로 프로세스의 흐름이 임의의 프로세스 P1의 사용자 영역에서 임의의 프로세스 Pn의 사용자 영역으로 옮겨갈 수 있다.

사용자 삽입 이미지

[그림 5] 프로세스 P1에서 프로세스 Pn으로의 전환


사용자 삽입 이미지

[그림 6] 프로세스 P1과 Pn의 같은 시스템 콜 영역의 접근




[그림 5]는 한 번 이상의 프로세스간 전환을 통해 임의의 프로세스 P1에서 임의의 프로세스 Pn으로 프로세스의 흐름이 옮겨갈 수 있음을 나타낸다.

[그림 6]은 임의의 프로세스 P1과 Pn이 각각 A와 B 영역에서 같은 시스템 콜 영역을 수행할 수 있음을 나타낸다. 우리가 작성하는 디바이스 드라이버의 일부는 시스템 콜 영역에서 동작을 하는데, 디바이스 드라이버를 작성할 때 동기화 문제를 고려하지 않을 경우 문제가 발생할 수 있다. [그림 6]은 [그림 5]의 한 예이다.

사용자 삽입 이미지

[그림 7] nested interrupt 와 process schedule에 의한 커널간 경쟁 상태



[그림 7]은 임의의 프로세스 P1이 시스템 콜 영역을 수행하는 도중에 nested interrupt가 발생하여 g 지점에서 프로세스 스케쥴링을 통해 임의의 프로세스 P2(여기서는 나타내지 않음)를 거쳐 임의의 프로세스 Pn으로 프로세스의 흐름이 옮겨가는 상황을 나타낸다. 이 경우 A와 B 영역이 같은 시스템 콜 영역이라 할 때 프로세스 P1와 프로세스 Pn은 시스템 콜 영 역에서 경쟁 상태가 될 수 있다. 이러한 경쟁 상태는 일반적으로 시스템에 논리적인 문제를 일으킨다.

[그림 6]과 [그림 7]에서 보듯이 nested interrupt와 process scheduling에 의해 리눅스 커널내에서는 커널 영역간에 여러 가지 경쟁 상태가 발생할 수 있으며, 이러한 경쟁 상태는 일반적으로 시스템을 멈추게 하는 등의 심각한 문제를 일으킨다.

앞에서도 말한 것처럼 우리가 작성하는 디바이스 드라이버는 시스템 콜 영역, top half 영역, bottom half 영역에서 모두 동작한다. 따라서 우리가 작성하는 디바이스 드라이버 내에서도 여러 가지 경쟁 상태가 발생할 수 있다.

이상에서 우리는 동기화 문제란 무엇인지, 디바이스 드라이버의 주요한 동작과 리눅스 커널의 흐름에서의 디바이스 드라이버의 위치, nested interrupt와 process scheduling에 의한 리눅스 커널의 흐름을 구체적으로 알아보았다.
다음 호에는 리눅스 디바이스 드라이버 작성시 Uni-Processor 또는 Multi-Processor 환경에 따라 발생할 수 있는 동기화 문제의 여러 가지 패턴을 살펴보고 그에 대한 해결책을 알아보기로 하자.


http://network.hanbitbook.co.kr/view.php?bi_id=1068
"Kernel" 카테고리의 다른 글
  • 리눅스 커널의 이해(5): 디바이스에 쓰기 동작에... (0)2007/05/10
  • 리눅스 커널의 이해(4): Uni-Processor & Multi-Pr... (0)2007/05/10
  • 리눅스 커널의 이해(3): 리눅스 디바이스 작성시... (0)2007/05/10
  • 리눅스 커널의 이해(2): 리눅스 커널의 동작 (0)2007/05/10
  • 리눅스 커널의 이해(1) : 커널의 일반적인 역할과... (0)2007/05/10
2007/05/10 10:18 2007/05/10 10:18
Posted by webdizen
Tags nested interrupt, process scheduling, race condition, Thread, 리눅스 커널
No Trackback No Comment

Trackback URL : http://www.webdizen.net/blog/trackback/2916

Leave your greetings.

[로그인][오픈아이디란?]

Programming/MFC2007/03/23 16:33

TLS(Thread Local Storage)

원본 : http://www.debuglab.com/knowledge/tls.html

1. 요약

TLS(Thread Local Storage)는 스레드 별로 고유한 저장공간을 가질 수 있는 방법입니다.


2. 본문

각각의 스레드는 고유한 스택을 갖기 때문에 스택 변수( 지역 변수)는 스레드 별로 고유합니다. 예를 들어서 각각의 스레드가 같은 함수를 실행한다고 해도 그 함수에서 정의된 지역변수는 실제로 서로 다른 메모리 공간에 위치한다는 의미입니다. 그러나 정적 변수나 전역 변수의 경우에는 프로세스 내의 모든 스레드에 의해서 공유됩니다. 이 역시 예를 들고 싶지만, 다 이해하셨으리라 믿고 생략하겠습니다.

그렇습니다. TLS는 정적, 전역 변수를 각각의 스레드에게 독립적으로 만들어 주고 싶을 때 사용하는 것입니다. 다시 말해서, 분명히 같은 문장(context)을 실행하고 있지만 실제로는 스레드 별로 다른 주소공간을 상대로 작업하는 것입니다.

TLS를 사용하는 방법은 2가지가 있습니다. 한 가지는 API에서 지원해주는 방식을 사용하는 것이고, 다른 하는 compiler에서 지원하는 방식을 사용하는 것입니다.

우선은 간단하면서도 신기한 두 번째 방법부터 살펴보겠습니다.

다음과 같이 선언된 전역변수가 있다고 가정해 봅시다.. ( 그럽시다..)

int nWindows;
이 변수는 모든 스레드에 의해서 공유됩니다.
그게 싫다면 다음과 같이 고쳐주시면 됩니다.
_declspec( thread ) int nWindows;
이제 이 변수는 모든 스레드에게 고유한(private) 변수가 되었습니다( 짝짝짝!!)

다음으로는 API를 사용한 방법을 알아봅시다.

DWORD dwIamIndex = ::TlsAlloc();  // 공간 확보 

::TlsSetValue( dwIamIndex, pMyData);  // 데이터 저장 

BYTE* pGiveMe = (BYTE*)::TlsGetValue( dwIamIndex);  // 데이터 얻기 

::TlsFree( dwIamIndex);  // 공간 해제 

자세한 부분은 역시 MSDN을 참조하셔야 겠지요..

한눈에 아실 수 있지만 storage-class modifier를 사용하는 방식이 훨씬 사용하기 쉽습니다.그러나 storage-class modifier를 사용하는 방식에는약간의 제약이 있습니다. 여기서 제약이란, 동적으로 로드되는 DLL에서는 사용될 수 없슴을 의미합니다. 아주 구체적인 속사정을 알 수 없지만, 어쨌든 전역변수를 위한 공간을 마련하는 데 많은 곤란이 있을 것 같지 않습니까? 동적으로 로드되는 DLL 에서는 API 타입을 사용하시는 수 밖에 없습니다.

마지막으로, 어떻게 스레드마다 고유한 공간을 유지할 수 있는 지 신기하지 않습니까?? 기회가 닿으면 다음 시간에 알아보도록 하겠습니다.


3. 예제 코드



4. 참소

Win32 Network Programming – Addison Wesley

위의 내용중에 Dynamic Loaded DLL 에서 Static TLS technic 을 사용하면 안된다고 했는데 그에 대한 설명이 부족한것 같아서 몇자 적어봅니다.

우선 API를 사용하는 방식이 Dynamic 한 방법이고, __declspec(thread)를 사용하는 방법이 static 한 방법이라고 생각할 수 있습니다. 그러면 __declspec(thread)를 사용하여 선언한 변수는 어디에서 관리 되는 걸까요? 우선 static TLS 방식을 사용하면 컴파일러는 자신만의 '.tls' 라는 메모리 section 에 선언된 변수를 등록시킵니다. 물론 이런 각각의 '.tls' section을 다루는 최상위 '.tls' section 이 따로 있습니다. 또한 하나의 프로그램이 또 다른 thread를 생성할 경우, 자신이 가지고 있는 '.tls' section 을 자식 thread 에 복사하는 방법을 사용하고 있습니다.바로 이점 때문에 문제가 발생한다고 보실 수 있습니다. 겉에서 보기에는 여러가지 thread 가 '.tls' section 을 공유하는것 처럼 보이지만 실제로는 내부에서 복사하는 방식을 사용하고 있기 때문이죠. 자.. 부가 설명은 여기까지 하고 제가 다루기로 했던 내용들을 간단한 예를 통해서 좀더 자세히 다루어 보기로 하겠습니다.

우선 A라는 프로그램이 있습니다. A 또한 자신만의 '.tls' section 을 가지고 있습니다. 그리고 A는 5개의 thread를 생성하였습니다. 그럼 A에서 사용하던 '.tls' section 이 모두 6개가 되었습니다. ( A + 5 thread ) 이러한 상황에서 A가 static TLS 를 가지고 있는 DLL 을 LoadLibrary()를 사용하여 Load 하였습니다. 그러면 A는 자신의 '.tls' section 을 늘려서 새로운 DLL 이 가지고 있는 static TLS 를 추가해주어야 합니다. 물론 A는 자신의 '.tls' section 뿐만 아니라 5개의 자식 thread 의 '.tls' section 까지 늘려주어야 합니다. 그리고 나서, 다시 A가 동적으로 load 했던 DLL을 FreeLibrary()를 통해서 메모리로부터 제거하였습니다. 그럼 A는 자신의 '.tls' section 에서 필요없는 DLL의 '.tls' section 을 제거 해야겠죠? section 영역도 줄여야 겠죠. 물론 이와 같은 작업을 5개의 자식 thread의 '.tls' section 에도 해주어야 합니다. 이와 같은 작업을 system 이 하게 되면, 여러가지 문제가 발생하게 됩니다.(초기화가 제대로 이루어 지지 않는 문제 혹은 access violation 등)

결국 이런 이유로 동적으로 DLL 을 Load 하기 위해서 만들어진 DLL은 static TLS 를 사용하는 대신 DLL 스스로가 자신의 TLS section 을 관리할 수 있는 Dynamic TLS 방식을 사용하는 것이 좋습니다.



- 2001.08.19 Smile Seo -
"MFC" 카테고리의 다른 글
  • 향상된 RichEdit control (RICHEDIT20A) (0)2007/03/23
  • OLE를 이용한 Drag and Drop (0)2007/03/23
  • TLS(Thread Local Storage) (0)2007/03/23
  • DECLARE_DYNCREATE()와 IMPLEMENT_DYNCREATE() (0)2007/03/22
  • WaitCursor가 만들어지지 않는 경우 (0)2007/03/22
2007/03/23 16:33 2007/03/23 16:33
Posted by webdizen
Tags Thread, TLS
No Trackback No Comment

Trackback URL : http://www.webdizen.net/blog/trackback/2730

Leave your greetings.

[로그인][오픈아이디란?]

Programming/C#2007/02/05 17:52

C# 스레드 사용

고수닷넷 - 방랑자님


Greg Ewing
Clarity Consulting Inc.

요약: 이 기사에서는 스레딩의 다른 모델(단일, 아파트 및 자유)과 각 모델의 사용에 대해 설명합니다. 스레드를 이용하는 응용 프로그램을 작성하는 데 도움을 줄 수 있도록 스레드를 사용하는 C# 코드 샘플도 소개합니다. 또한 다중 스레딩 코드에 포함된 중요한 문제에 대해서도 설명합니다(9페이지/인쇄 페이지 기준).

목차

소개
스레딩에 대한 배경 지식
예제 응용 프로그램
다중 스레드 코드의 문제
결론

소개

다중 스레드 MSMQ(Microsoft Message Queuing) 트리거 응용 프로그램을 작성하는 일은 일반적으로 까다로운 작업이었습니다. 그러나 .NET Framework 스레딩 및 메시징 클래스의 도입으로 어느 때보다 쉬워졌습니다. 이 클래스를 사용하면 .NET Framework를 대상으로 하는 모든 언어로 다중 스레드 응용 프로그램을 작성할 수 있습니다. 이전에 Microsoft Visual Basic과 같은 도구는 스레딩에 대한 지원이 매우 제한되어 있었습니다. 따라서 C++을 사용하여 다중 스레드 코드를 작성하거나 Visual Basic에서 여러 프로세스나 ActiveX DLL로 구성되는 이상적이지 않은 솔루션을 작성하거나 또는 다중 스레딩을 완전히 무시하는 수 밖에 없었습니다. .NET Framework에서는 어떤 언어를 사용하는지에 관계없이 풍부한 다중 스레드 응용 프로그램을 작성할 수 있습니다.

이 기사에서는 Microsoft 메시지 대기열의 메시지를 수신하고 처리하는 다중 스레드 응용 프로그램을 작성하는 프로세스를 단계적으로 소개하며, 특히 System.Threading 및 System.Messaging이라는 두 가지 네임스페이스에 초점을 둡니다. 샘플 코드는 C#으로 작성되어 있지만 원하는 다른 언어로 쉽게 변환할 수 있습니다.

스레딩에 대한 배경 지식

Win32 환경에서 스레딩의 기본 모델은 단일, 아파트 및 자유 등 세 가지입니다.

단일 스레딩

처음에 작성한 응용 프로그램은 아마 응용 프로그램의 프로세스에 해당하는 스레드만 포함된 단일 스레드였을 것입니다. 프로세스는 해당 응용 프로그램의 메모리 공간을 차지하는 응용 프로그램의 인스턴스로 정의할 수 있습니다. 대부분의 Windows 응용 프로그램은 단일 스레드에서 모든 작업을 수행하는 단일 스레드 응용 프로그램입니다.

아파트 스레딩

아파트 스레딩은 단일 스레드보다 복잡한 스레딩 모델입니다. 아파트 스레딩으로 표시된 코드는 자체의 아파트로 제한된 고유 스레드에서 실행될 수 있습니다. 스레드는 처리 시간 동안 일어날 프로세스에서 소유하는 엔터티로 정의할 수 있습니다. 아파트 스레딩 모델에서 모든 스레드는 기본 응용 프로그램의 메모리에서 각각의 하위 섹션 내에서만 작동합니다. 이 모델에서는 코드의 여러 인스턴스를 동시에 그리고 독립적으로 실행할 수 있습니다. 예를 들어 .NET 이전의 Visual Basic에서는 아파트 스레드 구성 요소와 응용 프로그램을 만드는 것으로 제한되어 있었습니다.

자유 스레딩

자유 스레딩은 가장 복잡한 스레딩 모델입니다. 자유 스레딩 모델에서는 동시에 여러 스레드가 같은 메서드와 구성 요소로 호출됩니다. 아파트 스레딩과 달리 자유 스레딩은 분리된 메모리 공간에 제한되지 않습니다. 예를 들어 응용 프로그램에서 매우 비슷하지만 독립적인 수학 계산을 대량으로 실행해야 하는 경우에 자유 스레드 개체를 사용할 수 있습니다. 이 경우 같은 코드 인스턴스를 사용하여 계산을 실행하는 여러 스레드를 만듭니다. Visual Basic 6.0과 같은 언어에서는 이와 같은 작업이 거의 불가능하므로 자유 스레드 응용 프로그램을 작성한 경험이 있는 응용 프로그램 개발자는 아마 C++ 개발자뿐일 것입니다.

스레딩 모델 작업

스레딩 모델에 대한 개념의 이해를 돕는 예로 한 집에서 다른 집으로 이사하는 일을 들 수 있습니다. 단일 스레드 방법은 포장에서 상자 운반과 짐 풀기까지의 모든 일을 직접하는 것이라고 볼 수 있습니다. 아파트 스레딩 모델로 작업하는 경우는 절친한 친구 몇에게 도움을 청하는 것과 같습니다. 각 친구는 각기 다른 방에서 일하고 다른 방에서 일하는 사람을 도울 수 없습니다. 그들은 각자의 공간과 이삿짐을 맡습니다. 자유 스레드 방법을 선택하는 경우, 친구들에게 도움을 요청하는 것은 아파트 스레딩 모델과 동일하지만 친구들이 모두 어느 시간이나 어느 방에서든 함께 이삿짐을 꾸릴 수 있다는 점이 다릅니다. 이 비유에서 집은 모든 스레드가 작동하는 프로세스이고 각 친구는 코드의 인스턴스이며 이삿짐은 응용 프로그램의 리소스와 변수입니다.

위의 예에서는 각 모델의 장점과 단점을 보여 줍니다. 아파트 스레딩은 구성 요소의 여러 인스턴스가 작동하므로 단일 스레딩보다 빠릅니다. 자유 스레딩에서는 모든 일이 동시에 일어나고 모든 리소스가 공유되므로 어떤 경우에는 아파트 스레딩보다 빠르고 훨씬 효율적입니다. 그러나 여러 스레드에서 공유 리소스를 변경하는 경우 문제가 일어날 수 있습니다. 한 사람이 상자를 사용해 부엌 물건을 싼 다음 다른 친구가 와서 같은 상자에 침실 물건을 포장하는 경우를 생각해 보십시오. 첫 번째 친구는 상자에 '부엌'이라는 레이블을 붙였는데 뒷 친구가 그 위에 '침실'이라는 레이블을 붙입니다. 결국 짐을 풀 때는 부엌 물건을 침실에서 풀게 될 것입니다.

예제 응용 프로그램

첫 단계로 예제 응용 프로그램의 디자인을 검토합니다. 이 응용 프로그램에서는 여러 스레드를 만들고 각 스레드는 MSMQ 대기열의 메시지를 수신합니다. 이 예제에서는 기본 Form 클래스와 사용자 지정 MQListen 클래스의 두 가지 클래스를 사용합니다. Form 클래스는 사용자 인터페이스를 처리하는 것과 아울러 작업자 스레드를 만들고 관리하고 소멸시킵니다. MQListen 클래스에는 메시지 대기열 항목을 비롯해 작업자 스레드를 실행하는 데 필요한 모든 코드가 포함됩니다.

응용 프로그램 준비

  • 응용 프로그램을 시작하려면 Visual Studio .NET을 열고 MultiThreadedMQListener라는 C# Windows 응용 프로그램을 새로 만듭니다. 폼의 속성을 열고 이름을 QueueListenerForm으로 지정합니다. 초기 폼이 그려지면 그 위에 레이블 두 개, 단추 두 개, 상태 표시줄 한 개 및 텍스트 상자 두 개를 끌어 놓습니다. 첫 번째 텍스트 상자는 Server로, 두 번째는 Queue로 이름을 지정합니다. 첫 번째 단추는 StartListening로, 두 번째는 StopListening으로 이름을 지정합니다. 상태 표시줄은 기본 이름인 statusBar1로 그대로 두어도 됩니다.
  • 그런 다음 프로젝트 메뉴에서 참조 추가를 클릭하여 System.Messaging 네임스페이스에 대한 참조를 추가합니다. .NET 구성 요소 목록에서 System.Messaging.Dll을 찾아 선택합니다. 이 네임스페이스에는 MSMQ 대기열과 통신하는 데 사용되는 클래스가 포함됩니다.
  • 그런 다음 파일 메뉴에서 새 항목 추가를 클릭하여 프로젝트에 새 클래스를 추가합니다. Class 템플릿을 선택하고 이름을 MQListen으로 지정합니다. 클래스 맨 위에 다음과 같은 using 문을 추가합니다.
    // C#
    using System.Threading;
    using System.Messaging;

    System.Threading 네임스페이스를 사용하여 필요한 모든 스레딩 기능(이 경우, Thread 클래스와 ThreadInterruptException 생성자)에 액세스할 수 있습니다. 이 네임스페이스에서 사용할 수 있는 다른 많은 추가 기능이 있으나, 이 기사에서는 이에 대해 다루지 않습니다. System.Messaging 네임스페이스를 사용하면 대기열의 메시지 보내기 및 받기를 포함한 MSMQ 기능에 액세스할 수 있습니다. 이 예제에서는 MessageQueue 클래스를 사용하여 메시지를 수신합니다. 또한 기본 폼 코드에 using System.Threading도 추가합니다.

모든 참조가 준비가 되면 코드 작성을 시작할 수 있습니다.

작업자 스레드

첫 단계로 모든 스레드 작업을 캡슐화하는 MQListen 클래스를 작성합니다. 다음 코드를 MQListen대해서도 삽입합니다.

// C#
public class MQListen
{   
   private string m_MachineName;
   private string m_QueueName;
      
   // 생성자는 필요한 대기열 정보를 적용합니다.
   public MQListen(string MachineName, string QueueName)
   {
      m_MachineName = MachineName;
      m_QueueName = QueueName;
   }
    

   // 각 스레드에서 MQ 메시지를 수신하는 데 사용하는 유일한 메서드입니다. 
   public void Listen()
   {
      // MessageQueue 개체를 만듭니다.    
System.Messaging.MessageQueue MQ = new System.Messaging.MessageQueue(); // MessageQueue 개체의 경로 속성을 설정합니다.
MQ.Path = m_MachineName + "\\private$\\" + m_QueueName; // Message 개체를 만듭니다. System.Messaging.Message Message = new System.Messaging.Message(); // 인터럽트가 수신될 때까지 반복합니다. while (true) { try { // 인터럽트가 throw된 경우 catch하기 위해 대기합니다. System.Threading.Thread.Sleep(100); // Message 개체를 receive 함수의 결과와 동일하게 설정합니다. // Timespan(일, 시간, 분, 초).
Message = MQ.Receive(new TimeSpan(0, 0, 0, 1)); // 받은 메시지의 레이블을 표시합니다. System.Windows.Forms.MessageBox.Show(" Label: " + Message.Label); } catch (ThreadInterruptedException e) { // 기본 스레드에서 ThreadInterrupt를 catch하고 끝냅니다.
Console.WriteLine("Exiting Thread"); Message.Dispose(); MQ.Dispose(); break; } catch (Exception GenericException) { // receive에서 throw된 예외를 catch합니다.
Console.WriteLine(GenericException.Message); } } } }

코드 설명

MQListen 클래스에는 생성자 이외에 함수가 하나 있습니다. 이 함수는 각 작업 스레드가 실행하는 모든 작업을 캡슐화합니다. 스레드가 시작될 때 이 함수가 실행될 수 있도록 기본 스레드에서 이 함수에 대한 참조를 스레드 생성자로

전달합니다. Listen에서는 우선 메시지 대기열 개체를 설정합니다. MessageQueue 생성자는 세 가지 구현으로 오버로드됩니다. 첫 번째 구현 작업은 수신할 대기열의 위치를 지정하는 문자열 인수와 대기열을 처음으로 액세스하는 응용 프로그램에 대기열에 대한 단독 읽기 권한을 부여해야 하는지 여부를 지정하는 부울 등 두 가지 인수를 취합니다. 두 번째 구현은 대기열 경로 인수만을 취하고 세 번째 구현은 아무 인수도 취하지 않습니다. 간단히 하기 위해 세 번째 구현을 사용하여 다음 줄에 경로를 할당할 수 있습니다.

대기열에 대한 참조를 마쳤으면 메시지 개체를 만들어야 합니다. 메시지 생성자도 세 가지 구현을 가집니다. 처음 두 구현은 대기열에 메시지를 쓸 경우에 사용할 수 있습니다. 이 구현에서는 메시지 본문에 들어갈 개체와 메시지 본문으로 개체가 serialize되는 방법을 정의하는 IMessageFormatter 개체를 취합니다. 여기서는 대기열에서 읽는 중이므로 빈 메시지 개체를 초기화합니다.

개체를 초기화한 후 모든 작업을 수행할 기본 루프를 입력합니다. 나중에 기본 스레드에서 이 스레드를 중지시키기 위해 Interrupt를 호출하면 스레드가 대기, 중지 또는 조인 상태에 있는 경우에만 중단됩니다. 세 가지 상태 중 하나에 있지 않은 경우 다음에 이런 상태로 들어가기 전까지는 중단되지 않습니다. 작업자 스레드가 대기, 중지 또는 조인 상태를 입력하도록 하려면 System.Threading 네임스페이스에 있는 Sleep 메서드를 호출합니다. Sleep 메서드는 이전에 Windows API sleep 함수를 사용한 경험이 있는 C++ 개발자와 Visual Basic 개발자에게는 아주 익숙할 것입니다. 이 메서드는 스레드가 대기하는 시간(밀리초)이라는 단일 인수를 취합니다. Sleep 메서드를 호출하지 않으면 작업자는 인터럽트 요청을 받을 수 있는 상태를 입력할 수 없고 프로세스를 수동으로 종료하기 전까지는 무기한 계속됩니다.

MQ Receive 메서드의 구현은 두 가지입니다. 첫 번째 구현은 아무것도 취하지 않으며 메시지를 수신할 때까지 무한히 대기합니다. 이 예제에서 사용되는 두 번째 구현에서는 TimeSpan 개체로 제한 시간을 지정합니다. TimeSpan 생성자에는 일, 시간, 분, 초 등 네 가지 인수가 있습니다. 이 예제에서는 Receive 메서드가 1초 동안 대기한 후 시간이 초과되어 반환됩니다.

메시지가 수신되면 메시지는 앞에서 만든 메시지 개체에 할당되어 처리에 사용될 수 있습니다. 이 예제에서는 해당 레이블이 있는 메시지 상자를 열고 메시지를 삭제합니다. 이 코드를 실제 사용을 위해 변경하려면 여기에 메시지 처리 코드를 넣습니다.

작업자 스레드에서 Interrupt 요청을 수신하면 ThreadInterruptedException 예외를 throw합니다. 해당 예외를 catch하려면 Sleep 및 Receive 함수를 try-catch 블록에 래핑합니다. 두 가지 catch를 지정해야 합니다. 첫째는 인터럽트 예외를 catch하고 둘째는 catch되는 오류 예외를 처리합니다. 인터럽트 예외가 catch되면 우선 스레드가 끝나는 디버그 창에 씁니다. 그런 다음 대기열 개체와 메시지 개체에서 Dispose 메서드를 호출하여 모든 메모리를 정리하고 가비지 수집기로 보냅니다. 마지막으로 while 루프를 빠져 나갑니다.

함수에서 while 루프를 끝내자마자 관련 스레드는 코드 0으로 종료합니다. 디버그 창에 'The thread '<name>' (0x660) has exited with code 0 (0x0)'와 같은 메시지가 나타납니다. 스레드는 이제 컨텍스트를 벗어났으며 자동으로 소멸됩니다. 기본 스레드와 작업 스레드 중 어디에서도 정리하기 위해 별도로 해야 할 일은 없습니다.

기본 폼

다음 단계에서는 폼에 코드를 추가하여 작업자 스레드를 만들고 각 스레드에서 MQListen 클래스를 시작합니다. 우선 폼에 다음과 같은 함수를 추가합니다.

// C#
private void StartThreads()
{
   int LoopCounter; // 스레드 수
   StopListeningFlag = false; // 작업자가 중지될지 여부를
                               // 추적하는 플래그입니다.

   	// 작업자 스레드가 될 5개의 스레드 배열을 선언합니다.    
Thread[] ThreadArray = new Thread[5]; // 작업자 스레드의 모든 코드를 포함하는 클래스를 선언합니다.
MQListen objMQListen = new MQListen(this.ServerName.Text,this.QueueName.Text); for (LoopCounter = 0; LoopCounter < NUMBER_THREADS; LoopCounter++) { // Thread 개체를 만듭니다.
ThreadArray[LoopCounter] = new Thread(new ThreadStart(objMQListen.Listen)); // 스레드를 시작하면 ThreadStart 대리자를 호출합니다.
ThreadArray[LoopCounter].Start(); } statusBar1.Text = LoopCounter.ToString() + " listener threads started"; while (!StopListeningFlag) { // 사용자가 중지 단추를 누를 때까지 대기합니다. // 대기하는 동안 시스템에서는 다른 이벤트를 처리할 수 있습니다.
System.Windows.Forms.Application.DoEvents(); } statusBar1.Text = "Stop request received, stopping threads"; // 인터럽트 요청을 각 스레드에 보냅니다. for (LoopCounter = 0;LoopCounter < NUMBER_THREADS; LoopCounter++) { ThreadArray[LoopCounter].Interrupt(); } statusBar1.Text = "All Threads have been stopped"; }

코드 설명

이 함수에서는 먼저 5개의 항목으로 구성된 스레드 배열을 만듭니다. 이 배열은 나중에 사용할 수 있도록 모든 스레드 개체에 대한 참조를 보관합니다.

MQListen 클래스의 생성자는 메시지 대기열을 호스팅하는 컴퓨터 이름과 수신할 대기열의 이름 등 두 가지 인수를 취합니다. 이 생성자는 텍스트 상자의 값을 사용하여 이 인수를 할당합니다.

스레드를 만들려면 각 스레드 개체를 초기화할 루프를 입력합니다. 스레드의 Start 메서드가 호출될 때 호출될 함수를 가리키는 대리자를 Thread 생성자에 전달해야 합니다. MQListen.Listen 함수를 사용하여 스레드를 시작할 수 있지만 이 함수는 대리자가 아닙니다. 스레드 생성자의 요구 사항을 충족하려면 ThreadStart 개체를 전달하여 지정된 함수 이름으로 대리자를 만들어야 합니다. 여기서는 MQListen.Listen 함수에 대한 참조를 ThreadStart 개체에 전달합니다. 이 배열 요소가 초기화되었으므로 곧바로 Start 메서드를 호출하여 스레드를 시작합니다.

스레드가 모두 시작되면 폼의 상태 표시줄을 적절한 메시지로 업데이트합니다. 스레드가 실행되면서 대기열을 수신하고 있는 동안 기본 스레드는 사용자가 응용 프로그램에 수신을 중단하도록 요청할 때까지 대기합니다. 이렇게 하려면 사용자가 StopListening 단추를 클릭할 때까지 while 루프를 입력하여 StopListeningFlag값을 변경합니다. 이 대기 루프에서 응용 프로그램은 Forms.Application.DoEvents 메서드를 사용하여 필요한 다른 모든 처리를 수행할 수 있습니다. 이것은 Visual Basic에 익숙한 사람에게는 이전의 DoEvents 메서드와 같고, C++에 익숙한 사람에게는 MSG 펌프를 작성하는 것과 동일합니다.

StopListening 단추를 클릭하면 이 루프가 끝나고 스레드 종료 코드로 진입합니다. 스레드를 모두 종료하기 위해 코드에서는 스레드 배열 내에서 루프하여 각 스레드에 인터럽트 신호를 보냅니다. 이 루프 내에서 배열의 각 스레드 개체에 대해 Interrupt 메서드를 호출합니다. 이 메서드가 호출되기 전까지는 MQListen 클래스의 코드는 정상적으로 실행됩니다. 이 때문에 다른 것을 처리하는 도중에라도 각 작업자에 대해 Interrupt를 호출할 수 있습니다. 스레드 클래스는 스레드가 완료되면 각 스레드의 정리를 처리합니다. 마지막 작업으로 기본 폼의 상태 표시줄을 업데이트하고 종료합니다.

이제 단추 뒤에 코드를 추가해야 합니다. 다음과 같은 코드를 StartListening 단추의 Click 이벤트에 추가합니다.

// C#
statusBar1.Text = "Starting Threads";
StartThreads();

이 코드는 상태 표시줄을 업데이트하고 StartThreads 메서드를 호출합니다. 이 코드에서 StopListening 단추의 StopListeningFlag를 True로 설정하기만 하면 됩니다.

// C#
StopListeningFlag = true;

마지막 단계로 StopListeningFlag에 대해 폼 수준 변수를 추가합니다. 폼 코드 맨 위에 다음 줄을 추가합니다.

// C#
private bool StopListeningFlag = false;

응용 프로그램을 테스트하려면 메시지 대기열에 쓰기 위한 예제 응용 프로그램인 MQWrite를 다운로드합니다.

다중 스레드 코드의 문제

샘플 코드 작업을 완료했으므로, 이제 다중 스레드 응용 프로그램을 작성하는 데 필요한 도구를 가지게 되었습니다. 스레딩은 응용 프로그램의 성능과 확장성을 극적으로 개선할 수 있습니다. 이런 강력함과 더불어 스레딩의 위험에 대해서도 알고 있어야 합니다. 어떤 경우에는 스레드 사용으로 응용 프로그램에 해를 끼칠 수 있습니다. 스레드로 인해 작업이 다운되거나 예상치 못한 결과가 일어날 수 있으며 심지어 응용 프로그램이 응답을 중지할 수도 있습니다.

스레드가 여러 개인 경우 스레드 서로가 특정 지점에 도달하거나 종료할 때까지 대기하고 있지 않은지 확인합니다. 제대로 수행하지 않으면 각 스레드가 서로 대기하고 있으므로 아무 스레드도 종료되지 않는 교착 상태를 초래할 수 있습니다.

여러 스레드에서 쉽게 공유될 수 없는 리소스(예: 플로피 디스크 드라이브, 직렬 포트 또는 적외선 포트)에 액세스해야 하는 경우 스레드 사용을 방지하거나 synclock 또는 mutex와 같은 고급 스레딩 도구를 사용하여 동시성을 관리할 수 있습니다. 두 스레드가 이 리소스 중 하나를 동시에 액세스하려 하면 한 스레드에서는 리소스를 얻을 수 없고 데이터 손상이 일어납니다.

스레딩의 또 다른 일반적인 문제는 경쟁 조건입니다. 한 스레드가 파일에 데이터를 쓰는 동안 다른 스레드에서 해당 파일을 읽고 있는 경우 어떤 스레드가 먼저 종료할지 알 수 없습니다. 이 문제는 두 스레드가 파일 끝으로 경주하고 있기 때문에 경쟁 조건이라고 합니다. 읽는 스레드가 쓰는 스레드보다 앞서는 경우 알 수 없는 결과가 반환됩니다.

스레드를 사용할 때 모든 스레드가 다른 스레드와 별도로 완전히 작업을 마칠 수 있을지 여부도 생각해 보아야 합니다. 데이터를 앞뒤로 전달해야 하는 경우 데이터가 비교적 간단해야 합니다. 복잡한 개체를 전달하는 경우 이 개체를 앞뒤로 이동하기 위해 막대한 마샬링 비용이 들어가기 시작합니다. 이에 따라 운영 체제에서 관리하는 데 오버헤드가 생기고 전체 성능이 떨어집니다.

또 다른 문제로는 코드를 다른 개발자에게 넘겨주는 데 따른 전환 비용이 있습니다. .NET으로 스레딩이 훨씬 쉬워졌지만 코드를 유지 관리하는 다음 개발자가 스레딩 작업을 하기 위해서는 스레딩에 대해 알고 있어야 합니다. 그러나 이 점은 스레드 사용을 피해야 할 이유라기 보다는 적절한 코드 주석을 제공해야 할 이유입니다.

이런 문제들로 인해 스레딩 자체를 사용하지 않는 것 보다는 응용 프로그램을 디자인하고 스레드를 사용할지 여부를 결정할 때 모든 문제들을 기억하고 있는 것이 좋습니다. 불행히도 이 백서에서는 이런 문제를 방지하기 위한 방법을 설명하지 않습니다. 스레드를 사용하기로 했으나 위에서 언급한 문제가 발생한 경우 synclock이나 mutex를 검토하여 문제를 해결하거나 다른 솔루션을 찾아 보십시오.

결론

여기에 있는 내용으로 스레딩을 이용하는 응용 프로그램을 작성할 수 있습니다. 그러나 이렇게 하는 동안 관련된 문제를 기억하십시오. 스레딩은 제대로 사용하면 응용 프로그램의 성능과 확장성을 단일 스레드 응용 프로그램에 비해 훨씬 개선할 수 있지만, 올바로 사용하지 못하는 경우 정반대의 결과를 가져올 수 있으며 응용 프로그램이 불안정하게 될 수 있습니다.


[이 자료는 MSDN 라이브러리에서 가져왔습니다.]

"C#" 카테고리의 다른 글
  • Event Handling in .NET Using C# (0)2007/06/26
  • C# 명령줄 컴파일러 옵션 (0)2007/05/03
  • C# 스레드 사용 (1)2007/02/05
  • Office 2003 : Visual Studio Tools for Office Sy... (0)2007/02/05
  • 웹 서비스의 세계로 - 구글 검색을 활용해보자. (0)2007/02/05
2007/02/05 17:52 2007/02/05 17:52
Posted by webdizen
Tags Thread
No Trackback 1 Comment

Trackback URL : http://www.webdizen.net/blog/trackback/2604

Leave your greetings.

  1. 니눅스

    오늘 스레딩 공부하다가 막히는 부분이 있었는데, 잘 정리된 글 잘 읽고 갑니다.

    2008/08/03 23:55 [ Permalink : Modify/Delete : Reply ]
[로그인][오픈아이디란?]

«Prev  1  Next»

RSS HanRSS
Blog Image
webdizen
이곳은 컴퓨터에 대해 연구하고, 공유하고, 소통하기 위한 연구실입니다. 개인적으로는 OLAP, Data Mining, Semantic Web, Data Modeling에 대해서 연구하고 있습니다.

Categories

전체 (3009)
Webdizen (141)
Life (6)
Diary (16)
Blog (9)
IDEA (2)
Travel (10)
Book (16)
Photo (7)
Movie (8)
Music (14)
Leisure Sports (10)
Funny (6)
Hardware (121)
Software (120)
Windows (5)
Unix & Linux (120)
Installation (5)
Kernel (10)
System (34)
Develop (22)
X-Window (0)
Applicaton (31)
Security (4)
Framework (2)
Hadoop (2)
Programming (804)
Algorithm & Data Structure (1)
Assembly (38)
UNIX/Linux C (95)
C++ (128)
STL (4)
Java (38)
Win32 API (92)
ATL/COM (44)
MFC (151)
.NET (26)
WCF/WPF (4)
C# (28)
Network Programming (17)
Database Programming (12)
OpenGL / DirectX (13)
Multimedia Programming (0)
Game Programming (21)
Parallel Distributed Progra... (0)
Reverse Engineering (0)
Debugging (9)
Python (1)
Ruby (1)
Ruby on Rails (1)
QT (4)
GTK (0)
JSP (0)
PHP (6)
ASP.NET (6)
ASP (2)
Development (28)
Useful Library (2)
Data Modeling (0)
Database (105)
Oracle (4)
MSSQL (41)
MySQL (2)
Data Warehouse (2)
Data Mining (4)
Network (66)
Web (79)
DHTML (4)
XHTML (1)
Javascript (1)
CSS (1)
AJAX (9)
XML (11)
Flex (1)
Silverlight (3)
Security (91)
DoS (1)
Kernel (10)
Scanning (3)
Sniffing (0)
Spoofing (4)
Overflow (28)
Web (11)
Shell (10)
Format String (14)
Window (2)
Embedded (70)
Multimedia (27)
Mobile (14)
Graphic (24)
Management (633)
Knowledge (581)
Hadoop (0)

Notice

  • 메타 블로그 사이트에 등록
  • 새해 맞이 블로그의 변화
  • 블로그 명칭 변경
  • 도메인(www.webdizen.net) 구...
  • TEXTCUBE 1.6.1로 업그레이드...

Tags

  • 네트워크 포트
  • 프레임 워크
  • .NET
  • 아키텍처
  • RC0
  • Icon
  • Access
  • 확장 update
  • 썬라이즈 슬로진
  • 속도향상
  • 짐빔 화이트
  • 온라인 복구
  • 적벽대전
  • SQL Server 2000
  • VB.NET
  • Worker
  • Weblog
  • 기타
  • Framework
  • nmap

Recent Articles

  • 트위터(Twitter)의 시작!.
  • 청년 리더의 조건.
  • 애플의 타블렛 PC - 아이패드....
  • 미래의 인터페이스 - 육감 기....
  • 기초발성법 동영상 강좌.

Recent Comments

  • 경청... 너무나 중요한데.......
    webdizen 14:59
  • 학교 과제물중 쓰레드에 대하....
    장진혁 03/17
  • 관리자만 볼 수 있는 댓글입....
    비밀방문자 03/12
  • 상대방의 이야기를 열심히 경....
    DoNuts 03/03
  • 좋은글 잘 보고 갑니다..
    Und_hacker 01/08

Recent Trackbacks

  • printf,scanf를 이용한 형식....
    yundream의 프로그래밍 이야기 03/10
  • 파일 열기/저장하기 CFileDialog.
    은마군의 나태블록 2009
  • World IT Show 2008.
    상우 :: Oranzie's BLOG 2008
  • cvs서버 설치하기.
    3인3색 2008
  • 속속 공개되는 Google Chart....
    PHP와 Web 2.0 2007

Archive

  • 2010/02 (1)
  • 2010/01 (6)
  • 2009/12 (5)
  • 2009/09 (3)
  • 2009/08 (1)

Calendar

«   2010/03   »
일 월 화 수 목 금 토
  1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30 31      

Bookmarks

    • Administration
      • IIS.NET
      • NTFAQ
      • OS의 모든 것
      • 리눅스포털
    • Database
      • SQL Server Central
      • SQL Team
    • Development
      • .NET Heaven
      • ASP Alliance
      • ASP.NET 2.0
      • Bullog.net
      • C# Corner
      • C++ (C PlusPlus.com)
      • C++ Reference
      • CodeGuru
      • CodePlex
      • DebugLab
      • Dev Articles
      • Devpia
      • DotNet Junkies
      • DotNet Zone
      • Driver Online
      • GOSU.NET
      • HOONS 닷넷
      • Joinc 팀블로그
      • KOSR
      • MSDN Home Page
      • OSR Online
      • Sky.ph - 개발자 커뮤니...
      • TAEYO.NET
      • The Code Project
      • WindowsClient.net
      • 김상욱의 개발자 Side
      • 조인시 위키
    • Human Networks
      • belief21c's e-space
      • I think I can
      • Invisible Rover's Blog :D
      • Polarux - Linuxing
      • Rodman®
      • 까만 나비
      • 나를 가꾸는 시간.
      • 단녕
      • 상우 :: Oranzie's BLOG
    • Information Technology
      • Microsoft TechNet
      • 지디넷코리아 - 글로벌...
    • Security
      • FoundStone
      • milw0rm
      • NewOrder
      • OpenRCE
      • Phrack.org
      • Reverse Engineering b1...
      • Reverse Engineering Team
      • RootKit
      • SecurityFocus
      • SecurityXploded by Nag...
      • Wow Hacker
      • Zone-H
Textcube
Louice Studio Inc.
Powered by Textcube. Original designed by Tistory.