Microsoft MVP성태의 닷넷 이야기
.NET Framework: 456. C# - CAS를 이용한 Lock 래퍼 클래스 [링크 복사], [링크+제목 복사],
조회: 21838
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
(연관된 글이 1개 있습니다.)

C# - CAS를 이용한 Lock 래퍼 클래스

기왕 lock-free를 통해 CAS(Compare-And-Swap)을 다뤄본 김에,

[부연] Lock-Free 알고리즘은 과연 빠른가?
; https://www.sysnet.pe.kr/2/0/1736

lock-free 방식이 과연 성능에 얼마나 도움이 될까요?
; https://www.sysnet.pe.kr/2/0/1458

간단하게 클래스로 래핑해 볼까요? ^^ 대충 다음과 같은 정도로 만들 수 있습니다.

public class CASLock : IDisposable // .NET IDisposable 처리 정리
                                    // ; https://www.sysnet.pe.kr/2/0/347
{
    int _lockVariable = 0;
    bool _disposed = true;

    public IDisposable Lock()
    {
        while (Interlocked.CompareExchange(ref _lockVariable, 1, 0) != 0) { }
        _disposed = false;
        return this;
    }

    void Free(bool disposing)
    {
        _lockVariable = 0;
        _disposed = true;
    }

    public void Dispose()
    {
        Free(true);
        GC.SuppressFinalize(this);
    }

    ~CASLock()
    {
#if DEBUG
        if (false == _disposed)
        {
            throw new ApplicationException("CASLock.Dispose() was not called!");
        }
#endif

        Free(false);
    }
}

사용법은 using을 통해 기존의 lock문과 거의 유사하게 쓸 수 있습니다.

CASLock lockA = new CASLock();

using (lockA.Lock())
{
    Console.WriteLine("locked");
}

전형적인 dead-lock 상황을 연출해 볼까요? ^^

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

namespace ConsoleApplication2
{
    class Program
    {
        CASLock lockA = new CASLock();
        CASLock lockB = new CASLock();

        static void Main(string[] args)
        {
            Program p = new Program();

            Thread t1 = new Thread(p.t1);
            Thread t2 = new Thread(p.t2);

            t1.Name = "lockAB";
            t2.Name = "lockBA";

            t1.Start();
            t2.Start();

            t1.Join(); // t1 스레드는 절대로 종료하지 않으므로 Join문은 반환하지 않음.
            t2.Join();
        }

        // Thread 1 
        void t1()
        {
            using (lockA.Lock())
            {
                Thread.Sleep(2000);
                using (lockB.Lock()) // Thread2의 t2메서드에서 이미 lock을 소유하고 있으므로 block
                {
                    Console.WriteLine("lockA -> lockB");
                }
            }
        }

        // Thread 2 
        void t2()
        {
            using (lockB.Lock())
            {
                Thread.Sleep(2000);
                using (lockA.Lock()) // Thread1의 t1메서드에서 이미 lock을 소유하고 있으므로 block
                {
                    Console.WriteLine("lockB -> lockA");
                }
            }
        }
    }
}

여기서, Thread.Abort 메서드로 특정 스레드를 강제 종료하는 것으로 우리가 만든 CASLock 클래스의 안정성 테스트를 해보겠습니다.

static void Main(string[] args)
{
    Program p = new Program();

    Thread t1 = new Thread(p.t1);
    Thread t2 = new Thread(p.t2);

    t1.Name = "lockAB";
    t2.Name = "lockBA";

    t1.Start();
    t2.Start();

    int retryCount = 5;
    while (retryCount-- > 0)
    {
        Console.Write(".");
        Thread.Sleep(1000);
    }

    t1.Abort(); // Thread1 강제 종료

    t1.Join();
    t2.Join();
}

테스트 코드를 실행시켜 보면 5초 후에 t1.Abort가 호출되고 이는 해당 스레드에 ThreadAbortException 예외를 발생시킵니다. 따라서 t1 메서드 내부의 using 문에서 IDisposable.Dispose 메서드가 실행되고 이로 인해 lockA의 잠금이 해제됩니다.

그런데, Native Win32 API인 TerminateThread를 호출하면 어떻게 될까요?

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

namespace ConsoleApplication2
{
    class Program
    {
        [DllImport("kernel32.dll")]
        static extern bool TerminateThread(IntPtr hThread, uint dwExitCode);

        [Flags]
        public enum ThreadAccess : int
        {
            TERMINATE = (0x0001),
        }

        [DllImport("kernel32.dll", SetLastError = true)]
        static extern IntPtr OpenThread(ThreadAccess dwDesiredAccess, bool bInheritHandle, int dwThreadId);

        static IntPtr _threadAHandle = IntPtr.Zero;

        static void Main(string[] args)
        {
            Program p = new Program();

            Thread t1 = new Thread(p.t1);
            Thread t2 = new Thread(p.t2);

            t1.Name = "lockAB";
            t2.Name = "lockBA";

            t1.Start();
            t2.Start();

            int retryCount = 5;
            while (retryCount-- > 0)
            {
                Console.Write(".");
                Thread.Sleep(1000);
            }

            Console.WriteLine("Terminating...: " + Process.GetCurrentProcess().Threads.Count);
            TerminateThread(_threadAHandle, 100);
            Console.WriteLine("Terminated.: " + Process.GetCurrentProcess().Threads.Count);

            t1.Join();
            t2.Join();
        }

        CASLock lockA = new CASLock();
        CASLock lockB = new CASLock();


        // Thread 1 
        void t1()
        {
            _threadAHandle = OpenThread(ThreadAccess.TERMINATE, false, AppDomain.GetCurrentThreadId());
            using (lockA.Lock())
            {
                Thread.Sleep(2000);
                using (lockB.Lock())
                {
                    Console.WriteLine("lockA -> lockB");
                }
            }
        }

        // Thread 2 
        void t2()
        {
            using (lockB.Lock())
            {
                Thread.Sleep(2000);
                using (lockA.Lock())
                {
                    Console.WriteLine("lockB -> lockA");
                }
            }
        }
    }
}

이런 경우, Thread1이 종료되었음에도 불구하고 lock은 여전히 잠김상태로 남게 됩니다. (기존의 Monitor.Enter/Exit를 해도 동일하게 문제가 나타납니다.) 이는 LockFree 클래스의 안정성에 문제가 있는 것은 아니고 TerminateThread API는 때로 이렇게 위험한 상황을 연출하기 때문에 문서에는 다음과 같이 주의 사항이 나옵니다.

TerminateThread function
; https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-terminatethread

TerminateThread is a dangerous function that should only be used in the most extreme cases. You should call TerminateThread only if you know exactly what the target thread is doing, and you control all of the code that the target thread could possibly be running at the time of the termination. For example, TerminateThread can result in the following problems:
  • If the target thread owns a critical section, the critical section will not be released.
  • If the target thread is allocating memory from the heap, the heap lock will not be released.
  • If the target thread is executing certain kernel32 calls when it is terminated, the kernel32 state for the thread's process could be inconsistent.
  • If the target thread is manipulating the global state of a shared DLL, the state of the DLL could be destroyed, affecting other users of the DLL.
A thread cannot protect itself against TerminateThread, ...

보시는 것처럼, .NET 세계에서뿐만 아니라 Win32 SDK 세계에서도 TerminateThread API를 잘못 사용하면 저렇게 많은 문제를 발생할 수 있습니다. ^^ 한마디로, 모든 상황을 잘 이해하고 있지 않은 상황에서는 절대 써서는 안될 API입니다.

(첨부한 프로젝트는 위의 예제를 포함하고 있습니다.)




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

[연관 글]






[최초 등록일: ]
[최종 수정일: 9/16/2023]

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

비밀번호

댓글 작성자
 




1  [2]  3  4  5  6  7  8  9  10  11  12  13  14  15  ...
NoWriterDateCnt.TitleFile(s)
13893정성태2/27/20252238Linux: 115. eBPF (bpf2go) - ARRAY / HASH map 기본 사용법
13892정성태2/24/20252994닷넷: 2325. C# - PowerShell과 연동하는 방법파일 다운로드1
13891정성태2/23/20252510닷넷: 2324. C# - 프로세스의 성능 카운터용 인스턴스 이름을 구하는 방법파일 다운로드1
13890정성태2/21/20252337닷넷: 2323. C# - 프로세스 메모리 중 Private Working Set 크기를 구하는 방법(Win32 API)파일 다운로드1
13889정성태2/20/20253071닷넷: 2322. C# - 프로세스 메모리 중 Private Working Set 크기를 구하는 방법(성능 카운터, WMI) [1]파일 다운로드1
13888정성태2/17/20252501닷넷: 2321. Blazor에서 발생할 수 있는 async void 메서드의 부작용
13887정성태2/17/20253083닷넷: 2320. Blazor의 razor 페이지에서 code-behind 파일로 코드를 분리 및 DI 사용법
13886정성태2/15/20252577VS.NET IDE: 196. Visual Studio - Code-behind처럼 cs 파일을 그룹핑하는 방법
13885정성태2/14/20253243닷넷: 2319. ASP.NET Core Web API / Razor 페이지에서 발생할 수 있는 async void 메서드의 부작용
13884정성태2/13/20253527닷넷: 2318. C# - (async Task가 아닌) async void 사용 시의 부작용파일 다운로드1
13883정성태2/12/20253288닷넷: 2317. C# - Memory Mapped I/O를 이용한 PCI Configuration Space 정보 열람파일 다운로드1
13882정성태2/10/20252588스크립트: 70. 파이썬 - oracledb 패키지 연동 시 Thin / Thick 모드
13881정성태2/7/20252839닷넷: 2316. C# - Port I/O를 이용한 PCI Configuration Space 정보 열람파일 다운로드1
13880정성태2/5/20253183오류 유형: 947. sshd - Failed to start OpenSSH server daemon.
13879정성태2/5/20253424오류 유형: 946. Ubuntu - N: Updating from such a repository can't be done securely, and is therefore disabled by default.
13878정성태2/3/20253199오류 유형: 945. Windows - 최대 절전 모드 시 DRIVER_POWER_STATE_FAILURE 발생 (pacer.sys)
13877정성태1/25/20253252닷넷: 2315. C# - PCI 장치 열거 (레지스트리, SetupAPI)파일 다운로드1
13876정성태1/25/20253716닷넷: 2314. C# - ProcessStartInfo 타입의 Arguments와 ArgumentList파일 다운로드1
13875정성태1/24/20253149스크립트: 69. 파이썬 - multiprocessing 패키지의 spawn 모드로 동작하는 uvicorn의 workers
13874정성태1/24/20253572스크립트: 68. 파이썬 - multiprocessing Pool의 기본 프로세스 시작 모드(spawn, fork)
13873정성태1/23/20252984디버깅 기술: 217. WinDbg - PCI 장치 열거파일 다운로드1
13872정성태1/23/20252902오류 유형: 944. WinDbg - 원격 커널 디버깅이 연결은 되지만 Break (Ctrl + Break) 키를 눌러도 멈추지 않는 현상
13871정성태1/22/20253302Windows: 278. Windows - 윈도우를 다른 모니터 화면으로 이동시키는 단축키 (Window + Shift + 화살표)
13870정성태1/18/20253752개발 환경 구성: 741. WinDbg - 네트워크 커널 디버깅이 가능한 NIC 카드 지원 확대
13869정성태1/18/20253469개발 환경 구성: 740. WinDbg - _NT_SYMBOL_PATH 환경 변수에 설정한 경로로 심벌 파일을 다운로드하지 않는 경우
13868정성태1/17/20253119Windows: 277. Hyper-V - Windows 11 VM의 Enhanced Session 모드로 로그인을 할 수 없는 문제
1  [2]  3  4  5  6  7  8  9  10  11  12  13  14  15  ...