멀티스레딩

멀티스레딩(multithreading)은 한 프로세스1)를 여러 수행 단위(스레드2))로 나누어 처리하는 것을 말한다.

멀티 태스킹과 유사한 점이 있다. 멀티태스킹은 한 PC에서 여러 작업(프로세스)를 동시에 수행하고, 멀티스레딩은 한 프로세스에서 여러 실행의 흐름(스레드)를 동시에 수행한다.

물론 CPU는 한번에 한 명령만 처리할 수 있으므로 동시라고 해도 한 작업을 일정부분 처리하고, 다른 작업을 일정부분 처리하고, 다시 처음 작업을 조금 처리하고… 이와 같은 과정을 반복하여 동시처럼 보이게 한다.

주 사용 이유는 다음과 같다.

  • CPU 이용률 최대화
    • CPU는 PC에서 가장 중요한(비싼) 자원이다. 이 자원을 쉴틈없이 활용하는 것은 굉장히 중요하다.
    • 그러나 CPU는 가장 빠르고, 다른 자원은 비교적 느리기 때문에 최대한 활용하는 것이 쉽지 않다.
  • 대기시간, 응답시간 최소화
    • 오랜 시간이 걸리는 작업을 수행하는 동안에도 사용자의 입력을 처리할 수 있다.
  • 실행의 흐름을 명확히 분리
    • 한 작업을 한 스레드로 만들어, 개념적으로 실행의 흐름을 분리할 수 있다.

이렇게 장점이 많지만 단점도 많다. 가장 어려운 점은 어렵다는 것. 멀티스레드 프로그래밍을 할 경우 다음 사항을 항상 주의하여야 한다.

  • race condition
  • 멀티 스레드 프로그램은 예측 불가능
  • 실행 순서가 보장되지 않음
  • 언제나, 어디서나 문맥 교환(context switching) 발생 가능
  • 스레드는 작은 변화에 매우 민감
  • 스레드는 즉시 시작하지 않을 수 있음

HOWTO: Visual C# .NET을 사용하여 다중 스레딩 환경에서 공유 리소스에 대한 액세스 동기화3)를 번역한 것이다. 상당히 의역했고, 주요 단어에 대해서는 각주를 달았다.

개요

멀티스레딩을 이용하면 여러 작업을 동시에 수행할 수 있다. 멀티스레딩은 프로그램의 성능과 응답성을 향상시킨다.

여러 스레드가 한 자원에 동시에 접근할 수 있기 때문에 이를 제어하여야 한다. 이 문서에서 그 방법에 대하여 설명한다.

공유 데이터를 보호하기

public 필드4)은 여러 스레드에서 접근할 수 있다. public 필드에 대한 접근을 동기화하기 위해서는 필드보다 속성5))를 이용하는 것이 좋다. 그리고 접근 제어를 위해 ReaderWriterLock 객체를 사용한다. 다음 코드에서 Number 프로퍼티의 구현을 잘 보자.

using System;
using System.Threading;

namespace MultiThreadApplication
{
	class Class1
	{
		private ReaderWriterLock rwl = new ReaderWriterLock();
		private long myNumber;
		public long Number   // the Number property
		{
			get
			{
				//Acquire a read lock on the resource.
				rwl.AcquireReaderLock(Timeout.Infinite);                
				try
				{
					Console.WriteLine("Thread:{0} starts getting the Number", Thread.CurrentThread.GetHashCode());
					Thread.Sleep(50);
					Console.WriteLine("Thread:{0} got the Number", Thread.CurrentThread.GetHashCode());

				}
				finally
				{
					//Release the lock.
					rwl.ReleaseReaderLock();
				}
				return myNumber;
			}
			set
			{
				//Acquire a write lock on the resource.
				rwl.AcquireWriterLock(Timeout.Infinite);
				try
				{
					Console.WriteLine("Thread: {0} start writing the Number", Thread.CurrentThread.GetHashCode());
					Thread.Sleep(50);
					myNumber = value;
					Console.WriteLine("Thread: {0} written the Number", Thread.CurrentThread.GetHashCode());
				}
				finally
				{
					//Release the lock.
					rwl.ReleaseWriterLock();
				}
			}
		}
		
		[STAThread]
		static void Main(string[] args)
		{
			Thread []threadArray = new Thread[20]; 
			int threadNum;


			Class1 Myclass = new Class1();
			ThreadStart myThreadStart = new ThreadStart(Myclass.AccessGlobalResource);

			//Create 20 threads.
			for( threadNum = 0; threadNum < 20; threadNum++)
			{
				threadArray[threadNum] = new Thread(myThreadStart);
			}

			//Start the threads.
			for( threadNum = 0; threadNum < 20; threadNum++)
			{   
				threadArray[threadNum].Start();
			}

			//Wait until all the thread spawn out finish.
			for( threadNum = 0; threadNum < 20; threadNum++)
				threadArray[threadNum].Join();

			Console.WriteLine("All operations have completed. Press enter to exit");
			Console.ReadLine();
		}

		public void AccessGlobalResource()
		{
			Random rnd = new Random();
			long theNumber;
			
			if (rnd.Next() % 2 != 0)
				theNumber = Number;
			else
			{
				theNumber = rnd.Next();
				Number = theNumber;
			}

		}
	}
}

클래스를 보호하기

멀티스레딩에서는 여러 스레드에서 한 객체에 동시에 접근하려고 할 수 있다. 둘 이상의 스레드가 한 객체에 접근하려고 할 때, 그 객체를 수정하는 스레드가 있다면, 다른 스레드는 그 객체의 잘못된 값을 가지게 될 수 있다.6)

이런 상황7)을 피하기 위해서는 lock을 사용해서 코드의 주요 부분8)을 보호할 수 있다. lock은 C#에서 lock이라는 키워드로 자체 지원한다. 이 키워드를 이용하면 한 코드 내에 동시에 한 스레드만 진입할 수 있다. 다음 코드에서 TeacherName이라는 프로퍼티를 자세히 보자.

using System;
using System.Threading;

namespace MultiThreadLockApplication
{
	class Student
	{
		private static string myTeacherName = "Bill";
		private string myName = "Grace";
		private static object somePrivateStaticObject = new Object();

		public static string TeacherName
		{
			get
			{
				string theName;

				// Synchronize access to the shared member.
				lock(somePrivateStaticObject)
				{
					Console.WriteLine("Thread {0} starts to get the teacher's name",Thread.CurrentThread.GetHashCode());
					theName = myTeacherName;

					// Wait for 0.3 second.
					Thread.Sleep(300);
					Console.WriteLine("Thread {0} finished to get the teacher's name:{1}.", Thread.CurrentThread.GetHashCode(), theName);
				}
				return theName;
			}

			set
			{
				lock(somePrivateStaticObject)
				{
					Console.WriteLine("Thread {0} starts to set the teacher's name.", Thread.CurrentThread.GetHashCode());
					myTeacherName = value;

					// Wait for 0.3 second.
					Thread.Sleep(300);
					Console.WriteLine("Thread {0} finished to set the teacher's name:{1}.", Thread.CurrentThread.GetHashCode(), value);
				}
			}
		}

		public string GetName()
		{
			string theName;
			lock(this)
			{
				Console.WriteLine("Thread {0} starts to get the student's name.", Thread.CurrentThread.GetHashCode());
				theName = myName;

				// Wait for 0.3 second.
				Thread.Sleep(300);
				Console.WriteLine("Thread {0} finished to get the student's name:{1}", Thread.CurrentThread.GetHashCode(), theName);
				return theName;
			}
		}

		public string SetName(string NewName)
		{
			string theOldName;
			lock(this)
			{
				Console.WriteLine("Thread {0} starts to set the student's name.", Thread.CurrentThread.GetHashCode());
				theOldName = myName;
				myName = NewName;

				// Wait for 0.3 second.
				Thread.Sleep(300);
				Console.WriteLine("Thread {0} finished to set the student's name:{1}", Thread.CurrentThread.GetHashCode(), NewName);
			}
			return theOldName;
		}
	}

	class Class1
	{
		public static int WorkItemNum = 20;
		public static AutoResetEvent Done = new AutoResetEvent(false);

		public static void AccessClassResource(object state)
		{
			Random rnd = new Random();
			string theName;
			Student AStudent = (Student) state;

			if( (rnd.Next() %2) != 0)
			{
				if( (rnd.Next() %2) != 0)
				{
					switch (rnd.Next() %3 )
					{
						case 0:
							Student.TeacherName = "Tom";
							break;
						case 1:
							Student.TeacherName = "Mike";
							break;
						case 2:
							Student.TeacherName = "John";
							break;
					}
				}
				else
				{
					theName = Student.TeacherName;
				}
			}
			else
			{
				if( (rnd.Next() %2) != 0)
				{
					switch (rnd.Next() %3 )
					{
						case 0:
							AStudent.SetName("Janet");
							break;
						case 1:
							AStudent.SetName("David");
							break;
						case 2:
							AStudent.SetName("Ben");
							break;
					}
				}
				else
				{
					theName = AStudent.GetName();
				}
			}

			if(Interlocked.Decrement( ref WorkItemNum) == 0)
			{
				Done.Set();
			}
		}

		[STAThread]
		static void Main(string[] args)
		{
			int threadNum;
			Student AStudent = new Student();

			// Queue up 20 work items in the ThreadPool.
			for (threadNum = 0 ; threadNum <= WorkItemNum -1 ; threadNum++) 
			{
				ThreadPool.QueueUserWorkItem(new WaitCallback(AccessClassResource),AStudent);
			}

			Done.WaitOne();
			Console.WriteLine("All operations have completed. Press enter to exit");
			Console.ReadLine();
		}
	}
}

~~LINKBACK~~


1)
실행중인 프로그램, 프로그램이 클래스라면 프로세스는 그 클래스의 객체이다. 자신만의 메모리 영역을 가진다.
2)
소프트웨어의 수행 경로, CPU 시간을 할당받는 수행의 단위. 작업을 실제로 수행하는 실행의 흐름
4)
필드: 클래스 또는 구조체의 직접 액세스할 수 있는 데이터 멤버
5)
프로퍼티(property
6) , 7)
race condition
8)
critical section
댓글을 입력하세요. 위키 문법이 허용됩니다:
J Y H W᠎ I