Microsoft MVP성태의 닷넷 이야기
.NET Framework: 674. Thread 타입의 Suspend/Resume/Join 사용 관련 예외 처리 [링크 복사], [링크+제목 복사],
조회: 23670
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
 

Thread 타입의 Suspend/Resume/Join 사용 관련 예외 처리

참고로 Suspend와 Resume 메서드는 몇 년째 obsolete로 표시되어 권장하지 않는 메서드입니다. 그래도 사용해야 한다면 이 글을 한번쯤 읽어보시고 사용하시길 바랍니다. ^^

우선, 다음과 같이 간단하게 예제를 만들어,

static void Main(string[] args)
{
    while (true)
    {
        Thread t = new Thread(threadFunc);
        t.Start(i);
        t.Suspend();
        t.Resume();
        t.Join();
    }
}

private static void threadFunc(object obj)
{
    Console.WriteLine("TEST: " + obj);
}

실행해 보면 다음과 같은 2개의 에러를 (상황에 따라) 만날 수 있습니다.

Unhandled Exception: System.Threading.ThreadStateException: Thread is not running; it cannot be suspended.
   at System.Threading.Thread.SuspendInternal()
   at System.Threading.Thread.Suspend()
   at ConsoleApp1.Program.Main(String[] args) 

Unhandled Exception: System.Threading.ThreadStateException: Thread is not running; it cannot be resumed.
   at System.Threading.Thread.ResumeInternal()
   at System.Threading.Thread.Resume()
   at ConsoleApp1.Program.Main(String[] args)

즉, Thread가 수행중이지 않았을 때 Suspend/Resume 메서드 호출을 하면 모두 예외가 발생하는 것입니다. 근데, 위의 에러 상황이 Suspend는 그렇다 치고 Resume의 경우에 발생하는 것은 이해가 안될 수도 있습니다. Suspend 한 시점에는 적어도 스레드가 실행 중이었으니 예외가 안 났다는 것이고 당연히 Suspend 호출로 인해 실행 중이었던 스레드는 중지했을 것입니다. 그런데 왜 Resume을 했는데 스레드가 실행 중이지 않고 있다면서 예외가 발생하는 것일까요?

그 이유는, Suspend 메서드는 대상 스레드에 중지하라는 신호만 날리는 비동기 식 호출이기 때문입니다. CLR은 대상 스레드가 중지해도 되는 상황이면 바로 중지를 하지만 그렇지 못한 상황이면 SuspendRequested로 접수를 해둡니다. 따라서 t.Suspend 메서드 호출이 완료되었어도 대상 스레드는 그 순간에 중지못했을 수도 있으니 위와 같은 상황에서는 스레드 종료까지 이어졌을 수도 있습니다. 그리곤 Resume을 호출하니 예외가 발생한 것이고.




또 다른 경우도 있습니다. 테스트하다 보면 다음의 라인에서 수행이 정지하는 경우가 있습니다.

static void Main(string[] args)
{
    while (true)
    {
        Thread t = new Thread(threadFunc);
        t.Start(i);
        Console.WriteLine(t.IsAlive);
        t.Suspend();
        Console.WriteLine(t.IsAlive); // 실행 정지
        t.Resume();
        t.Join();
    }
}

private static void threadFunc(object obj)
{
    Console.WriteLine("TEST: " + obj);
}

// 화면 출력 상태
True
TEST: 1

얼핏 보면 t 스레드의 IsAlive 속성을 얻어오지 못하고 blocking 상태에 빠진 듯 한데요. 실제 이유는 콜스택을 보면 알 수 있습니다.

// Hang 상태에 빠진 Main 스레드 콜스택

 	mscorlib.dll!System.IO.TextWriter.SyncTextWriter.WriteLine(bool value)  Unknown No symbols loaded.
    mscorlib.dll!System.Console.WriteLine(bool value)   Unknown No symbols loaded.
>    ConsoleApp1.exe!ConsoleApp1.Program.Main(string[] args) Line 22 C#  Symbols loaded.

// Suspend된 t1 스레드의 콜스택
    [Managed to Native Transition]      Annotated Frame
    mscorlib.dll!System.IO.__ConsoleStream.WriteFileNative(Microsoft.Win32.SafeHandles.SafeFileHandle hFile, byte[] bytes, int offset, int count, bool useFileAPIs) Unknown No symbols loaded.
    mscorlib.dll!System.IO.__ConsoleStream.Write(byte[] buffer, int offset, int count)  Unknown No symbols loaded.
    mscorlib.dll!System.IO.StreamWriter.Flush(bool flushStream, bool flushEncoder)  Unknown No symbols loaded.
    mscorlib.dll!System.IO.StreamWriter.Write(char[] buffer, int index, int count)  Unknown No symbols loaded.
    mscorlib.dll!System.IO.TextWriter.WriteLine(string value)   Unknown No symbols loaded.
    mscorlib.dll!System.IO.TextWriter.SyncTextWriter.WriteLine(string value)    Unknown No symbols loaded.
    mscorlib.dll!System.Console.WriteLine(string value) Unknown No symbols loaded.
>    ConsoleApp1.exe!ConsoleApp1.Program.threadFunc(object obj) Line 30  C#  Symbols loaded.
    mscorlib.dll!System.Threading.ThreadHelper.ThreadStart_Context(object state)    Unknown No symbols loaded.
    mscorlib.dll!System.Threading.ExecutionContext.RunInternal(System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, object state, bool preserveSyncCtx)   Unknown No symbols loaded.
    mscorlib.dll!System.Threading.ExecutionContext.Run(System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, object state, bool preserveSyncCtx)   Unknown No symbols loaded.
    mscorlib.dll!System.Threading.ExecutionContext.Run(System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, object state) Unknown No symbols loaded.
    mscorlib.dll!System.Threading.ThreadHelper.ThreadStart(object obj)  Unknown No symbols loaded.

SyncTextWriter 타입의 메서드들은 대부분 "[MethodImpl(MethodImplOptions.Synchronized)]" 특성이 적용되므로 단일 스레드에서만 독점 사용할 수 있습니다. 따라서 t1 스레드가 SyncTextWriter.WriteLine으로 먼저 진입해서 쓰고 있는 중에 Suspend가 되었고 Main 스레드에서는 다시 Console.WriteLine 호출로 인한 SyncTextWriter.WriteLine 진입 시도로 hang 상태에 빠진 것입니다.

즉, t.IsAlive 값을 못 구해온 것이 아니고 콘솔 출력에 대한 lock이 걸린 것입니다.

다시 말해서, 콘솔 출력 중인 다수의 스레드가 있을때 특정 스레드가 Suspend되면 다른 스레드들의 콘솔 출력에 모두 hang이 걸릴 수 있으니 주의해야 합니다.




비슷한 이유로 화면 출력이 hang 상태로 빠지는 경우가 있습니다.

static void Main(string[] args)
{
    while (true)
    {
        Thread t = new Thread(threadFunc);
        t.Start(i);
        Console.WriteLine(t.IsAlive);
        t.Suspend();
        Console.WriteLine(t.IsAlive); // 실행 정지
        t.Resume();
        t.Join();
    }
}

private static void threadFunc(object obj)
{
    Console.WriteLine("TEST: " + obj);
}

// 화면 출력 상태
True

즉, threadFunc의 Console.WriteLine이 호출되지 않은 듯 한데 Main 스레드의 Console.WriteLine에서 멈춘 것입니다. 역시 이유는 간단하게 호출 스택을 보면 알 수 있습니다.

// Suspend된 t1 스레드의 콜스택
    mscorlib.dll!System.IO.StreamWriter.Write(char[] buffer, int index, int count)  Unknown No symbols loaded.
    mscorlib.dll!System.IO.TextWriter.WriteLine(string value)   Unknown No symbols loaded.
    mscorlib.dll!System.IO.TextWriter.SyncTextWriter.WriteLine(string value)    Unknown No symbols loaded.
    mscorlib.dll!System.Console.WriteLine(string value) Unknown No symbols loaded.
>    ConsoleApp1.exe!ConsoleApp1.Program.threadFunc(object obj) Line 30  C#  Symbols loaded.
    mscorlib.dll!System.Threading.ThreadHelper.ThreadStart_Context(object state)    Unknown No symbols loaded.
    mscorlib.dll!System.Threading.ExecutionContext.RunInternal(System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, object state, bool preserveSyncCtx)   Unknown No symbols loaded.
    mscorlib.dll!System.Threading.ExecutionContext.Run(System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, object state, bool preserveSyncCtx)   Unknown No symbols loaded.
    mscorlib.dll!System.Threading.ExecutionContext.Run(System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, object state) Unknown No symbols loaded.
    mscorlib.dll!System.Threading.ThreadHelper.ThreadStart(object obj)  Unknown No symbols loaded.

SyncTextWriter.WriteLine까지 진입해서 lock은 획득했으나 화면에 쓰려는 찰나 Suspend로 인해 스레드 실행이 멈춰 버린 것입니다.




또 다른 hang 사례를 보겠습니다.

class Program
{
    static void Main(string[] args)
    {
        int i = 0;
        while (true)
        {
            i++;
            Thread t = new Thread(threadFunc);
            t.Start(i);

            bool alive = t.IsAlive;
            if (alive == true)
            {
                t.Suspend();
            }

            alive = t.IsAlive;
            if (alive == true)
            {
                t.Resume();
            }
            t.Join();
        }
    }

    private static void threadFunc(object obj)
    {
        Console.WriteLine("TEST: " + obj);
    }
}

위와 같이 테스트해보면 어느 순간 hang에 걸리는 것을 볼 수 있습니다. 이때의 Main 스레드 콜스택을 보면 t.Join에서 멈춰 있습니다.

    mscorlib.dll!System.Threading.Thread.Join() Unknown No symbols loaded.
>    ConsoleApp1.exe!ConsoleApp1.Program.Main(string[] args) Line 33 C#  Symbols loaded.

반면 t1 스레드 자체는 이미 종료되어 콜 스택이 없습니다. 즉, 제대로 스레드가 종료된 이후에 호출되는 Join도 hang 상태에 빠져버리므로 Join에는 반드시 timeout 설정을 하는 것이 좋습니다.




이제 코드를 안정화시켜 보겠습니다.

class Program
{
    static void Main(string[] args)
    {
        int i = 0;
        while (true)
        {
            i++;
            Thread t = new Thread(threadFunc);
            t.Start(i);

            bool alive = t.IsAlive;
            if (alive == true)
            {
                t.Suspend();
            }

            alive = t.IsAlive;
            if (alive == true)
            {
                t.Resume();
            }
            t.Join(1000);
        }
    }

    private static void threadFunc(object obj)
    {
        Console.WriteLine("TEST: " + obj);
    }
}

실행해 보면, 다음과 같은 식으로 오류가 발생하는 경우가 있습니다.

TEST: 1
TEST: 2
TEST: 3
TEST: 4
TEST: 5
TEST: 6
TEST: 7
TEST: 8
TEST: 9

Unhandled Exception: System.Threading.ThreadStateException: Thread is not running; it cannot be suspended.
   at System.Threading.Thread.SuspendInternal()
   at System.Threading.Thread.Suspend()
   at ConsoleApp1.Program.Main(String[] args)

즉, t.IsAlive로 체크할 당시만 해도 살아있던 t 스레드가 t.Suspend를 호출하는 사이 종료된 것입니다. 따라서 Suspend에 대해 try/catch를 하는 것이 좋습니다.

결국 Suspend/Resume/Join에 대한 안정적인 코드는 다음과 같습니다.

class Program
{
    static void Main(string[] args)
    {
        int i = 0;
        while (true)
        {
            i++;
            Thread t = new Thread(threadFunc);
            t.Start(i);

            bool alive = t.IsAlive;
            if (alive == true)
            {
                try
                {
                    t.Suspend();
                } catch { }
            }

            alive = t.IsAlive;
            if (alive == true)
            {
                try
                {
                    t.Resume();
                } catch { }

                t.Join(1000);
            }
        }
    }

    private static void threadFunc(object obj)
    {
        Console.WriteLine("TEST: " + obj);
    }
}

규칙은 간단합니다.

  1. Suspend와 Resume 호출은 반드시 try/catch를 해주고,
  2. Join은 timeout 설정을 한다.(음... 불안한데 Join도 try/catch를 해야 할까요? ^^)




[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]







[최초 등록일: ]
[최종 수정일: 8/23/2017]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer at outlook.com

비밀번호

댓글 작성자
 




... 61  62  63  64  65  66  67  68  69  70  71  72  73  74  [75]  ...
NoWriterDateCnt.TitleFile(s)
12153정성태2/23/202024443.NET Framework: 898. Trampoline을 이용한 후킹의 한계파일 다운로드1
12152정성태2/23/202021438.NET Framework: 897. 실행 시에 메서드 가로채기 - CLR Injection: Runtime Method Replacer 개선 - 세 번째 이야기(Trampoline 후킹)파일 다운로드1
12151정성태2/22/202024073.NET Framework: 896. C# - Win32 API를 Trampoline 기법을 이용해 C# 메서드로 가로채는 방법 - 두 번째 이야기 (원본 함수 호출)파일 다운로드1
12150정성태2/21/202024177.NET Framework: 895. C# - Win32 API를 Trampoline 기법을 이용해 C# 메서드로 가로채는 방법 [1]파일 다운로드1
12149정성태2/20/202021079.NET Framework: 894. eBEST C# XingAPI 래퍼 - 연속 조회 처리 방법 [1]
12148정성태2/19/202025765디버깅 기술: 163. x64 환경에서 구현하는 다양한 Trampoline 기법 [1]
12147정성태2/19/202021062디버깅 기술: 162. x86/x64의 기계어 코드 최대 길이
12146정성태2/18/202022260.NET Framework: 893. eBEST C# XingAPI 래퍼 - 로그인 처리파일 다운로드1
12145정성태2/18/202023868.NET Framework: 892. eBEST C# XingAPI 래퍼 - Sqlite 지원 추가파일 다운로드1
12144정성태2/13/202024050.NET Framework: 891. 실행 시에 메서드 가로채기 - CLR Injection: Runtime Method Replacer 개선 - 두 번째 이야기파일 다운로드1
12143정성태2/13/202018465.NET Framework: 890. 상황별 GetFunctionPointer 반환값 정리 - x64파일 다운로드1
12142정성태2/12/202022410.NET Framework: 889. C# 코드로 접근하는 MethodDesc, MethodTable파일 다운로드1
12141정성태2/10/202021397.NET Framework: 888. C# - ASP.NET Core 웹 응용 프로그램의 출력 가로채기 [2]파일 다운로드1
12140정성태2/10/202022738.NET Framework: 887. C# - ASP.NET 웹 응용 프로그램의 출력 가로채기파일 다운로드1
12139정성태2/9/202022429.NET Framework: 886. C# - Console 응용 프로그램에서 UI 스레드 구현 방법
12138정성태2/9/202028636.NET Framework: 885. C# - 닷넷 응용 프로그램에서 SQLite 사용 [6]파일 다운로드1
12137정성태2/9/202020295오류 유형: 592. [AhnLab] 경고 - 디버거 실행을 탐지했습니다.
12136정성태2/6/202021951Windows: 168. Windows + S(또는 Q)로 뜨는 작업 표시줄의 검색 바가 동작하지 않는 경우
12135정성태2/6/202027730개발 환경 구성: 468. Nuget 패키지의 로컬 보관 폴더를 옮기는 방법 [2]
12134정성태2/5/202024983.NET Framework: 884. eBEST XingAPI의 C# 래퍼 버전 - XingAPINet Nuget 패키지 [5]파일 다운로드1
12133정성태2/5/202022751디버깅 기술: 161. Windbg 환경에서 확인해 본 .NET 메서드 JIT 컴파일 전과 후 - 두 번째 이야기
12132정성태1/28/202025843.NET Framework: 883. C#으로 구현하는 Win32 API 후킹(예: Sleep 호출 가로채기) [1]파일 다운로드1
12131정성태1/27/202024504개발 환경 구성: 467. LocaleEmulator를 이용해 유니코드를 지원하지 않는(한글이 깨지는) 프로그램을 실행하는 방법 [1]
12130정성태1/26/202022052VS.NET IDE: 142. Visual Studio에서 windbg의 "Open Executable..."처럼 EXE를 직접 열어 디버깅을 시작하는 방법
12129정성태1/26/202029071.NET Framework: 882. C# - 키움 Open API+ 사용 시 Registry 등록 없이 KHOpenAPI.ocx 사용하는 방법 [3]
12128정성태1/26/202023191오류 유형: 591. The code execution cannot proceed because mfc100.dll was not found. Reinstalling the program may fix this problem.
... 61  62  63  64  65  66  67  68  69  70  71  72  73  74  [75]  ...