Microsoft MVP성태의 닷넷 이야기
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
(연관된 글이 1개 있습니다.)
(시리즈 글이 12개 있습니다.)
.NET Framework: 811. (번역글) .NET Internals Cookbook Part 1 - Exceptions, filters and corrupted processes
; https://www.sysnet.pe.kr/2/0/11838

.NET Framework: 816. (번역글) .NET Internals Cookbook Part 2 - GC-related things
; https://www.sysnet.pe.kr/2/0/11869

.NET Framework: 818. (번역글) .NET Internals Cookbook Part 3 - Initialization tricks
; https://www.sysnet.pe.kr/2/0/11871

.NET Framework: 819. (번역글) .NET Internals Cookbook Part 4 - Type members
; https://www.sysnet.pe.kr/2/0/11872

.NET Framework: 820. (번역글) .NET Internals Cookbook Part 5 - Methods, parameters, modifiers
; https://www.sysnet.pe.kr/2/0/11873

.NET Framework: 821. (번역글) .NET Internals Cookbook Part 6 - Object internals
; https://www.sysnet.pe.kr/2/0/11874

.NET Framework: 822. (번역글) .NET Internals Cookbook Part 7 - Word tearing, locking and others
; https://www.sysnet.pe.kr/2/0/11876

.NET Framework: 823. (번역글) .NET Internals Cookbook Part 8 - C# gotchas
; https://www.sysnet.pe.kr/2/0/11877

.NET Framework: 824. (번역글) .NET Internals Cookbook Part 9 - Finalizers, queues, card tables and other GC stuff
; https://www.sysnet.pe.kr/2/0/11878

.NET Framework: 825. (번역글) .NET Internals Cookbook Part 10 - Threads, Tasks, asynchronous code and others
; https://www.sysnet.pe.kr/2/0/11879

.NET Framework: 826. (번역글) .NET Internals Cookbook Part 11 - Various C# riddles
; https://www.sysnet.pe.kr/2/0/11882

.NET Framework: 831. (번역글) .NET Internals Cookbook Part 12 - Memory structure, attributes, handles
; https://www.sysnet.pe.kr/2/0/11891




(번역글) .NET Internals Cookbook Part 7 - Word tearing, locking and others


이번에도 .NET Internals Cookbook 시리즈의 7번째 글을 번역한 것입니다.

.NET Internals Cookbook Part 7 - Word tearing, locking and others
; https://blog.adamfurmanek.pl/2019/03/30/net-internals-cookbook-part-7/





45. 그래도 박싱(boxing)이 필요한 경우?

박싱은 스택의 데이터를 힙에 중복해서 올리기 때문에 분명히 성능상 좋은 작업은 아닙니다. 하지만, 필드를 많이 가진 값 형식의 인스턴스를 다룰 때 값의 치환을 스레드에 안정적으로 다루는 용도로 사용할 수 있습니다.

가령 다음과 같은 struct를,

struct Foo
{
    public int A { get; set; }
    public int B { get; set; }
    public int C { get; set; }
    public int D { get; set; }
    public int E { get; set; }
}

보유한 클래스가 있다고 가정해 보겠습니다.

public class Test
{
    Foo _foo = new Foo();

    public void ChangeFoo(Foo foo)
    {
        _foo = foo;
    }

    public void WriteValues()
    {
        Console.WriteLine(_foo.A);
        Console.WriteLine(_foo.E);
    }
}

위에서 _foo = foo 대입문은 5개의 int 필드에 대한 값 복사가 발생합니다. 즉, 코드는 한 줄이지만 실제로는 다음과 같은 코드가 실행되는 것과 같습니다.

_foo.A = foo.A;
_foo.B = foo.B;
_foo.C = foo.C;
_foo.D = foo.D;
_foo.E = foo.E;

일반적인 경우 저런 코드가 문제 되지 않지만 만약 다중 스레드 상황으로 바뀌면 의도치 않은 버그가 발생할 수 있습니다. 가령 A 스레드가 foo 구조체의 A, B까지 값을 대입한 상황에서 B 스레드가 WriteValues 메서드를 호출해 A와 E 멤버를 접근하면 foo가 _foo에 완전하게 대입되기도 전에 값을 접근하는 결과가 됩니다.

저런 경우를 방지하기 위해서는 Foo를 class로 바꾸고 다음과 같은 식으로 코딩하면 됩니다.

public class Test
{
    Foo _foo = new Foo();

    public void ChangeFoo(Foo foo)
    {
        _foo = foo;
    }

    public void WriteValues()
    {
        Foo temp = _foo;

        Console.WriteLine(temp.A);
        Console.WriteLine(temp.E);
    }
}

참조 형식의 경우 GC Heap을 가리키는 주솟값(x86 4바이트, x64 8바이트)에 불과하기 때문에 대입 과정이 atomic하게 처리되기 때문입니다.

또는, class로 바꾸지 않고 의도적으로 필요한 순간에만 박싱을 해 주솟값 대입으로 변경하는 방법도 있습니다.

public class Test
{
    object _foo = new Foo();

    public void ChangeFoo(Foo foo)
    {
        _foo = foo;
    }

    public void WriteValues()
    {
        object obj = _foo;
        Foo temp = (Foo)obj;

        Console.WriteLine(temp.A);
        Console.WriteLine(temp.E);
    }
}

위와 같은 특징을 감안하고 원 글(.NET Internals Cookbook Part 7 ? Word tearing, locking and others)의 예제 코드를 보면,

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

public class Program
{
    public static void Main()
    {
        var source = Enumerable.Range(0, 1000).ToArray();
        while(true)
        {
            Foo result = UnsafeParallel(source, n => {
                Thread.Sleep(0);
                return new Foo { A = n, B = n, C = n, D = n, E = n };
            });

            if (result.A != result.B || result.A != result.C || result.A != result.D || result.A != result.E)
            {
                Console.WriteLine("Tearing detected!");
                Console.WriteLine(result.A);
                Console.WriteLine(result.B);
                Console.WriteLine(result.C);
                Console.WriteLine(result.D);
                Console.WriteLine(result.E);
                break;
            }
        }
    }

    static T UnsafeParallel(IEnumerable source, Func action)
    {
        T result = default(T);
        Parallel.ForEach(source, (i, state) => {
            result = action(i);
            state.Stop();
        });

        return result;
    }


    static T SafeParallel(IEnumerable source, Func action)
    {
        object result = default(T);
        Parallel.ForEach(source, (i, state) => {
            result = action(i);
            state.Stop();
        });

        return (T)result;
    }
}

왜 UnsafeParallel에서는 문제가 발생하고, SafeParallel에서는 문제가 발생하지 않는지 이해할 수 있을 것입니다.





46. 다중 연결된 delegate의 경우 중간 호출에서 예외가 발생한다면?

예를 들어 다음의 코드를 보면,

using System;

public class Program
{
    delegate void Foo();
    public static void Main()
    {
        Foo foo = () => Console.WriteLine("First");
        foo += () => throw new Exception("Second");
        foo += () => Console.WriteLine("Third");

        try
        {
            foo();
        }
        catch (Exception e)
        {
            Console.WriteLine(e);
        }
    }
}

/* 출력 결과
First
System.Exception: Second
   at Program.<>c.<Main>b__1_1() in C:\temp\cookbook_series7_sample\q46\ConsoleApp1\Program.cs:line 9
   at Program.Foo.Invoke()
   at Program.Main() in C:\temp\cookbook_series7_sample\q46\ConsoleApp1\Program.cs:line 14
*/

두 번째 연결된 delegate 호출에서 예외가 발생하므로 그 이후의 콜백 호출이 안 되는 것을 볼 수 있습니다. 만약 예외를 감안해 모든 delegate를 호출하고 싶다면 다음의 글에서 설명한,

올바른 이벤트 예외 정보 출력
; https://www.sysnet.pe.kr/2/0/620

GetInvocationList 메서드를 이용해 하나씩 try/catch로 감쌀 수 있습니다.





47. foreach 문을 사용하려면 IEnumerator/IEnumerable 인터페이스를 반드시 구현해야 할까?

다음의 코드에서 보듯이,

using System;

public class Program
{
    public static void Main()
    {
        var bar = new Bar();

        foreach (int item in bar)
        {
            Console.WriteLine(item);
        }
    }
}

class Foo
{
    public int Current { get; private set; }
    private int step;
    public bool MoveNext()
    {
        if (step >= 5) return false;
        Current = step++;
        return true;
    }
}

class Bar
{
    public Foo GetEnumerator() { return new Foo(); }
}

IEnumerator/IEnumerable 인터페이스를 굳이 명시하지 않아도 관련 속성과 메서드를 가지는 것만으로 C# 컴파일러는 foreach에 해당 멤버들을 사용해 코드를 생성해 줍니다.

이 규칙은 비동기 호출에 관련된 await에도 적용되는데 다음의 코드를 보면 정숫값(2000)을 await할 수 있도록 확장 메서드를 연결하고 있습니다.

using System;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;

namespace AwaitOnInteger
{
    class Program
    {
        static void Main(string[] args)
        {
            WaitForInt().Wait();
        }

        static async Task WaitForInt()
        {
            Console.WriteLine($"Waiting starting at {DateTime.Now}");
            await 2000;
            Console.WriteLine($"Waiting finished at {DateTime.Now}");

        }
    }

    public static class AwaitableInt
    {
        public static TaskAwaiter GetAwaiter(this int miliseconds)
        {
            return Task.Delay(TimeSpan.FromMilliseconds(miliseconds)).GetAwaiter();
        }
    }
}

위의 규칙에 대해서는 저도 예전에 ^^ 설명한 적이 있습니다.

C# - await을 Task 타입이 아닌 사용자 정의 타입에 적용하는 방법
; https://www.sysnet.pe.kr/2/0/11456





48. LINQ 쿼리의 동작과 컴파일 방식

SQL 쿼리 구문처럼 보이는 LINQ는,

using System;
using System.Linq;

namespace Program
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var source = Enumerable.Range(0, 100);
            var filtered = from i in source 
                           where i % 3 == 0 
                           select i / 2;
        }
    }
}

빌드하면 결국 관련된 IEnumerable의 확장 메서드로 번역이 된다는 것을 .NET Reflector 도구 등으로 확인해 보면 알 수 있습니다.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
namespace Program
{
    public class Program
    {
        [CompilerGenerated]
        [Serializable]
        private sealed class <>c
        {
            public static readonly Program.<>c <>9 = new Program.<>c();
            public static Func <>9__0_0;
            public static Func <>9__0_1;
            internal bool b__0_0(int i)
            {
                return i % 3 == 0;
            }
            internal int b__0_1(int i)
            {
                return i / 2;
            }
        }
        public static void Main(string[] args)
        {
            IEnumerable enumerable = Enumerable.Range(0, 100);
            IEnumerable arg_2A_0 = enumerable;
            Func arg_2A_1;
            if ((arg_2A_1 = Program.<>c.<>9__0_0) == null)
            {
                arg_2A_1 = (Program.<>c.<>9__0_0 = new Func(Program.<>c.<>9.b__0_0));
            }
            IEnumerable arg_4E_0 = arg_2A_0.Where(arg_2A_1);
            Func arg_4E_1;
            if ((arg_4E_1 = Program.<>c.<>9__0_1) == null)
            {
                arg_4E_1 = (Program.<>c.<>9__0_1 = new Func(Program.<>c.<>9.b__0_1));
            }
            IEnumerable enumerable2 = arg_4E_0.Select(arg_4E_1);
        }
    }
}





49. IEnumerable.Select와 IQueryable.Select의 차이점

메서드 시그니처를 보면,

[IEnumerable.Select]
public static System.Collections.Generic.IEnumerable Select (this System.Collections.Generic.IEnumerable source, Func selector);

[IQueryable.Select]
public static System.Linq.IQueryable Select (this System.Linq.IQueryable source, System.Linq.Expressions.Expression> selector);

IEnumerable 측은 람다를 코드(Func)로 받아들여 실행시키는 반면 IQueryable의 경우 람다를 데이터(System.Linq.Expressions.Expression)로 받아들여 분석해 실행 시 SQL 쿼리로 번역합니다.





50. Reflection으로 private 멤버를 접근하지 못하도록 막는 방법

.NET 4.6/.NET Core 1.0부터 제공되는 DisablePrivateReflectionAttribute 특성을 어셈블리 수준에 부여하거나,

[assembly: DisablePrivateReflection]

ReflectionPermission을 사용하면 된다고 합니다.

그런데, 실제로 해보면 .NET Framework 응용 프로그램에서는 동작하지 않고, .NET Core에서만 동작합니다. 예를 들어 다음의 코드를 .NET Standard 라이브러리로 만들고,

using System;
using System.Runtime.CompilerServices;

[assembly: DisablePrivateReflection]

public class Class1
{
    private int i = 5;

    public void Print()
    {
        Console.WriteLine(i);
    }
}

.NET Core 콘솔에서 다음과 같이 Class1을 사용하면,

using System;
using System.Reflection;

class Program
{
    static void Main(string[] args)
    {
        Class1 cl = new Class1();
        cl.Print();

        SetPrivate(cl, 6);
        cl.Print();
    }

    private static void SetPrivate(Class1 cl, int v)
    {
        Type type = cl.GetType();

        FieldInfo fi = type.GetField("i", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
        if (fi == null)
        {
            return;
        }

        fi.SetValue(cl, v);
    }
}

SetValue 메서드 수행에서 다음과 같은 예외가 발생합니다.

Unhandled Exception: System.FieldAccessException: Attempt by method 'Program.SetPrivate(Class1, Int32)' to access field 'Class1.i' failed.
   at System.Reflection.RtFieldInfo.PerformVisibilityCheckOnField(IntPtr field, Object target, RuntimeType declaringType, FieldAttributes attr, UInt32 invocationFlags)
   at System.Reflection.RtFieldInfo.InternalSetValue(Object obj, Object value, BindingFlags invokeAttr, Binder binder, CultureInfo culture, StackCrawlMark& stackMark)
   at System.Reflection.RtFieldInfo.SetValue(Object obj, Object value, BindingFlags invokeAttr, Binder binder, CultureInfo culture)
   at System.Reflection.FieldInfo.SetValue(Object obj, Object value)
   at Program.SetPrivate(Class1 cl, Int32 v) in C:\temp\NetCoreCon1\Program.cs:line 25
   at Program.Main(String[] args) in C:\temp\NetCoreCon1\Program.cs:line 11

반면 동일한 코드를 .NET Framework 응용 프로그램에서 실행하면 예외가 발생하지 않습니다.





51. lock 구문에 값 형식(value type)의 인스턴스를 사용할 수 있을까?

아래의 글에서도 설명했지만,

.NET 참조 개체 인스턴스의 Object Header를 확인하는 방법
; https://www.sysnet.pe.kr/2/0/1175

값 형식은 Object Header가 없기 때문에 lock 구문으로 사용할 수 없습니다. 그래서 다음과 같이 lock에 사용하려고 시도하면 컴파일 오류가 발생합니다.

using System;

public class Program
{
    public static void Main(string[] args)
    {
        int i = 5;

        // 컴파일 에러
        // error CS0185: 'int' is not a reference type as required by the lock statement
        lock(i) {
        }
    }
}

lock 구문이 실제로는 Monitor.Enter 코드로 변경된다는 것을 지난 글에서 설명했는데요. 따라서 다음과 같이 풀어서 접근하는 것을 시도해 볼 수는 있습니다.

int i = 5;
bool wasTaken = false;
try
{
    Monitor.Enter(i, ref wasTaken);
}
finally
{
    if (wasTaken)
    {
        Monitor.Exit(i);
    }
}

이번엔 컴파일 오류는 발생하지 않지만, 실행 시 다음과 같은 오류가 발생합니다.

Unhandled Exception: System.Threading.SynchronizationLockException: Object synchronization method was called from an unsynchronized block of code.

왜냐하면, Monitor.Enter의 첫 번째 인자는 object 타입이므로 int i는 object로 박싱되어 컴파일을 통과할 수 있었지만, Monitor.Exit에 사용된 i도 그 순간 새롭게 object로 박싱된 것이므로 Enter와 Exit에 사용된 객체가 같지 않기 때문입니다. 결국 Exit(i)의 박싱된 object는 Enter에 사용된 적이 없으므로 Exit에서 해제 시 오류가 발생하는 것입니다.

(첨부 파일은 이 글의 예제 코드를 포함합니다.)



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

[연관 글]






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

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

비밀번호

댓글 작성자
 




... 16  17  18  [19]  20  21  22  23  24  25  26  27  28  29  30  ...
NoWriterDateCnt.TitleFile(s)
13147정성태10/25/20224761오류 유형: 823. Visual Studio 2022 - Unable to attach to CoreCLR. The debugger's protocol is incompatible with the debuggee.
13146정성태10/24/20225593.NET Framework: 2060. C# - Java의 Xmx와 유사한 힙 메모리 최댓값 제어 옵션 HeapHardLimit
13145정성태10/21/20225858오류 유형: 822. db2 - Password validation for user db2inst1 failed with rc = -2146500508
13144정성태10/20/20225715.NET Framework: 2059. ClrMD를 이용해 윈도우 환경의 메모리 덤프로부터 닷넷 모듈을 추출하는 방법파일 다운로드1
13143정성태10/19/20226226오류 유형: 821. windbg/sos - Error code - 0x000021BE
13142정성태10/18/20224953도서: 시작하세요! C# 12 프로그래밍
13141정성태10/17/20226709.NET Framework: 2058. [in,out] 배열을 C#에서 C/C++로 넘기는 방법 - 세 번째 이야기파일 다운로드1
13140정성태10/11/20226091C/C++: 159. C/C++ - 리눅스 환경에서 u16string 문자열을 출력하는 방법 [2]
13139정성태10/9/20225925.NET Framework: 2057. 리눅스 환경의 .NET Core 3/5+ 메모리 덤프로부터 모든 닷넷 모듈을 추출하는 방법파일 다운로드1
13138정성태10/8/20227211.NET Framework: 2056. C# - await 비동기 호출을 기대한 메서드가 동기로 호출되었을 때의 부작용 [1]
13137정성태10/8/20225601.NET Framework: 2055. 리눅스 환경의 .NET Core 3/5+ 메모리 덤프로부터 닷넷 모듈을 추출하는 방법
13136정성태10/7/20226175.NET Framework: 2054. .NET Core/5+ SDK 설치 없이 dotnet-dump 사용하는 방법
13135정성태10/5/20226405.NET Framework: 2053. 리눅스 환경의 .NET Core 3/5+ 메모리 덤프를 분석하는 방법 - 두 번째 이야기
13134정성태10/4/20225135오류 유형: 820. There is a problem with AMD Radeon RX 5600 XT device. For more information, search for 'graphics device driver error code 31'
13133정성태10/4/20225452Windows: 211. Windows - (commit이 아닌) reserved 메모리 사용량 확인 방법 [1]
13132정성태10/3/20225324스크립트: 42. 파이썬 - latexify-py 패키지 소개 - 함수를 mathjax 식으로 표현
13131정성태10/3/20227987.NET Framework: 2052. C# - Windows Forms의 데이터 바인딩 지원(DataBinding, DataSource) [2]파일 다운로드1
13130정성태9/28/20225090.NET Framework: 2051. .NET Core/5+ - 에러 로깅을 위한 Middleware가 동작하지 않는 경우파일 다운로드1
13129정성태9/27/20225390.NET Framework: 2050. .NET Core를 IIS에서 호스팅하는 경우 .NET Framework CLR이 함께 로드되는 환경
13128정성태9/23/20227965C/C++: 158. Visual C++ - IDL 구문 중 "unsigned long"을 인식하지 못하는 #import파일 다운로드1
13127정성태9/22/20226416Windows: 210. WSL에 systemd 도입
13126정성태9/15/20227030.NET Framework: 2049. C# 11 - 정적 메서드에 대한 delegate 처리 시 cache 적용
13125정성태9/14/20227236.NET Framework: 2048. C# 11 - 구조체 필드의 자동 초기화(auto-default structs)
13124정성태9/13/20226977.NET Framework: 2047. Golang, Python, C#에서의 CRC32 사용
13123정성태9/8/20227415.NET Framework: 2046. C# 11 - 멤버(속성/필드)에 지정할 수 있는 required 예약어 추가
13122정성태8/26/20227416.NET Framework: 2045. C# 11 - 메서드 매개 변수에 대한 nameof 지원
... 16  17  18  [19]  20  21  22  23  24  25  26  27  28  29  30  ...