Microsoft MVP성태의 닷넷 이야기
닷넷: 2242. C# - 관리 스레드와 비관리 스레드 [링크 복사], [링크+제목 복사],
조회: 5606
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
 

C# - 관리 스레드와 비관리 스레드

닷넷 세계에서 스레드는 2가지로 나뉩니다.

Managed and unmanaged threading in Windows
; https://learn.microsoft.com/en-us/dotnet/standard/threading/managed-and-unmanaged-threading-in-windows

관리 스레드는, 1) 닷넷에서 Thread 타입을 이용해 생성했거나, 2) 비관리 스레드일지라도 관리 환경으로 진입하는 코드를 실행했어야 합니다. 두 번째 사례의 좋은 예가 바로 P/Invoke나 COM Interop 호출입니다. 이런 유의 호출에서 관리/비관리 유무를 확인하는 방법은 TLS에 보관된 관리 Thread 개체를 찾아보는 거라고 합니다.

When an unmanaged thread enters the runtime through, for example, a COM callable wrapper, the system checks the thread-local store of that thread to look for an internal managed Thread object. If one is found, the runtime is already aware of this thread. If it cannot find one, however, the runtime builds a new Thread object and installs it in the thread-local store of that thread.


만약 있다면 그대로 호출을 진행하면 되고, 없다면 TLS에 관리 Thread 개체를 보관해 둔다고.

그리고 그 외의 모든 스레드는 비관리 스레드입니다.

위의 구분으로 문제가 될 수 있는 상황은, "비관리 스레드"에서 닷넷 메서드를 직접 호출하는 경우입니다. 즉, 비관리 스레드에서는 "관리 한경으로 진입하는 코드" 실행 없이 직접 닷넷 메서드를 실행하면 오동작을 할 수 있는 것입니다.

하지만, 저런 사실이 그동안은 별로 문제가 되지 않았습니다. 왜냐하면, 비관리 스레드에 닷넷 메서드의 함수 포인터를 직접 넘겨주는 방법이 쉽지 않았기 때문입니다. 그런데, C# 9.0에 추가된 함수 포인터로 인해,

C# 9.0 - (6) 함수 포인터(Function pointers)
; https://www.sysnet.pe.kr/2/0/12374

드디어 저 상황을 쉽게 재현할 수 있는 사례가 발생합니다. ^^




함수 포인터는 "managed" 유형과 "unmanaged" 유형으로 나뉩니다.

[managed 함수를 가리키는 함수 포인터]
delegate*<string, void> writeLineFunc = &Program.WriteLine;
// 또는
delegate* managed<string, void> writeLineFunc = &Program.WriteLine;

[unmanaged 함수를 가리키는 함수 포인터]
delegate* unmanaged[Stdcall]<int, bool, int> sleepExFunc = (delegate* unmanaged[Stdcall]<int, bool, int>)ptrSleepEx;

하지만 2개 모두 어차피 "포인터"라는 점에는 동일합니다. 즉, 해당 메서드의 메모리 주소를 직접 가리키는 포인터 값입니다. 그렇다면, 이 포인터 값들을 Native 스레드에서 호출하면 어떻게 될까요?

이것을 테스트할 수 있는 예제가 바로 지난 글에 있습니다. ^^

C# - WAV 파일의 PCM 사운드 재생(Windows Multimedia)
; https://www.sysnet.pe.kr/2/0/13599

위의 예제에서 Win32 API에 Callback 함수를 넘겨주는 코드가 다음과 같이 이뤄져 있습니다.

delegate* unmanaged[Stdcall]<IntPtr, WaveMessage, IntPtr, IntPtr, IntPtr, void> waveOutCallbackFunc = &Program.waveOutCallback;

// ...[생략]...

MmResult result = NativeMethods.waveOutOpen(out IntPtr hWaveOut, -1, &format, waveOutCallbackFunc,
    IntPtr.Zero, WaveInOutOpenFlags.CallbackFunction);

// ...[생략]...

[UnmanagedCallersOnly(CallConvs = new Type[] { typeof(CallConvStdcall) })]
static void waveOutCallback(IntPtr hWaveOut, WaveMessage uMsg, IntPtr dwInstance, IntPtr wavhdr, IntPtr dwReserved)
{
    System.Console.WriteLine($"waveOutCallback: {uMsg}");
}

그런데 위의 예제를 delegate* unmanaged가 아니라 delegate*로 바꾸면,

delegate* <IntPtr, WaveMessage, IntPtr, IntPtr, IntPtr, void> waveOutCallbackFunc = &Program.waveOutCallback;

MmResult result = NativeMethods.waveOutOpenManaged(out IntPtr hWaveOut, -1, &format, waveOutCallbackFunc,
    IntPtr.Zero, WaveInOutOpenFlags.CallbackFunction);

static void waveOutCallback(IntPtr hWaveOut, WaveMessage uMsg, IntPtr dwInstance, IntPtr wavhdr, IntPtr dwReserved)
{
    System.Console.WriteLine($"waveOutCallback");
}

[DllImport("winmm.dll", EntryPoint = "waveOutOpen")]
public static extern MmResult waveOutOpenManaged(out IntPtr hWaveOut, IntPtr uDeviceID, WaveFormatEx* lpFormat, delegate* <IntPtr, WaveMessage, IntPtr, IntPtr, IntPtr, void> dwCallback, IntPtr dwInstance, WaveInOutOpenFlags dwFlags);

waveOutWrite로 시작한 소리 재생이 완료되는 시점에, 즉 waveOutCallback이 호출되는 시점에 프로그램이 강제 종료됩니다. 그래서 출력 결과가 이런 식으로 나옵니다.

waveOutCallback
waveOutPrepareHeader: Prepared
waveOutWrite: Prepared, InQueue
.
.
.
.
.
.
.
C:\temp\ConsoleApp1\bin\Debug\net8.0\ConsoleApp1.exe (process 73216) exited with code -1073741819.




문제 분석을 위해, Windbg에서 EXE 파일을 열어 직접 실행하면, 소리 재생이 끝날 즈음 다음과 같이 Access violation 예외가 발생하는 것을 볼 수 있습니다.

(1002c.e420): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
coreclr!Volatile<enum Thread::ThreadState>::LoadWithoutBarrier [inlined in coreclr!JIT_MonEnterWorker_Portable+0x36]:
00007ff9`2e175536 418b4008        mov     eax,dword ptr [r8+8] ds:00000000`00000008=????????

이때의 호출 스택은,

0:017> k
 # Child-SP          RetAddr               Call Site
00 (Inline Function) --------`--------     coreclr!Volatile<enum Thread::ThreadState>::LoadWithoutBarrier [D:\a\_work\1\s\src\coreclr\inc\volatile.h @ 382] 
01 (Inline Function) --------`--------     coreclr!Thread::HasThreadStateOpportunistic [D:\a\_work\1\s\src\coreclr\vm\threads.h @ 1082] 
02 (Inline Function) --------`--------     coreclr!Thread::CatchAtSafePointOpportunistic [D:\a\_work\1\s\src\coreclr\vm\threads.h @ 1244] 
03 (Inline Function) --------`--------     coreclr!Object::TryEnterObjMonitorSpinHelper+0x1d [D:\a\_work\1\s\src\coreclr\vm\object.inl @ 117] 
04 00000037`186bf430 00007ff9`1b2c64a3     coreclr!JIT_MonEnterWorker_Portable+0x36 [D:\a\_work\1\s\src\coreclr\vm\jithelpers.cpp @ 3809] 
05 00000037`186bf470 00007ff9`bc0b95b0     System_Private_CoreLib!System.IO.TextWriter.SyncTextWriter.WriteLine+0x33 [/_/src/libraries/System.Private.CoreLib/src/System/IO/TextWriter.cs @ 874] 
06 00000037`186bf4d0 00007ff8`ce614829     System_Console!System.Console.WriteLine+0x20 [/_/src/libraries/System.Console/src/System/Console.cs @ 839] 
07 00000037`186bf500 00007ffa`558b56f9     0x00007ff8`ce614829
08 00000037`186bf540 00007ffa`3683358b     winmmbase!DriverCallback+0xa9
09 00000037`186bf580 00007ffa`3683334e     msacm32!mapWaveDriverCallback+0x3b
0a 00000037`186bf5d0 00007ffa`558b56f9     msacm32!mapWaveCallback+0x17e
0b 00000037`186bf600 00007ffa`1dca4083     winmmbase!DriverCallback+0xa9
0c 00000037`186bf640 00007ffa`1dca3dd0     wdmaud!CWaveHeader::~CWaveHeader+0x53
0d 00000037`186bf690 00007ffa`1dca31c3     wdmaud!CWaveHandle::Work+0x2f0
0e 00000037`186bf700 00007ffa`1dca2ff0     wdmaud!CWorker::_ThreadProc+0x1a3
0f 00000037`186bf990 00007ffa`782a257d     wdmaud!CWorker::_StaticThreadProc+0x40
10 00000037`186bf9c0 00007ffa`7922aa48     KERNEL32!BaseThreadInitThunk+0x1d
11 00000037`186bf9f0 00000000`00000000     ntdll!RtlUserThreadStart+0x28

콜백 함수까지는 잘 진입이 되었지만, 그 함수가 포함한 Console.WriteLine 내의 CLR 함수를 호출하는 단계에서 예외가 발생한 것을 볼 수 있습니다. 그리고, 이런 문제가 네이티브 스레드에서의 호출로 인한 것이었다는 점을 "!threads" 명령어로 유추할 수 있습니다.

0:018> !threads
ThreadCount:      5
UnstartedThread:  0
BackgroundThread: 4
PendingThread:    0
DeadThread:       0
Hosted Runtime:   no
                                                                                                            Lock  
 DBG   ID     OSID ThreadOBJ           State GC Mode     GC Alloc Context                  Domain           Count Apt Exception
   0    1     d0c4 0000015CFB9DC120  202a020 Preemptive  0000015D00013B90:0000015D00014020 0000015cfb9d18a0 -00001 MTA 
  13    2     271c 0000015CFBA85C40    2b220 Preemptive  0000000000000000:0000000000000000 0000015cfb9d18a0 -00001 MTA (Finalizer) 
  15    4     b944 0000015C80435FC0  302b220 Preemptive  0000015D0000D4F8:0000015D0000EBA0 0000015cfb9d18a0 -00001 MTA (Threadpool Worker) 
  16    5     6fd0 0000015C80438B80  302b220 Preemptive  0000015D0000EFA0:0000015D00010BC0 0000015cfb9d18a0 -00001 MTA (Threadpool Worker) 
  17    6     2730 0000015C80442050  102b220 Preemptive  0000015D00010EF0:0000015D00010FD0 0000015cfb9d18a0 -00001 MTA (Threadpool Worker) 

보는 바와 같이 (callback 함수를 실행한) 18번 스레드가 없습니다. 즉, 18번 스레드는 비관리 스레드이고 그런 환경에서 닷넷 메서드를 호출했기 때문에 결국 crash가 발생한 것입니다. 따라서, managed 함수 포인터를 절대로 native 측에 전달해서는 안 됩니다.




"C# - WAV 파일의 PCM 사운드 재생(Windows Multimedia)" 예제가 재미있는 것은, Callback 메서드가 초기에는 정상적으로 실행이 되었다는 점입니다.

waveOutCallback
waveOutPrepareHeader: Prepared
waveOutWrite: Prepared, InQueue
.
.
.
.
.
.
.
C:\temp\ConsoleApp1\bin\Debug\net8.0\ConsoleApp1.exe (process 73216) exited with code -1073741819.

즉, NativeMethods.waveOutOpen 호출 시점에는 callback 메서드의 코드가 잘 수행되었다는 점인데요, 아니... Native 스레드에서는 managed 함수 포인터를 실행해서는 안 되는데 왜 저 시점에서는 잘 된 것일까요?

왜냐하면, waveOutOpen 시에 발생한 callback은 해당 함수를 실행한 스레드에서 직접 실행하기 때문입니다. 즉, 닷넷 런타임이 초기화된 스레드에서 managed 함수가 실행된 것이므로 문제가 없었던 것입니다. 반면, 소리 재생이 완료된 시점에는 윈도우 운영체제가 임의로 선택한 Native Thread에서 managed 함수 포인터를 실행하기 때문에 문제가 발생한 것입니다.




물론, "managed *" 함수 포인터가 아닌, 기존의 Delegate를 전달하는 유형이라면 위와 같은 문제가 발생하지 않습니다.

waveOutCallbackDelegate waveOutCallbackFunc = Program.waveOutCallback;

MmResult result = NativeMethods.waveOutOpenDelegate(out IntPtr hWaveOut, -1, &format, waveOutCallbackFunc,
    IntPtr.Zero, WaveInOutOpenFlags.CallbackFunction);

static void waveOutCallback(IntPtr hWaveOut, WaveMessage uMsg, IntPtr dwInstance, IntPtr wavhdr, IntPtr dwReserved)
{
    System.Console.WriteLine($"waveOutCallback: {uMsg}");
}

delegate void waveOutCallbackDelegate(IntPtr hWaveOut, WaveMessage uMsg, IntPtr dwInstance, IntPtr wavhdr, IntPtr dwReserved);

[DllImport("winmm.dll", EntryPoint = "waveOutOpen")]
public static extern MmResult waveOutOpenDelegate(out IntPtr hWaveOut, IntPtr uDeviceID, WaveFormatEx* lpFormat, waveOutCallbackDelegate dwCallback, IntPtr dwInstance, WaveInOutOpenFlags dwFlags);

(단지, 이런 경우 GC 후에도 waveOutCallbackFunc 인스턴스가 제거되지 않도록 전역 변수 처리를 해야 합니다.)

위와 같이 Delegate를 전달해 callback 함수가 Native 함수에서 불리는 상태의 callstack을 보면,

ConsoleApp1.dll!ConsoleApp1.Program.waveOutCallback(nint hWaveOut = 0x0000020f80515790, AudioLibrary.WaveMessage uMsg = WaveOutDone, nint dwInstance = 0x0000000000000000, nint wavhdr = 0x0000008adf3ae748, nint dwReserved = 0x0000000000000000) Line 91
[Native to Managed Transition]
winmmbase.dll!DriverCallback()
msacm32.drv!mapWaveDriverCallback()
msacm32.drv!mapWaveCallback()
winmmbase.dll!DriverCallback()
wdmaud.drv!CWaveHeader::~CWaveHeader(void)
wdmaud.drv!CWaveHandle::Work()
wdmaud.drv!CWorker::_ThreadProc()
wdmaud.drv!CWorker::_StaticThreadProc(void *)
kernel32.dll!BaseThreadInitThunk()
ntdll.dll!RtlUserThreadStart()

저렇게 중간에 "Native to Managed Transition" 층을 볼 수 있습니다. Delegate가 감싼 덕분에 생긴 층으로 바로 저 코드가 있기 때문에 문제없이 닷넷 코드들이 잘 동작한 것입니다. 반면, (Delegate가 아닌) managed 함수 포인터로 전달한 경우에는 다음과 같은 호출 스택이 나옵니다.

ntdll.dll!RtlpExecuteHandlerForException()
ntdll.dll!RtlDispatchException()
ntdll.dll!KiUserExceptionDispatch()
System.Private.CoreLib.dll!00007ff94bca72a1()
System.Private.CoreLib.dll!00007ff94bbc1b68()
winmmbase.dll!DriverCallback()
msacm32.drv!mapWaveDriverCallback()
msacm32.drv!mapWaveCallback()
winmmbase.dll!DriverCallback()
wdmaud.drv!CWaveHeader::~CWaveHeader(void)
wdmaud.drv!CWaveHandle::Work()
wdmaud.drv!CWorker::_ThreadProc()
wdmaud.drv!CWorker::_StaticThreadProc(void *)
kernel32.dll!BaseThreadInitThunk()
ntdll.dll!RtlUserThreadStart()

DriverCallback에서 Program.waveOutCallback 메서드를 호출하는 중간에 "Native to Managed Transition" 층이 없고, 이로 인해 닷넷 런타임에 의존한 코드를 callback 메서드에서 실행하는 경우 crash가 발생하는 것입니다.




그런데, UnmanagedCallersOnly 특성을 부여한 unmanaged 함수 포인터를 Native가 호출한 경우에는 어떻게 정상적으로 닷넷 코드를 실행할 수 있었던 걸까요? Native 스레드 입장에서는 닷넷이 전달한 함수 포인터를 그대로 실행했을 텐데, 그렇다면 그 경우에도 마찬가지로 crash가 발생했어야 합니다.

그 이유는, UnmanagedCallersOnly 특성으로 인해 해당 메서드의 JIT 컴파일이 달라지기 때문입니다.

[UnmanagedCallersOnly(CallConvs = new Type[] { typeof(CallConvStdcall) })]
static void waveOutCallback(IntPtr hWaveOut, WaveMessage uMsg, IntPtr dwInstance, IntPtr wavhdr, IntPtr dwReserved)
{
    System.Console.WriteLine($"waveOutCallback: {uMsg}");
}

위의 waveOutCallback 메서드에 대한 disassembly 코드를 보면,

00007FF8EC5EE300 55                   push        rbp  
00007FF8EC5EE301 57                   push        rdi  
00007FF8EC5EE302 56                   push        rsi  
00007FF8EC5EE303 48 83 EC 60          sub         rsp,60h  
00007FF8EC5EE307 48 8B EC             mov         rbp,rsp  
00007FF8EC5EE30A C5 D8 57 E4          vxorps      xmm4,xmm4,xmm4  
00007FF8EC5EE30E C5 F9 7F 65 20       vmovdqa     xmmword ptr [rbp+20h],xmm4  
00007FF8EC5EE313 C5 F9 7F 65 30       vmovdqa     xmmword ptr [rbp+30h],xmm4  
00007FF8EC5EE318 C5 F9 7F 65 40       vmovdqa     xmmword ptr [rbp+40h],xmm4  
00007FF8EC5EE31D C5 F9 7F 65 50       vmovdqa     xmmword ptr [rbp+50h],xmm4  
00007FF8EC5EE322 48 89 8D 80 00 00 00 mov         qword ptr [rbp+80h],rcx  
00007FF8EC5EE329 89 95 88 00 00 00    mov         dword ptr [rbp+88h],edx  
00007FF8EC5EE32F 4C 89 85 90 00 00 00 mov         qword ptr [rbp+90h],r8  
00007FF8EC5EE336 4C 89 8D 98 00 00 00 mov         qword ptr [rbp+98h],r9  
00007FF8EC5EE33D 48 8D 4D 20          lea         rcx,[rbp+20h]  
00007FF8EC5EE341 E8 DA AF A9 5F       call        JIT_ReversePInvokeEnter (07FF94C089320h)  
00007FF8EC5EE346 83 3D DB FF 07 00 00 cmp         dword ptr [7FF8EC66E328h],0  
00007FF8EC5EE34D 74 05                je          ConsoleApp1.Program.waveOutCallback(IntPtr, AudioLibrary.WaveMessage, IntPtr, IntPtr, IntPtr)+054h (07FF8EC5EE354h)  
00007FF8EC5EE34F E8 FC E2 C5 5F       call        JIT_DbgIsJustMyCode (07FF94C24C650h)  
...[생략]...

중간에 JIT_ReversePInvokeEnter 호출이 있습니다. 아마도 저 함수가 관리 스레드 개체를 TLS에 설정하는 코드로 짐작됩니다. 실제로 디버거를 통해 호출 스택을 보면 위의 경우 "[Native to Managed Transition]" 층이 있는 것을 확인할 수 있습니다. (그런데, 한 가지 재미있는 점은, 도대체 저 "층"을 어떻게 알고 Windbg는 보여주는 걸까요? 단순히 저렇게 함수 호출로 인한 차이라면 "층"이 있다는 것을 알 수 없는데 뭔가 제가 모르는 또 다른 처리가 있는 듯합니다. 혹시 알고 계신 분은 덧글 부탁드립니다. ^^)

반면, UnmanagedCallersOnly 특성이 없다면,

static void waveOutCallback(IntPtr hWaveOut, WaveMessage uMsg, IntPtr dwInstance, IntPtr wavhdr, IntPtr dwReserved)
{
    System.Console.WriteLine($"waveOutCallback: {uMsg}");
}

이렇게 바뀌는데요,

00007FF8EBFD6120 55                   push        rbp  
00007FF8EBFD6121 57                   push        rdi  
00007FF8EBFD6122 56                   push        rsi  
00007FF8EBFD6123 48 83 EC 50          sub         rsp,50h  
00007FF8EBFD6127 48 8B EC             mov         rbp,rsp  
00007FF8EBFD612A C5 D8 57 E4          vxorps      xmm4,xmm4,xmm4  
00007FF8EBFD612E C5 F9 7F 65 20       vmovdqa     xmmword ptr [rbp+20h],xmm4  
00007FF8EBFD6133 C5 F9 7F 65 30       vmovdqa     xmmword ptr [rbp+30h],xmm4  
00007FF8EBFD6138 C5 F9 7F 65 40       vmovdqa     xmmword ptr [rbp+40h],xmm4  
00007FF8EBFD613D 48 89 4D 70          mov         qword ptr [rbp+70h],rcx  
00007FF8EBFD6141 89 55 78             mov         dword ptr [rbp+78h],edx  
00007FF8EBFD6144 4C 89 85 80 00 00 00 mov         qword ptr [rbp+80h],r8  
00007FF8EBFD614B 4C 89 8D 88 00 00 00 mov         qword ptr [rbp+88h],r9  
00007FF8EBFD6152 83 3D CF 81 08 00 00 cmp         dword ptr [7FF8EC05E328h],0  
00007FF8EBFD6159 74 05                je          ConsoleApp1.Program.waveOutCallback(IntPtr, AudioLibrary.WaveMessage, IntPtr, IntPtr, IntPtr)+040h (07FF8EBFD6160h)  
00007FF8EBFD615B E8 F0 64 C9 5F       call        JIT_DbgIsJustMyCode (07FF94BC6C650h)  
...[생략]...

당연히 닷넷 메서드이기 때문에 비관리 스레드에서 호출될 것을 신경 쓰지 않아도 되므로 JIT_ReversePInvokeEnter 호출이 없습니다. 따라서, 비관리 스레드에서 호출되는 경우 "[Native to Managed Transition]" 층을 거치지 않으므로 CLR 함수 호출 시 crash가 발생하는 것입니다.




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







[최초 등록일: ]
[최종 수정일: 4/19/2024]

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)
13637정성태6/6/20244789Phone: 20. C# MAUI - 유튜브 동영상을 MediaElement로 재생하는 방법
13636정성태5/30/20244858닷넷: 2264. C# - 형식 인자로 인터페이스를 갖는 제네릭 타입으로의 형변환파일 다운로드1
13635정성태5/29/20245347Phone: 19. C# MAUI - 안드로이드 "Share" 대상으로 등록하는 방법
13634정성태5/24/20244997Phone: 18. C# MAUI - 안드로이드 플랫폼에서의 Activity 제어 [1]
13633정성태5/22/20244956스크립트: 64. 파이썬 - ASGI를 만족하는 최소한의 구현 코드
13632정성태5/20/20245075Phone: 17. C# MAUI - Android 내에 Web 서비스 호스팅
13631정성태5/19/20245057Phone: 16. C# MAUI - /Download 등의 공용 디렉터리에 접근하는 방법 [1]
13630정성태5/19/20245469닷넷: 2263. C# - Thread가 Task보다 더 빠르다는 어떤 예제(?)
13629정성태5/18/20245295개발 환경 구성: 710. Android - adb.exe를 이용한 파일 전송
13628정성태5/17/20244957개발 환경 구성: 709. Windows - WHPX(Windows Hypervisor Platform)를 이용한 Android Emulator 가속
13627정성태5/17/20245000오류 유형: 904. 파이썬 - UnicodeEncodeError: 'ascii' codec can't encode character '...' in position ...: ordinal not in range(128)
13626정성태5/15/20245094Phone: 15. C# MAUI - MediaElement Source 경로 지정 방법파일 다운로드1
13625정성태5/14/20245256닷넷: 2262. C# - Exception Filter 조건(when)을 갖는 catch 절의 IL 구조
13624정성태5/12/20245236Phone: 14. C# - MAUI에서 MediaElement 사용파일 다운로드1
13623정성태5/11/20245115닷넷: 2261. C# - 구글 OAuth의 JWT (JSON Web Tokens) 해석파일 다운로드1
13622정성태5/10/20245337닷넷: 2260. C# - Google 로그인 연동 (ASP.NET 예제)파일 다운로드1
13621정성태5/10/20244977오류 유형: 903. IISExpress - Failed to register URL "..." for site "..." application "/". Error description: Cannot create a file when that file already exists. (0x800700b7)
13620정성태5/9/20244992VS.NET IDE: 190. Visual Studio가 node.exe를 경유해 Edge.exe를 띄우는 경우
13619정성태5/7/20245129닷넷: 2259. C# - decimal 저장소의 비트 구조파일 다운로드1
13618정성태5/6/20245259닷넷: 2258. C# - double (배정도 실수) 저장소의 비트 구조파일 다운로드1
13617정성태5/5/20245318닷넷: 2257. C# - float (단정도 실수) 저장소의 비트 구조파일 다운로드1
13616정성태5/3/20245367닷넷: 2256. ASP.NET Core 웹 사이트의 HTTP/HTTPS + Dual mode Socket (IPv4/IPv6) 지원 방법파일 다운로드1
13615정성태5/3/20245402닷넷: 2255. C# 배열을 Numpy ndarray 배열과 상호 변환
13614정성태5/2/20245343닷넷: 2254. C# - COM 인터페이스의 상속 시 중복으로 메서드를 선언
13613정성태5/1/20245855닷넷: 2253. C# - Video Capture 장치(Camera) 열거 및 지원 포맷 조회파일 다운로드1
13612정성태4/30/20245508오류 유형: 902. Visual Studio - error MSB3021: Unable to copy file
1  2  3  4  5  6  7  8  [9]  10  11  12  13  14  15  ...