몬티홀 딜레마는 미국에서 약 40여 년간 방영한 미국의 TV쇼에서 유래한 확률 문제임.

사회자의 이름인 몬티 홀(Monty Hall)의 이름을 따서 몬티홀 딜레마 혹은 몬티홀 문제(Monty Hall Problem)이라고 불림.

 

A, B, C 세 개 문 중 하나의 문 뒤에 자동차가 있고, 나머지 문 뒤에는 염소가 있다.

참가자가 하나를 고르면, 정답을 아는 사회자가 나머지 두 개의 문 중 염소가 있는 문을 하나 연다.

이후 참가자에게 답을 바꿀 기회를 준다. 이때, 답을 바꾸는 게 좋을까 아니면 그대로 있는 게 좋을까?

 

얼핏 보면 최종적으로는 2개 중 하나이니까 50% 확률이라고 생각하기 쉽다.

하지만, 처음에 정답을 골랐을 확률은 1/3이고, 오답이 제거된 상황에서 처음 답을 유지하면 여전히 1/3 확률로 정답이지만, 선택을 바꿀 경우 1 - 1/3 = 2/3로 정답을 맞힐 확률이 올라간다.

 

좀 더 이해하기 쉽게 처음에 선택지가 10개가 있었다고 가정하자.

처음에 정답을 맞힐 확률은 10%밖에 안된다. 이후 사회자가 나머지 8개의 오답을 모두 제거하고 이제 딱 2개만 남았다고 생각해보자. 이런 경우엔 당연히 본인이 처음에 선택한 문보다는 사회자가 열지 않은 마지막 남은 문에 자연스럽게 눈길이 가게 될 것이다. 사회자가 남긴 문이 정답일 확률이 90%인 것이다.

 

이 문제를 코드로 작성해서 확인해 보자.

// Online C# Editor for free
// Write, Edit and Run your C# code using C# Online Compiler

using System;

public class HelloWorld
{
    static Random rand = new Random();
    
    static float TestMontyHall(int totalItemCount, bool changeFirstSelection, int testCount)
    {
        int successCount = 0;

        for (int i = 0; i < testCount; ++i)
        {
            int correctIndex = rand.Next(totalItemCount);
            int firstAnswerIndex = rand.Next(totalItemCount);
            
            // 처음 선택을 바꾼 경우
            if (changeFirstSelection)
            {
                // 첫번째 선택이 답이 아닌라면, 바꾼 후에 성공!
                if (correctIndex != firstAnswerIndex)
                    ++successCount;
            }
            else // 선택을 바꾸지 않은 경우
            {
                // 첫번째 선택이 정답인 경우에만, 성공
                if (correctIndex == firstAnswerIndex)
                    ++successCount;
            }
        }
        
        return (float)successCount / testCount;
    }
    
    public static void Main(string[] args)
    {
        Console.WriteLine ("Hello Monty Hall Problem World");
        
        Console.WriteLine ("If you change the first selection, success ratio is " + TestMontyHall(3, true, 10000));

        Console.WriteLine ("If you don't change the first selection, success ratio is " + TestMontyHall(3, false, 10000));
    }
}

결과는 아래와 같다.

Hello Monty Hall Problem World
If you change the first selection, success ratio is 0.664
If you don't change the first selection, success ratio is 0.3301

몬티홀 문제를 풀게 될 경우가 생긴다면, 처음 선택을 고집하지 말고 답을 바꾸자!

c# 프로젝트에서 코드 분석기를 돌렸더니, dispose를 두 번 하지 말라는 경고가 떴다.

그래서 알아 봤더니, using 구문안에 using을 또 사용할 때 나는 경고였다. 

using (Stream stream = new FileStream("file.txt", FileMode.OpenOrCreate))
{
    using (StreamWriter writer = new StreamWriter(stream))
    {
        // Use the writer object...
    }
}

이 이슈를 해결하려면 다음과 같이 처음 오브젝트는 using 대신 try catch로 예외 처리를 해 줘야 한다.

Stream stream = null;
try
{
    stream = new FileStream("file.txt", FileMode.OpenOrCreate);
    using (StreamWriter writer = new StreamWriter(stream))
    {
        stream = null;
        // Use the writer object...
    }
}
finally
{
    if(stream != null)
        stream.Dispose();
}

코드가 훨씬 더 복잡하고 귀찮아졌지만, 이게 맞는 방법이라고 한다.

 

출처 : docs.microsoft.com/ko-kr/previous-versions/visualstudio/visual-studio-2015/code-quality/ca2202-do-not-dispose-objects-multiple-times?view=vs-2015&redirectedfrom=MSDN

생산자 소비자 패턴은 생산자 스레드가 생산한 자료를 큐에 넣어 놓으면 소비자 스레드가 그걸 가져가서 처리하는 패턴을 말한다. 자료구조는 일반적인 큐와 lock을 함께 사용하거나 스레드에 안전한 ConcurrentQueue를 사용하면 되지 않을까 싶다. 단순하게 얘기하면 이렇게 간단하지만 조건에 따라서 굉장히 복잡해질 수 있다.

 

1. 생산할 데이터가 고정된 개수인가 무제한인가?

2. 큐의 사이즈는 고정인가 무제한인가?

3. 큐에 들어간 자료는 순서대로 처리되어야 하는가?

4. 생산자 스레드는 1개인가 여러 개인가?

5. 소비자 스레드는 1개인가 여러 개인가?

6. 스레드에서 데이터를 처리하는 시간은 얼마나 짧은가?

7. 생산보다 소비 속도가 빠른 경우, 큐에 데이터가 입력될 때까지 소비자 스레드가 대기하도록 해서 CPU를 낭비하지 않게 만들 수 있는가?

8. 생산자가 정상적으로 생산을 중단한 경우 소비자에게 알리는 방법?

 

.. 등등이 있다.

 

BlockingCollection을 이용하면 위와 같은 고민의 대부분을 해결할 수 있다. BlockingCollection 클래스는 내부적으로는 ConcurrentQueue를 사용하고 스레드가 동기화를 하기 편하게 만들어 놓은 클래스이다.

 

기본적인 함수는 다음과 같다.

// 생성자. 사이즈 제한을 걸 수 있는데, 최대 사이즈에 도달하면 생산자에 block이 걸린다.
public BlockingCollection();
public BlockingCollection(int boundedCapacity);

// 큐에 들어 있는 데이터 개수
public int Count { get; }

// 데이터 추가가 중단되었는가?
public bool IsAddingCompleted { get; }

// 데이터 추가가 중단되었고, 큐도 비었는가?
public bool IsCompleted { get; }

// 큐에 데이터 추가하기
// IsAddingCompleted 상태이면 예외가 발생한다
public void Add(T item);

// 데이터 추가 중단하도록 설정하기
// 이 함수 호출하면 이후 IsAddingCompleted가 true가 된다.
public void CompleteAdding();

// 큐에서 데이터 꺼내오기
// 큐에 데이터가 없으면, 계속 대기한다.
// IsCompleted 상태가 되면 예외가 발생한다.
public T Take();

// 큐에서 데이터 꺼내오기
// Take 함수와 기능이 동일하지만, cancellationToken가 설정되어 있으면 예외가 발생한다.
// 스레드 대기 중단을 위해선 이것을 사용해야 한다.
public T Take(CancellationToken cancellationToken);

생산자 스레드에서 Add 함수를 이용해서 큐에 데이터를 넣고, 소비자 스레드에서 Take함수를 이용해서 데이터를 꺼내와서 처리하면 된다.

 

큐의 사이즈에 제한이 걸려 있고 큐가 꽉 찬 경우엔 Add 함수에서 대기가 걸리고, 큐가 비어 있는 경우엔 Take에서 대기가 걸리기 때문에 CPU 낭비가 없다.

 

스레드를 중단하고 싶으면 CompleteAdding 함수만 한번 호출하면 된다. 이후에 Add와 Take에서 예외가 발생하기 때문에 예외만 잘 처리해 주면 된다.

 

소비자 스레드는 큐에 데이터가 남아 있으면 다 소비할 때까지 계속 도는데, CancellationToken을 이용해서 강제로 취소할 수도 있다.

 

 그외 TryAdd, TryGet 등 대기가 없는 함수들도 있는데, 앞서 6번에서 언급한 스레드에서 고속으로 데이터를 처리해야 하는 경우에 사용하면 된다. 넌블로킹 처리에 대해 자세한 내용을 알고 싶으면 다음의 링크를 참조하면 된다.

참고 링크 : docs.microsoft.com/ko-kr/dotnet/standard/collections/thread-safe/how-to-add-and-take-items (두번째 예제)

 

데이터의 처리 순서가 중요하지 않으면 AddAny, TakeAny 등의 함수를 사용하면 될 것으로 보이는데, 인자로 BlockingCollection 배열이 필요하다. 이것 또한 내용이 복잡해 보여서 이 글에서는 다루지 않겠다.

 

다음은 본인이 BlockingCollection을 사용한 생산자 소비자 패턴 예제이다. (c# 콘솔 프로젝트이고, 비주얼 스튜디오 2015로 작성하였다.)

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Threading;
using System.Collections;
using System.Collections.Concurrent;
using System.Diagnostics;

namespace CSBlockingCollection
{
	public class MyProduceConsumeQueue
	{
		// 추가/추출된 데이터 개수
		private int _addCount = 0;
		private int _takeCount = 0;

		// 블로킹 컬렉션 (내부적으론 ConcurrentQueue 사용)
		private BlockingCollection<int> _queue = new BlockingCollection<int>();

		// 최대 사이즈 제한을 걸수도 있다. 사이즈 제한이 있는 경우 큐가 가득 차면, 생산자가 대기해야 한다.
		//private BlockingCollection<int> _queue = new BlockingCollection<int>(10);

		// 추출 취소를 위한 토큰
		private CancellationTokenSource _source = new CancellationTokenSource();


		// 락을 위한 오브젝트
		public object LockObj = new object();

		// 큐 사이즈
		public int Count { get { return _queue.Count; } }

		// 추가된 데이터 개수
		public int AddedCount { get { return _addCount; } }

		// 추출한 데이터 개수
		public int TakenCount { get { return _takeCount; } }
		
		// 데이터 추가 중단
		public void CompleteAdding() { _queue.CompleteAdding(); }

		// 데이터 큐가 완전히 비었는지 검사
		public bool IsCompleted() { return _queue.IsCompleted; }

		// 데이터 추출 중단
		public void CancelTake() { _source.Cancel(); }


		// 데이터 추가
		public bool Add(int data)
		{
			try
			{
				_queue.Add(data);

				//++_addCount;
				Interlocked.Increment(ref _addCount);

				return true;
			}
			catch (Exception e)
			{
				// CompleteAdding를 호출하면 여기서 예외가 발생한다.
				Console.WriteLine(e.Message);
				return false;
			}
		}

		// 데이터 추출
		public bool Take(ref int data)
		{
			try
			{
				data = _queue.Take(_source.Token);

				//++_takeCount;
				Interlocked.Increment(ref _takeCount);

				return true;
			}
			catch (Exception e)
			{
				// CancelTake를 호출하면 여기서 예외가 발생한다.
				Console.WriteLine(e.Message);
				return false;
			}
		}

		// 큐 내용 출력하기 - 디버깅용
		public void PrintContents()
		{
			Console.Write($"[Queue] Add({AddedCount}), Take({TakenCount}), Count({_queue.Count}) => ");

			foreach (int item in _queue)
			{
				Console.Write("{0} ", item);
			}

			Console.WriteLine("");
		}
	}

	// 생산자 소비자 공통 부모용 클래스
	public abstract class ProducerConsumerBase
	{
		// 데이터 하나 처리에 필요한 최소, 최대 시간 시뮬레이션 값 (ms 단위)
		int _minProcessTime;
		int _maxProcessTime;

		// 랜덤
		protected Random _random = new Random();

		// 데이터 전달용 큐
		protected MyProduceConsumeQueue _queue;

		// 스레드 아이디
		public int ThreadId { get; private set; }

		// 데이터 처리된 개수
		public int ProcessedCount { get; set; }


		// 생성자
		public ProducerConsumerBase(MyProduceConsumeQueue q, int minProcessTime, int maxProcessTime)
		{
			_queue = q;

			_minProcessTime = minProcessTime;
			_maxProcessTime = maxProcessTime;
		}

		// 스레드 시작시 호출
		protected void OnThreadStart()
		{
			ThreadId = Thread.CurrentThread.ManagedThreadId;
		}

		// 스레드 잠시 대기 (데이터 처리 시뮬레이션용)
		protected void ThreadWait()
		{
			Thread.Sleep(_random.Next(_minProcessTime, _maxProcessTime));
		}

		// 스레드 함수
		public abstract void ThreadRun();
	}


	// 생산자
	public class Producer : ProducerConsumerBase
	{
		// 생성자
		public Producer(MyProduceConsumeQueue q, int minProcessTime, int maxProcessTime)
			: base(q, minProcessTime, maxProcessTime)
		{}

		// 스레드 함수
		public override void ThreadRun()
		{
			// 스레드 시작 처리
			OnThreadStart();

			//Stopwatch stopwatch = new Stopwatch();

			while(true)
			{
				// 랜덤 데이터 생성
				int data = _random.Next(0, 100);

				//stopwatch.Restart();

				// 데이터를 큐에 추가. 스레드에 안전하다.
				if (_queue.Add(data) == false)
					break;

				//stopwatch.Stop();

				// 대기시간이 있을 경우 표시
				//if (stopwatch.ElapsedMilliseconds != 0)
					//Console.WriteLine($"[{ThreadId:D2}] Produce Add Time : {stopwatch.ElapsedMilliseconds} ms");

				// 처리 카운트 증가
				++ProcessedCount;

				// 디버깅용 콘솔 출력 부분 (락을 걸어야 콘솔 출력이 깨지지 않는다.)
				// Add 와 별도로 락을 걸었기 때문에 출력되는 큐의 내용은 정확하지 않을 수 있다.
				lock (_queue.LockObj)
				{
					Console.Write($"[{ThreadId:D2}] Produce ({data:D2}) => ");
					_queue.PrintContents();
				}

				// 스레드 잠시 대기
				ThreadWait();
			}

			// 생산자 결과 출력
			Console.WriteLine($"[{ThreadId:D2}] Produced {ProcessedCount} items");
		}
	}

	// 소비자
	public class Consumer : ProducerConsumerBase
	{
		// 생성자
		public Consumer(MyProduceConsumeQueue q, int minProcessTime, int maxProcessTime)
			: base(q, minProcessTime, maxProcessTime)
		{ }

		// 스레드 함수
		public override void ThreadRun()
		{
			// 스레드 시작 처리
			OnThreadStart();

			while (true)
			{
				int data = 0;

				// 큐에서 데이터 하나 추출. 스레드에 안전하다.
				if (_queue.Take(ref data) == false)
					break;

				// 처리 카운트 증가
				++ProcessedCount;

				// 디버깅용 콘솔 출력 부분 (락을 걸어야 콘솔 출력이 깨지지 않는다.)
				// Take 와 별도로 락을 걸었기 때문에 출력되는 큐의 내용은 정확하지 않을 수 있다.
				lock (_queue.LockObj)
				{
					Console.Write($"[{ThreadId:D2}] Consume ({data:D2}) => ");
					_queue.PrintContents();
				}

				// 스레드 잠시 대기
				ThreadWait();
			}

			// 소비자 결과 출력
			Console.WriteLine($"[{ThreadId:D2}] Consumed {ProcessedCount} items", ProcessedCount);
		}
	}

	// 샘플 클래스
	public class ThreadSyncSample
	{
		static void Main()
		{
			Console.WriteLine("Configuring worker thread...");
			
			MyProduceConsumeQueue queue = new MyProduceConsumeQueue();

			// 생산자 정의
			Producer[] producerList = new[]
			{
				new Producer(queue, 100, 300),
				new Producer(queue, 200, 400),
			};

			// 소비자 정의 - 처리 속도를 느리게 하면 큐가 점점 쌓인다.
			Consumer[] consumerList = new[]
			{
				//new Consumer(queue, 100, 300),
				//new Consumer(queue, 100, 300),
				new Consumer(queue, 100, 300),				
				new Consumer(queue, 200, 600),
			};

			// 생산자 소비자 태스크 정의
			var producerTasks = new Task[producerList.Length];
			var consumerTasks = new Task[consumerList.Length];

			// 생산자 생성
			for (int i = 0; i < producerTasks.Length; ++i)
				producerTasks[i] = new Task(producerList[i].ThreadRun);

			// 소비자 생성
			for (int i = 0; i < consumerTasks.Length; ++i)
				consumerTasks[i] = new Task(consumerList[i].ThreadRun);


			Console.WriteLine("Launching producer and consumer threads...");
			
			// 생산자 태스크 실행
			Array.ForEach(producerTasks, t => t.Start());

			// 소비자 태스크 실행
			Array.ForEach(consumerTasks, t => t.Start());


			// ESC 키를 누르면 생산자를 중단한다는 안내
			Console.WriteLine("Press ESC to stop producers");

			// 키 입력이 있는가?
			while (true)
			{
				if (Console.KeyAvailable)
				{
					// 입력된 키가 ESC인가?
					var keyInfo = Console.ReadKey();
					if (keyInfo.Key == ConsoleKey.Escape)
					{
						// 생산자 스레드 중단 요청
						Console.WriteLine("Signaling producer threads to terminate...");
						queue.CompleteAdding();
						break;
					}
				}
			}

			// 생산자 스레드 종료 대기
			Task.WaitAll(producerTasks);


			// ESC 키를 누르면 소비자를 중단한다는 안내
			Console.WriteLine("Press ESC to stop consumers");

			// 큐가 빌 때까지 계속 대기
			while (queue.IsCompleted() == false)
			{
				if (Console.KeyAvailable)
				{
					// 입력된 키가 ESC인가?
					var keyInfo = Console.ReadKey();
					if (keyInfo.Key == ConsoleKey.Escape)
					{
						// 소비자 스레드 중단 요청
						Console.WriteLine("Signaling consumer threads to terminate...");
						queue.CancelTake();
						break; // while 빠져나가기
					}
				}
			}

			// 소비자 스레드 종료 대기
			Task.WaitAll(consumerTasks);

			Console.WriteLine("========================================");

			// 전체 생산량 계산
			int totalProduced = 0;
			foreach (var item in producerList)
			{
				Console.WriteLine($"[{item.ThreadId:D2}] Produced count : {item.ProcessedCount}");
				totalProduced += item.ProcessedCount;
			}

			// 전체 소비량 계산
			int totalConsumed = 0;
			foreach (var item in consumerList)
			{
				Console.WriteLine($"[{item.ThreadId:D2}] Consumed count : {item.ProcessedCount}");
				totalConsumed += item.ProcessedCount;
			}

			// 결과 출력
			Console.WriteLine($"Total Produced count : {totalProduced}");
			Console.WriteLine($"Total Consumed count : {totalConsumed}");
			Console.WriteLine($"Queue count : {queue.Count}");
			Console.WriteLine($"Queue add count : {queue.AddedCount}");
			Console.WriteLine($"Queue take count : {queue.TakenCount}");

			// 결과 검증 코드
			if (queue.AddedCount != totalProduced)
				Console.WriteLine($"ERROR : _queue.AddCount != totalProduced");

			if (queue.TakenCount != totalConsumed)
				Console.WriteLine($"ERROR : _queue.TakeCount != totalConsumed");

			if (queue.Count != (totalProduced - totalConsumed))
				Console.WriteLine($"ERROR : _queue.Count != (totalProduced - totalConsumed)");

			// 종료 대기
			Console.WriteLine("Press ENTER to exit.");
			Console.ReadLine();
		}
	}
}

예제에서는 생산자와 소비자 스레드가 각각 2개씩 돌아가도록 되어 있고, 각자의 처리 속도도 조절할 수 있게 했다. 스레드 개수도 조절이 가능하다.

 

일부러 소비자 속도를 좀 더 느리게 해서 시간이 지날수록 큐가 커지게 해 놓았는데, 큐에 남은 데이터를 끝까지 처리할 수 있는지 확인하기 위함이다. 소비자 스레드를 빠르게 설정할 수도 있는데, 그러면 소비자 스레드의 대기 시간이 길어진다.

 

예제 실행 화면은 다음과 같다.

 

ESC를 한번 누르면 생산자가 먼저 중단되고, 큐에 남은 데이터를 소비자가 마저 처리를 하는데, 이때 ESC를 한번 더 누르면 소비자도 강제 중단할 수 있다.

 

이 예제에서는 생산자를 먼저 중단하고 소비자를 나중에 종료하는 방식으로 처리했는데, 소비자를 먼저 중단하고 생산자를 중단하려면 Add 함수에도 CancellationToken이 필요할 수도 있다.

 

애플리케이션에서 메인 스레드를 멈추지 않고 뭔가 다른 일을 하고 싶다면 보통은 스레드를 만들어서 사용할 수밖에 없다. 그런데, 스레드를 만들어서 사용하려고 하다 보면 스레드 간 데이터 전달이나 스레드를 멈추는 방법, 스레드가 종료되었을 때 알아내는 방법이 의외로 복잡해서 쉽게 접근하기가 어려운 면이 있다. 그런데, 그런 스레드를 쉽게 사용할 수 있게 도와주는 클래스가 있는데, 그 이름은 바로 BackgroundWork이다. 

 

BackgroundWork 클래스를 사용할 때 스레드에서 호출할 함수만 지정하면 간단하게 사용이 가능하고,

작업의 진척도를 알아 내거나, 완료되는 시점도 알 수 있고, 작업 도중 취소도 가능하다.

 

다음은 WPF 예제 프로그램을 실행했을 때의 모습이다.

예제 툴 실행 모습

Start를 누르면 가상의 백그라운드 작업을 시작하고, 진행률은 상단 프로그레스 바와 우측의 TextBlock으로 알려준다.

작업이 중단되거나 종료되면 그 결과 또한 우측의 TextBlock에 메시지로 표시해 주는 간단한 예제이다.

 

Start 버튼 아래에 있는 라디오버튼은 테스트를 위한 실행 옵션인데, Normal을 선택하면 정상적으로 종료가 되고, Fail은 스레드 작업 도중 문제가 생겨 강제로 중단한 경우를 가정한 것이고, 마지막 Error는 예외(Exception)가 발생해서 스레드가 예상치 못하게 종료된 경우를 가정한 것이다.

작업 도중 진행율 표시. 작업 중에는 Start 버튼이 Cancel 버튼으로 바뀐다.
Cancel 버튼을 누르면 작업이 중단된다.
성공적으로 종료.
실패한 경우.
예외가 발생한 경우.

주의) 비주얼 스튜디오에서 디버그 모드 상태에서 Error 옵션으로 실행하면, 비주얼 스튜디오가 예외를 먼저 잡아서 일시 중단되지만, 계속 진행을 하면 위의 스샷과 같은 결과를 볼 수 있다. 릴리즈 모드에서는 바로 저렇게 표시된다.

 

다음은 위의 예제의 WPF UI 코드이다.

<Window x:Class="WpfBackgroundWorkApp.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfBackgroundWorkApp"
        mc:Ignorable="d"
        Title="WPF-BackgroundWorkApp" Height="350" Width="577">
    <Grid>
        <Button x:Name="button" Content="Start" HorizontalAlignment="Left" Margin="17,70,0,0" VerticalAlignment="Top" Width="75" Click="buttonStart_Click"/>
        <ProgressBar x:Name="progressBar" HorizontalAlignment="Left" Height="15" Margin="17,29,0,0" VerticalAlignment="Top" Width="264"/>
        <TextBlock x:Name="textBlock" HorizontalAlignment="Left" Margin="151,70,0,0" TextWrapping="Wrap" Text="Press Start" VerticalAlignment="Top" Width="408" Height="239"/>
        <RadioButton x:Name="radioButtonSuccess" GroupName="option" Content="Normal" IsChecked="True" HorizontalAlignment="Left" Margin="27,109,0,0" VerticalAlignment="Top" Checked="radioButtonSuccess_Checked"/>
        <RadioButton x:Name="radioButtonFail" GroupName="option" Content="Fail" HorizontalAlignment="Left" Margin="27,129,0,0" VerticalAlignment="Top" Checked="radioButtonFail_Checked"/>
        <RadioButton x:Name="radioButtonError" GroupName="option" Content="Error" HorizontalAlignment="Left" Margin="27,149,0,0" VerticalAlignment="Top" Checked="radioButtonError_Checked"/>
    </Grid>
</Window>

 

아래는 c# 소스코드이다.

using System;
using System.ComponentModel;
using System.Windows;

namespace WpfBackgroundWorkApp
{
	/// <summary>
	/// Interaction logic for MainWindow.xaml
	/// </summary>
	public partial class MainWindow : Window
	{
		enum RunOption { Normal, Fail, Error }

		RunOption _runOption = RunOption.Normal;

		BackgroundWorker _worker;

		public MainWindow()
		{
			InitializeComponent();

			UpdateControls();
		}

		private void UpdateControls()
		{
			if (_worker == null)
				button.Content = "Start";
			else
				button.Content = "Cancel";
		}

		private void buttonStart_Click(object sender, RoutedEventArgs e)
		{
			// 백그라운드 작업이 이미 있는 지 확인
			if (_worker == null)
			{
				progressBar.Value = 0;

				_worker = new BackgroundWorker();
				_worker.WorkerReportsProgress = true;
				_worker.WorkerSupportsCancellation = true;

				_worker.DoWork += DoWork;
				_worker.ProgressChanged += ProgressChanged;
				_worker.RunWorkerCompleted += RunWorkerCompleted;

				// 작업 시작
				_worker.RunWorkerAsync();
			}
			else
			{
				// 작업 취소
				_worker.CancelAsync();
			}

			UpdateControls();
		}
		
		// 백그라운드 스레드 작업
		public void DoWork(object sender, DoWorkEventArgs e)
		{
			BackgroundWorker worker = sender as BackgroundWorker;

			int count = 10;
			for (int i = 0; i < count; ++i)
			{
				// 작업이 취소 되었을 경우
				if ((worker.CancellationPending == true))
				{
					e.Cancel = true;
					break;
				}

				// 작업이 실패 했을 때
				if (_runOption == RunOption.Fail && (i == count / 2))
				{
					e.Result = "Failed";
					return;
				}

				// 작업 중 예기치 않은 오류가 발생했을 때
				if (_runOption == RunOption.Error && (i == count / 2))
				{
					throw new Exception("Custom Error");
				}

				System.Threading.Thread.Sleep(100);

				worker.ReportProgress((i + 1) * 100 / count);
			}
		}

		// 진행 상황 - UI 스레드
		public void ProgressChanged(object sender, ProgressChangedEventArgs e)
		{
			progressBar.Value = e.ProgressPercentage;
			textBlock.Text = e.ProgressPercentage + "%";
		}

		// 작업 종료 - UI 스레드
		private void RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
		{
			string message = "";

			if (e.Cancelled)
			{
				message = "Cancelled";
			}
			else if (e.Error != null)
			{
				message = "Error: " + e.Error.ToString();
			}
			else if (e.Result != null)
			{
				message = e.Result.ToString();
			}
			else
			{
				message = "Success";
			}

			textBlock.Text = message;

			_worker = null;

			UpdateControls();
		}

		// 실행 옵션
		private void radioButtonSuccess_Checked(object sender, RoutedEventArgs e)
		{
			_runOption = RunOption.Normal;
		}

		private void radioButtonFail_Checked(object sender, RoutedEventArgs e)
		{
			_runOption = RunOption.Fail;
		}

		private void radioButtonError_Checked(object sender, RoutedEventArgs e)
		{
			_runOption = RunOption.Error;
		}
	}
}

 

스레드에서의 취소 처리는 worker.CancellationPending 값을 검사해서 e.Cancel에 true를 직접 넣고 리턴해야 한다. 스레드가 강제로 종료되는 게 아니라는 점에 주의해야 한다.

 

이 예제에서는 RunOption.Error인 경우 임의로 예외를 만들어서 throw 하는 코드를 작성해서 넣었는데, 실제 어플을 작성할 때는 필요 없는 코드이다. 이 예제에서는 딱히 오류 날 부분이 없어서 임의로 추가한 것이다.

 

작업 도중 실패(fail)가 난 경우, 즉 외부에서 중단한 게 아니라 스레드 내부에서 작업 도중에 더 이상 진행이 불가능할 때도 e.Cancel에 값을 넣어서 중단해도 되지 않을까 싶지만, 해당 변수는 그런 용도로 만들어진 게 아니라서 권장하지 않는다고 한다. 정상적으로 종료하되 e.Result에 값을 넣는 게 좋다.

 

MSDN 예제를 보면 성공했을 때에도 e.Result에 무언가 값을 넣어서 종료 처리할 수도 있다. 하지만, 이 예제에서는 성공했을 때는 아무 값을 안 넣은 상태를 완전한 성공으로 간주했다. 이건 코드 작성자의 선택사항이다.

 

이 예제에서는 e.Result에 string을 넣었으나 다른 타입의 변수, 예를 들어 오류코드를 담을 수 있는 int 값이나, enum 등을 사용하는 것도 가능하다.

 

RunWorketCompleted 함수에서 e.Cancelled가 true인 상태에서는 e.Result 변수에 접근할 수 없다. 예외가 발생하니 주의가 필요하다.

 

이 예제에서는 백그라운드 작업의 진행 여부를 판단하기 위해 _worker 변수가 null 인지 아닌지를 검사하도록 했는데, 이것도 코드 작성자의 선택사항이다.

 

_worker 변수를 딱히 쓸 일이 없는 경우, 클래스 멤버 변수가 아니라 로컬변수로 정의해서 사용해도 사실 상관없다. 내부적으로 레퍼런스가 있기 때문에 스레드 작업이 완전히 종료될 때까지 가비지 컬렉터가 지우지 않는다.

 

BackgroundWorker 클래스는 나온지 오래되었기 때문에 사실 꽤 안정적이고 하위 호환성도 좋다.

 

이 예제의 소스코드는 WPF용으로 만들어졌지만, WinForm에서도 약간만 수정하면 그대로 사용이 가능하다.

 

ProgressChanged, RunWorkerCompleted 함수는 WinForm과 WPF에서는 UI 스레드이기 때문에 UI 컨트롤을 사용하는 데 지장이 없다.

 

하지만, 이 예제를 C# 콘솔버전에서 돌리면 Main 스레드가 아닌 다른 스레드에서 실행된다. 

콘솔 버전에서는 스레드 ID가 제각각이다.

즉, 콘솔용으로는 적합하지 않을 수 있다. 애초에 콘솔 어플에서 백그라운드 스레드를 돌릴 이유가 없어서 고려를 안 했다는 얘기도 있다. 이건 그냥 참고로 알고 있자.

C#은 가비지 컬렉터(GC)가 메모리를 자동으로 관리한다. 필요 없는 클래스의 인스턴스를 메모리에서 바로 지우는 게 아니라, 조건이 될 때까지 기다렸다가 지우기 때문에 클래스를 지웠다고 해도 그게 실제로 바로 삭제되는 것은 아니다. 일반적인 메모리라면 GC에 맡겨도 상관이 없지만, 관리되지 않는(Unmanaged, Native) 리소스는 즉각 해제해야 하는 경우가 생기는데, 그럴 때 필요한 것이 Dispose이다.

 

그래서, C++의 경우 소멸자에 각종 변수의 메모리를 해제하는 것으로 간단하게 구현이 될 만한 내용이지만, C#에서는 바로 삭제가 필요한 리소스를 해제하기 위해서 Dispose 함수가 필요하다.

 

소멸자와 Dispose의 차이 : stackoverflow.com/questions/339063/what-is-the-difference-between-using-idisposable-vs-a-destructor-in-c?newreg=eff4d2b40e844d5581272cc65983c418

 

What is the difference between using IDisposable vs a destructor in C#?

When would I implement IDispose on a class as opposed to a destructor? I read this article, but I'm still missing the point. My assumption is that if I implement IDispose on an object, I can

stackoverflow.com

그런데, Dispose를 호출하는 걸 깜박하면 어떻게 될까?

그럴 경우에는 GC에서 해당 인스턴스를 제거할 때, (소멸자에서) Dispose를 호출해준다.

큰 문제가 없으면 그냥 냅 둬도 상관없다는 얘기인데, 다만 다음과 같은 부작용이 생길 수 있다고 한다.

 

1.GC가 해당 인스턴스를 언제 지울 지 알 수 없다.

 즉, Dispose가 언제 호출될지 알 수 없다. 애플리케이션이 종료될 때까지 호출이 안될 수도 있다. 중요한 리소스를 사용하거나, 메모리를 많이 먹는 클래스라면 위험할 수도 있겠다.

 

2.GC가 해당 인스터스를 지울 때 Dispose를 호출하게 되면 그 순간 부하가 걸린다.

 즉, 랜덤하게 랙을 유발할 수도 있다. 성능에 예민한 앱이라면 주의가 필요하다.

 

3.GC가 해당 인스턴스를 지울 때 Dispose에서 예외가 발생하면 어플 자체가 크래시 할 수도 있다.

 수동으로 Dispose를 할 때에는 try/catch로 보호할 수 있지만, 자동제거일 경우 예외를 잡을 수 없기 때문에 매우 위험할 수도 있다.

 

참고 링크 : stackoverflow.com/questions/32397248/what-happens-if-i-dont-call-dispose

 

 

매번 Dispose를 호출하기 귀찮을 때는 using 블록을 사용하면 된다. using을 사용하면 자동으로 Dispose를 해준다.

using과 try/catch를 같이 사용해야 할 때에는 using을 try 안쪽에 넣으면 된다.

 

참고 링크 : theeye.pe.kr/archives/2736

 

Unity C# – IDisposable 알아보기 | 아이군의 블로그

이번에는 IDisposable 인터페이스에 대해서 알아보겠습니다. C#은 가비지콜랙터(Garbage Collector)를 가지고 있습니다. 이 GC는 기본적으로 관리되는 모든 객체들의 참조 링크를 관리하며 더이상 참조되

theeye.pe.kr

 

Dispose 기능이 필요한 커스텀 클래스를 만들어야 할 때에는 IDisposal을 상속 받아서 만들면 된다.

 

참고 링크  : medium.com/dotnetdev/idisposable-%ED%8C%A8%ED%84%B4%EC%9D%98-%EC%98%AC%EB%B0%94%EB%A5%B8-%EA%B5%AC%ED%98%84-%EB%B0%A9%EB%B2%95-4fa0fcf0e67a

 

IDisposable 패턴의 올바른 구현 방법

.NET Framework에서 새로운 클래스를 만들 때 여러가지 메모리 관리 디자인 패턴과 기법을 적용할 수 있지만, 한시적으로 사용해야 할 필요가 있는 자원들을 묶어서 관리할 때에는 IDisposable 패턴을

medium.com

참고 링크 2 : codeless.tistory.com/entry/C-Disposable-%ED%8C%A8%ED%84%B4

 

C# Disposable 패턴

public class MyResourceHog : IDisposable { private bool alreadyDisposed = false; public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool isDisposing)..

codeless.tistory.com

 

스킨은 UI 컨트롤의 색상과 이미지 정도만 바꾸는 것이고, 테마(Theme)은 스킨에 더해 컨트롤의 크기나 연출 방식 까지도 바뀌는 걸 의미한다. (더 큰 개념)

 

WPF 어플에서 테마나 스킨을 사용하려면 다음과 같은 것을 고려해야 한다.

 

1. 윈도우의 기본 테마 중 하나를 가져다 쓰기.

2. 프로그램 내부에 테마나 스킨을 미리 세팅하기 vs 외부 파일(*.xaml)로 저장해서 그 중 하나를 읽어 오기 (내장 혹은 외부 파일)

3. 프로그램 구동시 정해진 테마나 스킨 중 하나를 선택하기(static) vs 프로그램 구동 중에 바꿀 수 있게 하기(dynamic)

 

등 여러가기 경우가 있기 때문에 이 중 어떤 방식을 사용할 것인가에 따라 구현 방식도 달라진다.

자세한 내용은 아래의 링크를 참고하자.

 

michaelscodingspot.com/wpf-complete-guide-themes-skins/

 

WPF complete guide to Themes and Skins - Michael's Coding Spot

In WPF we define a UI resource system (mostly in XAML resource dictionaries) which includes colors, templates, icons, styles and so on. Sometimes, we need the ability to change our application’s look and feel dynamically, allowing different Themes or Sk

michaelscodingspot.com

위의 블로그를 가보면 내부분의 내용은 이미지나 글로 설명이 되어 있고, 실제 구동하는 소스는 아래쪽에 링크가 첨부 되어 있다.

 

윈도우 7 내장 테마를 적용하는 방법

stackoverflow.com/questions/2075720/windows-7-theme-for-wpf

 

Windows 7 theme for WPF?

Is there any way to make a WPF app look like it's running on Windows 7 even if it's running on XP? I'm looking for some kind of theme I can just paste in. I'm aware of the themes project on Codeplex (

stackoverflow.com

 

코드 내부에 정의된 스킨 사용 예제

www.codeproject.com/Articles/19782/Creating-a-Skinned-User-Interface-in-WPF

 

Creating a Skinned User Interface in WPF

Reviews the basics of creating a WPF user interface with various visual styles.

www.codeproject.com

 

윈도우 테마와 내부 테마 파일로 정의된 테마까지 다 사용하는 테마 스킨 엔진 예제

www.codeproject.com/Articles/22575/WPF-Themes-and-Skins-Engine

 

WPF Themes and Skins Engine

In this article, I will talk about different techniques to load WPF themes and skins. I will also provide a helper class for loading and unloading themes.

www.codeproject.com

 

+ Recent posts