Microsoft MVP성태의 닷넷 이야기
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
 
(연관된 글이 2개 있습니다.)
(시리즈 글이 7개 있습니다.)
.NET Framework: 394. async/await 사용 시 hang 문제가 발생하는 경우
; https://www.sysnet.pe.kr/2/0/1541

.NET Framework: 512. async/await 사용 시 hang 문제가 발생하는 경우 - 두 번째 이야기
; https://www.sysnet.pe.kr/2/0/10801

.NET Framework: 631. async/await에 대한 "There Is No Thread" 글의 부가 설명
; https://www.sysnet.pe.kr/2/0/11129

.NET Framework: 720. 비동기 메서드 내에서 await 시 ConfigureAwait 호출 의미
; https://www.sysnet.pe.kr/2/0/11418

.NET Framework: 721. WebClient 타입의 ...Async 메서드 호출은 왜 await + 동기 호출 시 hang 현상이 발생할까요?
; https://www.sysnet.pe.kr/2/0/11419

디버깅 기술: 196. windbg - async/await 비동기인 경우 메모리 덤프 분석의 어려움
; https://www.sysnet.pe.kr/2/0/13563

닷넷: 2225. Windbg - dumasync로 분석하는 async/await 호출
; https://www.sysnet.pe.kr/2/0/13573




async/await 사용 시 hang 문제가 발생하는 경우 - 두 번째 이야기

오호~~~ 재미있는 글이 쓰여졌군요. ^^

SynchronizationContext와 async/await - 1. What the?
; http://blog.naver.com/vactorman/220371851151

SynchronizationContext와 async/await - 2. 얉은 구조
; http://blog.naver.com/vactorman/220371861151

SynchronizationContext와 async/await - 3. Custom SynchronizationContext 구현
; http://blog.naver.com/vactorman/220371881666

SynchronizationContext와 async/await - 4. async/await 궁합 
; http://blog.naver.com/vactorman/220371896727

4번째 "SynchronizationContext와 async/await - 4. async/await 궁합 " 글에 보면 제가 쓴 다음의 글도 소개하고 있습니다.

async/await 사용 시 hang 문제가 발생하는 경우
; https://www.sysnet.pe.kr/2/0/1541

그런데, 4번째 글에서 언급하고 있는 내용이 제 글의 내용과 충돌합니다. 제 글에서는 스레드 블록킹 현상의 원인이 "SynchronizationContext"의 "Post" 처리를 하지 않고 "Send" 처리를 했기 때문이라고 했는데 "SynchronizationContext와 async/await - 4. async/await 궁합 " 글에서는 "async/await 사용 시 await 이하 구문은 현재 SynchronizationContext의 Post()로 재진입하여 수행된다"라고 언급합니다.

분명히 누구 하나는 틀린 이야기를 하고 있습니다. ^^




그래서 천천히 검증을 한번 해봤습니다. ^^ 우선, 소스 코드 디버깅을 하기 위해 cmd.exe 창을 띄우고 아래의 글에 따라,

COMPLUS_ZapDisable - JIT 최적화 코드 생성 제어
; https://www.sysnet.pe.kr/2/0/646

다음과 같이 최적화 옵션을 끄고 개발 환경을 실행했습니다. (F5 디버깅을 위해!)

C:\Program Files (x86)\Microsoft Visual Studio 12.0>set COMPLUS_ZapDisable=1
C:\Program Files (x86)\Microsoft Visual Studio 12.0>devenv

그런 다음 ".NET Reflector"의 소스 코드 디버깅 옵션을 mscorlib.dll, System.dll, System.Core.dll System.Windows.Forms.dll 들에 대해 "Generate PDB"를 적용해 두었습니다.

자... 그래서 "async/await 사용 시 hang 문제가 발생하는 경우"의 글에 첨부한 예제 프로젝트를 실행시켜 hang이 걸리게 실행한 다음 콜 스택을 통해 실행이 멈춘 지점을 확인해 보았습니다. "this.Text = textTask.Result" 코드의 실행 부분의 콜 스택에 다음의 InternalWait 호출을 확인할 수 있습니다.

[System.Threading.Tasks.Task<TResult>]

        internal TResult GetResultCore(bool waitCompletionNotification)
        {
            if (!base.IsCompleted)
            {
                base.InternalWait(-1, new CancellationToken());
            }
            if (waitCompletionNotification)
            {
                base.NotifyDebuggerOfWaitCompletionIfNecessary();
            }
            if (!base.IsRanToCompletion)
            {
                base.ThrowIfExceptional(true);
            }
            return this.m_result;
        }

InternalWait 함수를 내려가 보면,

[System.Threading.Tasks.Task]

        [MethodImpl(MethodImplOptions.NoOptimization)]
        internal bool InternalWait(int millisecondsTimeout, System.Threading.CancellationToken cancellationToken)
        {
            TplEtwProvider log = TplEtwProvider.Log;
            bool flag = log.IsEnabled();
            if (flag)
            {
                Task internalCurrent = InternalCurrent;
                log.TaskWaitBegin((internalCurrent != null) ? internalCurrent.m_taskScheduler.Id : TaskScheduler.Default.Id, (internalCurrent != null) ? internalCurrent.Id : 0, this.Id, TplEtwProvider.TaskWaitBehavior.Synchronous);
            }
            bool isCompleted = this.IsCompleted;
            if (!isCompleted)
            {
                Debugger.NotifyOfCrossThreadDependency();
                if (((millisecondsTimeout == -1) && !cancellationToken.CanBeCanceled) && (this.WrappedTryRunInline() && this.IsCompleted))
                {
                    isCompleted = true;
                }
                else
                {
                    isCompleted = this.SpinThenBlockingWait(millisecondsTimeout, cancellationToken);
                }
            }
            if (flag)
            {
                Task task2 = InternalCurrent;
                if (task2 != null)
                {
                    log.TaskWaitEnd(task2.m_taskScheduler.Id, task2.Id, this.Id);
                    return isCompleted;
                }
                log.TaskWaitEnd(TaskScheduler.Default.Id, 0, this.Id);
            }
            return isCompleted;
        }

this.SpinThenBlockingWait 호출로 이어지고,

[System.Threading.Tasks.Task]

        private bool SpinThenBlockingWait(int millisecondsTimeout, System.Threading.CancellationToken cancellationToken)
        {
            bool flag = millisecondsTimeout == -1;
            uint num = flag ? 0 : ((uint) Environment.TickCount);
            bool flag2 = this.SpinWait(millisecondsTimeout);
            if (!flag2)
            {
                SetOnInvokeMres action = new SetOnInvokeMres();
                try
                {
                    this.AddCompletionAction(action, true);
                    if (flag)
                    {
                        return action.Wait(-1, cancellationToken);
                    }
                    uint num2 = ((uint) Environment.TickCount) - num;
                    if (num2 < millisecondsTimeout)
                    {
                        flag2 = action.Wait((int) (millisecondsTimeout - ((int) num2)), cancellationToken);
                    }
                }
                finally
                {
                    if (!this.IsCompleted)
                    {
                        this.RemoveContinuation(action);
                    }
                }
            }
            return flag2;
        }

보는 바와 같이 SpinThenBlockingWait 내에서는 SetOnInvokeMres 타입의 인스턴스를 생성한 후 AddCompletionAction을 호출 한후 action.Wait 대기 상태로 빠집니다. 그럼, SetOnInvokeMres 타입과 AddCompletionAction으로 들어가 볼까요?

[System.Threading.Tasks.Task]

        private sealed class SetOnInvokeMres : ManualResetEventSlim, ITaskCompletionAction
        {
            internal SetOnInvokeMres() : base(false, 0)
            {
            }
            
            public void Invoke(Task completingTask)
            {
                base.Set();
            }
        }

        private void AddCompletionAction(ITaskCompletionAction action, bool addBeforeOthers) // addBeforeOthers == true
        {
            if (!this.AddTaskContinuation(action, addBeforeOthers)) // AddTaskContinuation 호출이 return == true 반환해서 메서드를 빠져나감.
            {
                action.Invoke(this); // 호출되지 않았음.
            }
        }

만약 위의 코드에서 AddTaskContinuation 메서드가 false를 반환했다면 action.Invoke가 불렸을 테고, SetOnInvokeMres.Invoke 메서드에서는 "base.Set()"의 호출로 이벤트를 Signal하기 때문에 이후의 action.Wait 대기가 곧바로 해제되어 hang 현상은 없었을 것입니다. 그럼, 다시 AddTaskContinuation을 들어가 보면,

[System.Threading.Tasks.Task]

        private bool AddTaskContinuation(object tc, bool addBeforeOthers)
        {
            if (this.IsCompleted)
            {
                return false;
            }
            return (((this.m_continuationObject == null) && (Interlocked.CompareExchange(ref this.m_continuationObject, tc, null) == null)) || this.AddTaskContinuationComplex(tc, addBeforeOthers));
        }

hang 현상이 발생했을 때의 코드 실행은 this.m_continuationObject == null인 상태로, Interlocked.CompareExchange 코드에 의해 tc (즉, SetOnInvokeMres 인스턴스) 객체를 this.m_continuationObject 에 할당하는 것으로 마무리를 지은 후 true를 반환합니다.

최종적으로는 결국, this.m_continuationObject 객체의 Invoke 메서드가 호출되지 않았기 때문에 발생하는 것입니다. 그렇다면 해당 객체의 Invoke 메서드는 언제 발생하는 것일까요? 추적해 보면 특별한 예외 상황이 발생하지 않는 경우를 제외한다면,

[System.Threading.Tasks.Task]

        [SecuritySafeCritical]
        internal void FinishContinuations()
        {
            object obj2 = Interlocked.Exchange(ref this.m_continuationObject, s_taskCompletionSentinel);
            TplEtwProvider.Log.RunningContinuation(this.Id, obj2);
            if (obj2 != null)
            {
                if (AsyncCausalityTracer.LoggingOn)
                {
                    AsyncCausalityTracer.TraceSynchronousWorkStart(CausalityTraceLevel.Required, this.Id, CausalitySynchronousWork.CompletionNotification);
                }
                bool allowInlining = (((this.m_stateFlags & 0x8000000) == 0) && (Thread.CurrentThread.ThreadState != ThreadState.AbortRequested)) && ((this.m_stateFlags & 0x40) == 0);
                Action action = obj2 as Action;
                if (action != null)
                {
                    AwaitTaskContinuation.RunOrScheduleAction(action, allowInlining, ref t_currentTask);
                    this.LogFinishCompletionNotification();
                }
                else
                {
                    ITaskCompletionAction action2 = obj2 as ITaskCompletionAction;
                    if (action2 != null)
                    {
                        action2.Invoke(this);
                        this.LogFinishCompletionNotification();
                    }
                    else
                    {
                        // ...[생략]...
                    }
                }
            }
        }

FinishContinuations 메서드가 나오고, 그것을 호출하는 메서드들은 FinishStageThree, FinishStageTwo를 이어 Finish 메서드까지 이어갑니다.

[System.Threading.Tasks.Task]

        internal void FinishStageThree()
        {
            this.m_action = null;
            if (((this.m_parent != null) && ((this.m_parent.CreationOptions & TaskCreationOptions.DenyChildAttach) == TaskCreationOptions.None)) && (((this.m_stateFlags & 0xffff) & 4) != 0))
            {
                this.m_parent.ProcessChildCompletion(this);
            }
            this.FinishContinuations();
        }
        
        internal void FinishStageTwo()
        {
            int num;
            this.AddExceptionsFromChildren();
            // ...[생략]...
            Interlocked.Exchange(ref this.m_stateFlags, this.m_stateFlags | num);
            ContingentProperties contingentProperties = this.m_contingentProperties;
            if (contingentProperties != null)
            {
                contingentProperties.SetCompleted();
                contingentProperties.DeregisterCancellationCallback();
            }
            this.FinishStageThree();
        }
        
        internal void Finish(bool bUserDelegateExecuted)
        {
            if (!bUserDelegateExecuted)
            {
                this.FinishStageTwo();
            }
            else
            {
                ContingentProperties contingentProperties = this.m_contingentProperties;
                if (((contingentProperties == null) || ((contingentProperties.m_completionCountdown == 1) && !this.IsSelfReplicatingRoot)) || (Interlocked.Decrement(ref contingentProperties.m_completionCountdown) == 0))
                {
                    this.FinishStageTwo();
                }
                else
                {
                    this.AtomicStateUpdate(0x800000, 0x1600000);
                }
                // ...[생략]...
            }
        }

마지막 최상위 호출은 ExecuteWithThreadLocal에서 이뤄집니다.

[System.Threading.Tasks.Task]

        [SecurityCritical]
        private void ExecuteWithThreadLocal(ref Task currentTaskSlot)
        {
            Task task = currentTaskSlot;
            TplEtwProvider log = TplEtwProvider.Log;
            Guid oldActivityThatWillContinue = new Guid();
            bool flag = log.IsEnabled();
            if (flag)
            {
                if (log.TasksSetActivityIds)
                {
                    EventSource.SetCurrentThreadActivityId(TplEtwProvider.CreateGuidForTaskID(this.Id), out oldActivityThatWillContinue);
                }
                if (task != null)
                {
                    log.TaskStarted(task.m_taskScheduler.Id, task.Id, this.Id);
                }
                else
                {
                    log.TaskStarted(TaskScheduler.Current.Id, 0, this.Id);
                }
            }
            if (AsyncCausalityTracer.LoggingOn)
            {
                AsyncCausalityTracer.TraceSynchronousWorkStart(CausalityTraceLevel.Required, this.Id, CausalitySynchronousWork.Execution);
            }
            try
            {
                currentTaskSlot = this;
                ExecutionContext capturedContext = this.CapturedContext;
                if (capturedContext == null)
                {
                    this.Execute();
                }
                else
                {
                    if (this.IsSelfReplicatingRoot || this.IsChildReplica)
                    {
                        this.CapturedContext = CopyExecutionContext(capturedContext);
                    }
                    ContextCallback callback = s_ecCallback;
                    if (callback == null)
                    {
                        s_ecCallback = callback = new ContextCallback(Task.ExecutionContextCallback);
                    }
                    ExecutionContext.Run(capturedContext, callback, this, true);
                }
                if (AsyncCausalityTracer.LoggingOn)
                {
                    AsyncCausalityTracer.TraceSynchronousWorkCompletion(CausalityTraceLevel.Required, CausalitySynchronousWork.Execution);
                }
                this.Finish(true);
            }
            finally
            {
                currentTaskSlot = task;
                if (flag)
                {
                    if (task != null)
                    {
                        log.TaskCompleted(task.m_taskScheduler.Id, task.Id, this.Id, this.IsFaulted);
                    }
                    else
                    {
                        log.TaskCompleted(TaskScheduler.Current.Id, 0, this.Id, this.IsFaulted);
                    }
                    if (log.TasksSetActivityIds)
                    {
                        EventSource.SetCurrentThreadActivityId(oldActivityThatWillContinue);
                    }
                }
            }
        }

ExecuteWithThreadLocal이 실행될 때의 callstack을 보면,

mscorlib.dll!System.Threading.Tasks.Task.ExecuteWithThreadLocal(ref System.Threading.Tasks.Task currentTaskSlot) Line 1003   
mscorlib.dll!System.Threading.Tasks.Task.ExecuteEntry(bool bPreventDoubleExecution) Line 929     
mscorlib.dll!System.Threading.Tasks.Task.System.Threading.IThreadPoolWorkItem.ExecuteWorkItem() Line 2217    
mscorlib.dll!System.Threading.ThreadPoolWorkQueue.Dispatch() Line 119    
mscorlib.dll!System.Threading._ThreadPoolWaitCallback.PerformWaitCallback() Line 12  

_ThreadPoolWaitCallback.PerformWaitCallback이 루트인데, 결국 ThreadPool의 작업자 스레드의 하나로 실행된 것입니다. 이 스레드는 await로 인해 최초 비동기 호출을 했을 때의 바로 그 스레드입니다.

public async Task<string> GetHtmlTextAsync()
{
    string result = await GetTextAsync();
    return result;
}

여기까지 정리한 것을 우리가 만들었던 C# 소스코드에 적용해 설명해 보면, 대충 이렇습니다.

private void Form1_Load(object sender, EventArgs e)
{
    var textTask = GetHtmlTextAsync(); // testTask는 Task 인스턴스이고,
              // testTask 인스턴스에 할당된 스레드는 _ThreadPoolWaitCallback.PerformWaitCallback ~ ExecuteWithThreadLocal 호출의 작업자 스레드임
    this.Text = textTask.Result;
              // 이곳에서 action.Wait의 호출로 Task 작업자 스레드가 모든 작업이 완료될때까지 대기.
}

public async Task<string> GetHtmlTextAsync()
{
    string result = await GetTextAsync(); // _ThreadPoolWaitCallback.PerformWaitCallback 작업자 스레드에 의해
                    // Task.Factory.StartNew에 전달된 익명 함수를 실행하고,

                    // 나머지 다음의 작업을,
                    //    string result = ...
                    //    return result;
                    // _ThreadPoolWaitCallback.PerformWaitCallback을 실행하던 작업자 스레드가 마저 실행해야 함.
                    // 그런데, 윈도우 폼 응용 프로그램에는 미리 설정된 SynchronizationContext가 있으므로,
                    // _ThreadPoolWaitCallback.PerformWaitCallback을 실행하던 작업자 스레드가 나머지 작업을 직접 실행하지 못하고,
                    // WindowsFormsSynchronizationContext.Post로 전달하고,
                    // 이 단계에서 Task의 작업자 스레드는 모든 처리를 완료했으므로 스레드 풀에 반환됨.
    return result;                      
}

public Task<string> GetTextAsync()
{
    return Task.Factory.StartNew(
    () =>
    {
        Thread.Sleep(1000);
        return "Hello World";
    });
}

자, 여기가 문제입니다. 저는 WindowsFormsSynchronizationContext의 Post가 아닌 Send로 했기 때문에 블록킹이 발생했을 거라고 예상했는데요. 반면, "SynchronizationContext와 async/await - 4. async/await 궁합" 글에서는 await은 Post로 전달한다고 쓰여 있는 것입니다. 확인은 WindowsFormsSynchronizationContext.Post 메서드에 BP를 거는 것으로 금방 알 수 있습니다. 그리고, 결과는 실제로 ^^; Send가 아닌 Post가 실행되었습니다.

synch_context_1.png

그렇습니다. Send가 아닌 Post가 맞습니다.




아니, 그럼 아래의 글에서 설명한 hang 현상의 원인은 도대체 무엇입니까?

async/await 사용 시 hang 문제가 발생하는 경우
; https://www.sysnet.pe.kr/2/0/1541

결론먼저 말하면 이 원인은, Send/Post의 문제가 아니고 어찌되었든 "this.Text = textTask.Result;" 코드의 호출로 인해 UI 스레드가 blocking이 되면서 WindowsFormsSynchronizationContext에서 Post로 전달해 준 await 이후의 분리된 코드를 호출하지 못해서 발생하는 것입니다. 이는 WindowsFormsSynchronizationContext.Post의 호출에 전달된 SendOrPostCallback 델리게이트 타입인 d 인자의 값을 보면 어느 정도 추측할 수 있습니다. 위의 그림에서 보면 "d.Target" 인자의 값이 "System.Threading.Tasks.SynchronizationContextAwaitTaskContinuation" 임을 알 수 있는데, 이때의 코드가 바로 await 이후로 분리된 코드인 것입니다. (정확히는, 분리된 코드는 MoveNext의 switch 코드의 하나이고, 그 MoveNext를 호출하고 Task.Finish를 호출해줄 코드가 담긴 "Void <_cctor>b__3(System.Object)" 메서드입니다.)

문제의 시나리오를 정리해 보면 이렇게 됩니다.

1. UI Thread가 Form1_Load를 실행
    1.1. UI Thread가 GetHtmlTextAsync 메서드를 실행
        1.1.1 UI Thread가 TestClass.GetTextAsync 메서드를 비동기로 실행
    1.2. UI Thread는 TestClass.GetTextAsync 메서드에 반환된 Task 인스턴스인 textTask의 get_Result 프로퍼티를 호출.
         내부적으로 Task 타입의 Result는 해당 Task가 담당한 모든 작업이 끝날 때 까지 대기
         여기서 Task가 담당한 작업은 == await 대상 코드 + await 이후의 코드

2. ThreadPool의 자유 스레드가 TestClass.GetTextAsync 메서드에서 반환한 Task에 할당됨.
    2.1. Task.Factory.StartNew에 넘겨진 익명 메서드를 실행
    2.2. 메서드 실행 후 결과 값 반환
    2.3. await 처리로 인해 분리된 "return result;" 코드를 SynchronizationContextAwaitTaskContinuation 델리게이트로 WindowsFormsSynchronizationContext.Post에 전달
    2.4  전달된 SynchronizationContextAwaitTaskContinuation 델리게이트 코드까지 실행되어야만 Task의 모든 작업이 완료되는데,
         UI Thread는 현재 Task 타입의 Result 호출로 인해 블록 상태이므로, WindowsFormsSynchronizationContext가 전달한 델리게이트를 실행하지 못함.

휴~~~ 이렇게 정리하고 나니, 속이 시원하군요. ^^ 암튼, "SynchronizationContext와 async/await - 4. async/await 궁합 " 글 덕분에 문제를 좀 더 바로 보게 되어 다행입니다.




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

[연관 글]






[최초 등록일: ]
[최종 수정일: 3/5/2024]

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

비밀번호

댓글 작성자
 



2015-06-01 04시55분
멋진 정리 감사합니다. async/await 의 사용에 이런 문제가 있었던지는 몰랐습니다.
모르는 상태에서 처음 보고 당황하게 될 많은 이들을(저 포함) 구제해주는 글이 아닐까 싶습니다.
Beren Ko
2015-06-01 03시24분
결국, 이 모든 문제의 근원은... 'UI를 만든 스레드만이 UI를 접근해야 한다'라서, 이런 속내를 알지 못하면 그야말로 황당한 hang 현상일 수 있습니다. (아마 모르긴 해도... 마이크로소프트도 이 기능을 넣으면서 꽤나 고민했을 듯합니다. ^^)
정성태
2024-05-02 09시06분
Detecting blocking calls using async in C#
; https://www.jamescrosswell.dev/posts/async-detecting-blocking-calls
; https://github.com/benaadams/Ben.BlockingDetector/blob/main/src/Ben.BlockingDetector/TaskBlockingListener.cs

SynchronizationContext를 사용하던 ASP.NET (.NET Framework) 시절까지만 해도 DetectBlockingSynchronizationContext를 교체해 넣어서 blocking 호출을 잡아낼 수 있었지만, 그것이 없는 ASP.NET Core부터는 그것을 감지하기 위해 EventListener를 활용한 TaskBlockingListener를 이용하는 방법을 소개하고 있습니다.
정성태

[1]  2  3  4  5  6  7  8  9  10  11  12  13  14  15  ...
NoWriterDateCnt.TitleFile(s)
13646정성태6/14/2024271오류 유형: 908. Process Explorer - "Error configuring dump resources: The system cannot find the file specified."
13645정성태6/13/2024513개발 환경 구성: 711. Visual Studio로 개발 시 기본 등록하는 dev tag 이미지로 Docker Desktop k8s에서 실행하는 방법
13644정성태6/12/2024548닷넷: 2265. C# - System.Text.Json의 기본적인 (한글 등에서의) escape 처리
13643정성태6/12/2024530오류 유형: 907. MySqlConnector 사용 시 System.IO.FileLoadException 오류
13642정성태6/11/2024537스크립트: 65. 파이썬 - asgi 버전(2, 3)에 따라 달라지는 uvicorn 호스팅
13641정성태6/11/2024736Linux: 71. Ubuntu 20.04를 22.04로 업데이트
13640정성태6/10/2024892Phone: 21. C# MAUI - Android 환경에서의 파일 다운로드(DownloadManager)
13639정성태6/8/2024830오류 유형: 906. C# MAUI - Android Emulator에서 "Waiting For Debugger"로 무한 대기
13638정성태6/8/2024963오류 유형: 905. C# MAUI - 추가한 layout XML 파일이 Resource.Layout 멤버로 나오지 않는 문제
13637정성태6/6/20241079Phone: 20. C# MAUI - 유튜브 동영상을 MediaElement로 재생하는 방법
13636정성태5/30/20241101닷넷: 2264. C# - 형식 인자로 인터페이스를 갖는 제네릭 타입으로의 형변환파일 다운로드1
13635정성태5/29/2024930Phone: 19. C# MAUI - 안드로이드 "Share" 대상으로 등록하는 방법
13634정성태5/24/20241187Phone: 18. C# MAUI - 안드로이드 플랫폼에서의 Activity 제어
13633정성태5/22/20241132스크립트: 64. 파이썬 - ASGI를 만족하는 최소한의 구현 코드
13632정성태5/20/20241236Phone: 17. C# MAUI - Android 내에 Web 서비스 호스팅
13631정성태5/19/20241232Phone: 16. C# MAUI - /Download 등의 공용 디렉터리에 접근하는 방법
13630정성태5/19/20241588닷넷: 2263. C# - Thread가 Task보다 더 빠르다는 어떤 예제(?)
13629정성태5/18/20241408개발 환경 구성: 710. Android - adb.exe를 이용한 파일 전송
13628정성태5/17/20241396개발 환경 구성: 709. Windows - WHPX(Windows Hypervisor Platform)를 이용한 Android Emulator 가속
13627정성태5/17/20241378오류 유형: 904. 파이썬 - UnicodeEncodeError: 'ascii' codec can't encode character '...' in position ...: ordinal not in range(128)
13626정성태5/15/20241515Phone: 15. C# MAUI - MediaElement Source 경로 지정 방법파일 다운로드1
13625정성태5/14/20241788닷넷: 2262. C# - Exception Filter 조건(when)을 갖는 catch 절의 IL 구조
13624정성태5/12/20241979Phone: 14. C# - MAUI에서 MediaElement 사용파일 다운로드1
13623정성태5/11/20242145닷넷: 2261. C# - 구글 OAuth의 JWT (JSON Web Tokens) 해석파일 다운로드1
13622정성태5/10/20242065닷넷: 2260. C# - Google 로그인 연동 (ASP.NET 예제)파일 다운로드1
[1]  2  3  4  5  6  7  8  9  10  11  12  13  14  15  ...