성태의 닷넷 이야기
홈 주인
모아 놓은 자료
프로그래밍
질문/답변
사용자 관리
사용자
메뉴
아티클
외부 아티클
유용한 코드
온라인 기능
MathJax 입력기
최근 덧글
[정성태] 그냥 RSS Reader 기능과 약간의 UI 편의성 때문에 사용...
[이종효] 오래된 소프트웨어는 보안 위협이 되기도 합니다. 혹시 어떤 기능...
[정성태] @Keystroke IEEE의 문서를 소개해 주시다니... +_...
[손민수 (Keystroke)] 괜히 듀얼채널 구성할 때 한번에 같은 제품 사라고 하는 것이 아...
[정성태] 전각(Full-width)/반각(Half-width) 기능을 토...
[정성태] Vector에 대한 내용은 없습니다. Vector가 닷넷 BCL...
[orion] 글 읽고 찾아보니 디자인 타임에는 InitializeCompon...
[orion] 연휴 전에 재현 프로젝트 올리자 생각해 놓고 여의치 않아서 못 ...
[정성태] 아래의 글에 정리했으니 참고하세요. C# - Typed D...
[정성태] 간단한 재현 프로젝트라도 있을까요? 저런 식으로 설명만 해...
글쓰기
제목
이름
암호
전자우편
HTML
홈페이지
유형
제니퍼 .NET
닷넷
COM 개체 관련
스크립트
VC++
VS.NET IDE
Windows
Team Foundation Server
디버깅 기술
오류 유형
개발 환경 구성
웹
기타
Linux
Java
DDK
Math
Phone
Graphics
사물인터넷
부모글 보이기/감추기
내용
<div style='display: inline'> <h1 style='font-family: Malgun Gothic, Consolas; font-size: 20pt; color: #006699; text-align: center; font-weight: bold'>UI 요소의 접근은 반드시 그 UI를 만든 스레드에서! - 두 번째 이야기</h1> <p> 아래와 같은 질문이 있군요. ^^<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > Winform에서 invoke없이 UI요소에 접근이 가능한가요? ; <a target='tab' href='https://forum.dotnetdev.kr/t/winform-invoke-ui/411'>https://forum.dotnetdev.kr/t/winform-invoke-ui/411</a> </pre> <br /> 질문을 정리하면 아래와 같은 코드를 Visual Studio에서 디버그 모드로 실행했을 때,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > using System; using System.Threading.Tasks; using System.Windows.Forms; namespace UIElementAccessNoneInvoke { public partial class Form1 : Form { public Form1() { InitializeComponent(); } private async void button1_Click(object sender, EventArgs e) { await Task.Run(() => { <span style='color: blue; font-weight: bold'>textBox1.Text = "test"; // InvalidOperationException 예외 발생</span> }); } } } </pre> <a name='ioe'></a> <br /> <a target='tab' href='https://www.sysnet.pe.kr/2/0/11561'>2차 스레드에서 textBox1 컨트롤의 요소를 접근하면 예외가 발생한다는 점</a>입니다. <br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > System.InvalidOperationException HResult=0x80131509 Message=Cross-thread operation not valid: Control 'textBox1' accessed from a thread other than the thread it was created on. Source=System.Windows.Forms StackTrace: at System.Windows.Forms.Control.get_Handle() This exception was originally thrown at this call stack: System.Windows.Forms.Control.Handle.get() </pre> <a name='check_call'></a> <br /> 반면, 그냥 실행하면 예외가 발생하지 않습니다. 그 이유를 알아볼까요? 이에 대해서는 Control 타입의 소스 코드를 보면 금방 알 수 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > static Control() { // ... [생략]... <span style='color: blue; font-weight: bold'>checkForIllegalCrossThreadCalls = Debugger.IsAttached;</span> // ... [생략]... } public static bool <a target='tab' href='https://learn.microsoft.com/en-us/dotnet/api/system.windows.forms.control.checkforillegalcrossthreadcalls'>CheckForIllegalCrossThreadCalls</a> { get { return checkForIllegalCrossThreadCalls; } set { checkForIllegalCrossThreadCalls = value; } } [Browsable(false)] [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] [DispId(-515)] [SRDescription("ControlHandleDescr")] public IntPtr Handle { get { if (<span style='color: blue; font-weight: bold'>checkForIllegalCrossThreadCalls</span> && !inCrossThreadSafeCall && InvokeRequired) { <span style='color: blue; font-weight: bold'>throw new InvalidOperationException</span>(SR.GetString("IllegalCrossThreadCall", Name)); } if (!IsHandleCreated) { CreateHandle(); } return HandleInternal; } } </pre> <br /> 보는 바와 같이 checkForIllegalCrossThreadCalls 필드의 값이 Control 정적 생성자가 초기화될 때 <a target='tab' href='https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.debugger.isattached'>Debugger.IsAttached</a> 값에 영향을 받기 때문입니다.<br /> <br /> 따라서, 만약 디버거가 붙어 있지 않은 상황에서도 - 가령 파일 탐색기에서 EXE 파일을 두 번 클릭해 실행하는 경우에도 InvalidOperationException 예외가 발생하도록 만들고 싶다면 다음과 같이 한 줄만 추가해 주면 됩니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > private async void button1_Click(object sender, EventArgs e) { <span style='color: blue; font-weight: bold'>Control.CheckForIllegalCrossThreadCalls = true;</span> await Task.Run(() => { <span style='color: blue; font-weight: bold'>textBox1.Text = "test"; // InvalidOperationException 예외 발생</span> }); } </pre> <br /> <hr style='width: 50%' /><br /> <br /> 그런데, 실행해 보면 알겠지만 예제와 같은 경우 딱히 컨트롤을 만들지 않은 다른 스레드에서 Text 설정을 하는 것이 아무런 문제가 없습니다. 그럼, 왜? 이렇게 강제로 막아버린 것일까요?<br /> <br /> 우선 윈도우 응용 프로그램의 원시적인 형태인 Win32 API를 사용하던 시절로 가볼까요? (WinForms/WPF도 마찬가지지만) 모든 윈도우 운영체제상의 프로그램은 Message Loop를 가지게 됩니다. <br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > About Messages and Message Queues ; <a target='tab' href='https://learn.microsoft.com/en-us/windows/win32/winmsg/about-messages-and-message-queues'>https://learn.microsoft.com/en-us/windows/win32/winmsg/about-messages-and-message-queues</a> </pre> <br /> 저 당시에는 사실 개발자의 역량에 따라 적절하게 윈도우 메시지를 사용하면 UI 스레드와 worker 스레드 간의 협업이 문제될 것이 없었습니다. 엄밀히 윈도우 메시지의 개념 자체는 Message Queue로 인해 직렬화가 되기 때문에 다중 스레드에서도 안전합니다. 실제로 COM 같은 경우에는 STA 개체의 사용을 thread-safe하게 접근할 수 있도록 함수 호출을 윈도우의 Message Queue로 해결하고 있습니다.<br /> <br /> 그런데 문제는, <a target='tab' href='https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-postmessagea#remarks'>윈도우 메시지 운영</a>이 단순하지 않다는 것입니다. <br /> <br /> <ul> <li>Send와 Post의 차이(<a target='tab' href='http://www.tipssoft.com/bulletin/board.php?bo_table=FAQ&wr_id=692'>SendMessage 와 PostMessage 함수에 대하여...</a>)</li> <li>다른 프로세스/스레드인 경우 Send 역시 Message Queue를 이용</li> <li>다른 프로세스인 경우 WM_USER 미만의 메시지는 시스템이 마샬링 담당</li> <li>같은 프로세스라고 해도 WM_USER 미만의 메시지는 포인터 인자를 사용해 PostMessage로 전달하면 실패 반환</li> <li>메시지 큐 최대 크기: 기본값 10,000 (HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Windows, "USERPostMessageLimit") 지정 가능한 최솟값은 4,000</li> <li>...</li> </ul> <br /> 저런 식의 규칙들은 윈도우상의 응용 프로그램 개발 환경이 어렵다는 인식을 만들기에 부족하지 않습니다. 이후 마이크로소프트는 MFC에서 그런 복잡성을 숨겨 윈도우 메시지보다는 가능한 해당 컨트롤 타입의 함수 호출로 래핑을 했습니다. C++ 클래스 기반으로 만든 MFC는 성능을 위해 <a target='tab' href='https://learn.microsoft.com/en-us/cpp/parallel/multithreading-programming-tips#_core_accessing_objects_from_multiple_threads'>멤버 함수들의 thread-safe을 보장하지 않았고</a>, 이후 UI 스레드에 대한 중요성이 부각됩니다. (MFC는 다중 스레드에서의 접근을 일부러 막기 위해 TLS까지 사용합니다.)<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > Multithreading: MFC Programming Tips - Windows Handle Maps ; <a target='tab' href='https://learn.microsoft.com/en-us/cpp/parallel/multithreading-programming-tips#_core_windows_handle_maps'>https://learn.microsoft.com/en-us/cpp/parallel/multithreading-programming-tips#_core_windows_handle_maps</a> </pre> <br /> <ul> <li>MFC objects are not thread-safe by themselves.</li> <li>As a general rule, a thread can access only MFC objects that it created.</li> </ul> <br /> 이와 유사하게, .NET Framework의 Windows Forms에서도 해당 타입들이 thread-safe하지 않습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > How to: Make thread-safe calls to Windows Forms controls ; <a target='tab' href='https://learn.microsoft.com/en-us/dotnet/desktop/winforms/controls/how-to-make-thread-safe-calls-to-windows-forms-controls'>https://learn.microsoft.com/en-us/dotnet/desktop/winforms/controls/how-to-make-thread-safe-calls-to-windows-forms-controls</a> </pre> <br /> <div style='BACKGROUND-COLOR: #ccffcc; padding: 10px 10px 5px 10px; MARGIN: 0px 10px 10px 10px; FONT-FAMILY: Malgun Gothic, Consolas, Verdana; COLOR: #005555'> Multithreading can improve the performance of Windows Forms apps, but access to Windows Forms controls isn't inherently thread-safe.<br /> </div><br /> <br /> 아울러, COM과 엮이면 문제가 더 복잡해집니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > WTF Reentrancy ; <a target='tab' href='https://blog.impulse101.org/posts/WTF_Reentrancy/'>https://blog.impulse101.org/posts/WTF_Reentrancy/</a> What exactly is a reentrant function? ; <a target='tab' href='https://stackoverflow.com/questions/2799023/what-exactly-is-a-reentrant-function'>https://stackoverflow.com/questions/2799023/what-exactly-is-a-reentrant-function</a> </pre> <br /> 하지만, thread-safe할 수 있는 동작임에도 그 모든 것을 일일이 구분 지을 수 없기 때문에 보수적으로 전체를 다 thread-safe하지 않은 걸로 단정 지은 면이 있습니다. 일례로, 이 글의 예제에서 다룬 "textBox1.Text" 속성은,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > // TextBox public override string Text { get { return base.Text; } set { base.Text = value; selectionSet = false; } } // TextBoxBase public override string Text { get { return base.Text; } set { if (value != base.Text) { base.Text = value; if (base.IsHandleCreated) { SendMessage(185, 0, 0); } } } } // Control public virtual string Text { get { // ...[생략]... } set { if (value == null) { value = ""; } if (value == Text) { return; } if (CacheTextInternal) { text = value; } WindowText = value; <span style='color: blue; font-weight: bold'>OnTextChanged</span>(EventArgs.Empty); if (!IsMnemonicsListenerAxSourced) { return; } for (Control control = this; control != null; control = control.ParentInternal) { <span style='color: blue; font-weight: bold'>ActiveXImpl</span> activeXImpl = (ActiveXImpl)control.Properties.GetObject(PropActiveXImpl); if (activeXImpl != null) { activeXImpl.UpdateAccelTable(); break; } } } } internal virtual string WindowText { get { // ...[생략]... } set { if (value == null) { value = ""; } <span style='color: blue; font-weight: bold'>if (!WindowText.Equals(value))</span> { if (IsHandleCreated) { UnsafeNativeMethods.SetWindowText(new HandleRef(window, Handle), value); } else if (value.Length == 0) { text = null; } else { text = value; } } } } </pre> <br /> 연관된 코드들 없이, UnsafeNativeMethods.SetWindowText 코드만 동작하는 수준이라면 thread-safe하다고 할 수 있지만 그 이외의 코드에 대한 제어를 일일이 개발자에게 맡길 수 없으므로 아예 Text 속성 자체를 thread-safe하지 않은 걸로 제약을 걸어둔 것입니다.<br /> <br /> 그리고 그 제약을 걸기 가장 좋은 곳이, UI를 가진 Window 프로그램이라면 항상 접근하게 될 HWND 값이므로 Control.Handle 속성에서 checkForIllegalCrossThreadCalls 필드를 이용해 체크하는 방법을 마이크로소프트는 선택합니다.<br /> <br /> 참고로, Windows Forms에서의 이러한 정책은 thread-safe하지 않다는 것 이외에도 주요하게는 dead-lock 문제를 막기 위한 것이 더 큽니다.<br /> </p><br /> <br /><hr /><span style='color: Maroon'>[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]</span> </div>
첨부파일
스팸 방지용 인증 번호
6533
(왼쪽의 숫자를 입력해야 합니다.)