본문으로 건너뛰기

C#에서 Thread와 Parallel.ForEach 안전하게 중단하는 방법

· 약 2분
Jeongyong Park
쌍팔년생 개발자

GUI 환경에서 버튼을 클릭하는 등의 이벤트로 시간이 오래 걸리는 작업을 구동하는 경우 스레드를 분리하여 개발하는 방법이 좋다는 방법은 C# 뿐만 아니라 안드로이드나 MFC 등 GUI를 어느 정도 개발한 사람이라면 익숙하리라 생각한다.

그렇다면 그 시간이 오래 걸리는 작업을 더욱더 빠르게 하고 싶다면 병렬 처리가 가장 쉽고 빨리 적용할 수 있는 합리적인 방법이라고 생각한다.

TL;DR: Thread.Abort()는 위험하므로 사용하지 마세요.

이 글에서는 Thread 내부의 Parallel.ForEach 작업을 안전하게 중단하는 방법과 현대적인 비동기 패턴을 소개합니다.

Thread.Abort() 사용 금지

중요: 이 글의 원본 코드는 2017년에 작성되었으며, Thread.Abort() 사용을 포함하고 있습니다.

이 방법은 현재 권장되지 않으며, .NET Core/.NET 5+ 에서는 지원되지 않습니다.

구글링을 통하여 어찌어찌 병렬 For 문을 적용하였지만, 문제는 "클라이언트가 구동은 했지만 오래 걸리는 프로세스를 작업 도중에 중단하고 싶다"라고 말했을 때 발생하였다.

다음은 몇 가지 코드를 조합하여 만든 메인 스레드와 분리된 다중 포문 작업 클래스와 그 작업을 중단시키는 예를 콘솔 응용프로그램으로 구성하였다. 1

메인 클래스 및 중단 클래스

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

namespace ThreadTest
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("press any key for Thread start!");
Console.ReadKey();

RulyCanceler canceler = new RulyCanceler();
TestFilter tf = new TestFilter(10000000);

Thread t = new Thread(() =>
{
try
{
tf.execute(canceler);
}catch(OperationCanceledException)
{
Console.WriteLine("Canceled!");
}
});
t.Start();
Console.ReadKey();
Console.WriteLine("Abort!");

canceler.Cancel();
t.Abort();
t.Join();

Console.WriteLine("Aborted!");
Console.ReadKey();

}
}
public class RulyCanceler
{
object _cancelLocker = new object();
bool _cancelRequest;
public bool IsCancellationRequested
{
get { lock (_cancelLocker) return _cancelRequest; }
}

public void Cancel() { lock (_cancelLocker) _cancelRequest = true; }

public void ThrowIfCancellationRequested()
{
if (IsCancellationRequested) throw new OperationCanceledException();
}
}

}

시간이 오래걸리는 작업 클래스

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

namespace ThreadTest
{
public class TestFilter
{
private long featureCount;
private int MULTI_THREAD_COUNT = 10;

EventWaitHandle waitEvent = new EventWaitHandle(true, EventResetMode.AutoReset, "SHARED_BY_ALL_PROCESSES");
public TestFilter(long featurecount)
{
this.featureCount = featurecount;
}

public void execute(RulyCanceler cancer)
{
int workingRangeSize = 1;
if (featureCount > 100)
{
workingRangeSize = (int)(featureCount / MULTI_THREAD_COUNT);
}
var part = Partitioner.Create(0, featureCount, workingRangeSize);
Parallel.ForEach(part, (num, state) =>
{
for (long featureIdx = num.Item1, cnt = num.Item2; featureIdx < cnt; featureIdx++)
{
try
{

//waitEvent.WaitOne();(파일 쓰기모드일 경우 하나의 스레드만 쓸수 있도록 줄세우기)
//waitEvent.Set();

// 중단 요청이있으면 Throw
cancer.ThrowIfCancellationRequested();

// 실제 일
Console.WriteLine(String.Format("{0} - {1}", num.Item1, featureIdx));
}
catch (OperationCanceledException ex)
{
Console.WriteLine("Breaked.");
Console.WriteLine("Clean");
state.Break();
break;

}
}
});
}
}

}

Footnotes

  1. http://www.albahari.com/threading/part3.aspx

📢 AdSense 광고 영역로딩 중...

💬 댓글 시스템