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

비밀번호

댓글 작성자
 




... 61  62  63  64  65  66  67  68  [69]  70  71  72  73  74  75  ...
NoWriterDateCnt.TitleFile(s)
12211정성태4/27/202019266개발 환경 구성: 486. WSL에서 Makefile로 공개된 리눅스 환경의 C/C++ 소스 코드 빌드
12210정성태4/20/202020711.NET Framework: 903. .NET Framework의 Strong-named 어셈블리 바인딩 (1) - app.config을 이용한 바인딩 리디렉션 [1]파일 다운로드1
12209정성태4/13/202017415오류 유형: 614. 리눅스 환경에서 C/C++ 프로그램이 Segmentation fault 에러가 발생한 경우 (2)
12208정성태4/12/202015979Linux: 29. 리눅스 환경에서 C/C++ 프로그램이 Segmentation fault 에러가 발생한 경우
12207정성태4/2/202015835스크립트: 19. Windows PowerShell의 NonInteractive 모드
12206정성태4/2/202018447오류 유형: 613. 파일 잠금이 바로 안 풀린다면? - The process cannot access the file '...' because it is being used by another process.
12205정성태4/2/202015110스크립트: 18. Powershell에서는 cmd.exe의 명령어를 지원하진 않습니다.
12204정성태4/1/202015112스크립트: 17. Powershell 명령어에 ';' (semi-colon) 문자가 포함된 경우
12203정성태3/18/202017957오류 유형: 612. warning: 'C:\ProgramData/Git/config' has a dubious owner: '...'.
12202정성태3/18/202021210개발 환경 구성: 486. .NET Framework 프로젝트를 위한 GitLab CI/CD Runner 구성
12201정성태3/18/202018440오류 유형: 611. git-credential-manager.exe: Using credentials for username "Personal Access Token". [1]
12200정성태3/18/202018531VS.NET IDE: 145. NuGet + Github 라이브러리 디버깅 관련 옵션 3가지 - "Enable Just My Code" / "Enable Source Link support" / "Suppress JIT optimization on module load (Managed only)"
12199정성태3/17/202016175오류 유형: 610. C# - CodeDomProvider 사용 시 Unhandled Exception: System.IO.DirectoryNotFoundException: Could not find a part of the path '...\f2_6uod0.tmp'.
12198정성태3/17/202019533오류 유형: 609. SQL 서버 접속 시 "Cannot open user default database. Login failed."
12197정성태3/17/202018830VS.NET IDE: 144. .NET Core 콘솔 응용 프로그램을 배포(publish) 시 docker image 자동 생성 - 두 번째 이야기 [1]
12196정성태3/17/202015950오류 유형: 608. The ServicedComponent being invoked is not correctly configured (Use regsvcs to re-register).
12195정성태3/16/202018268.NET Framework: 902. C# - 프로세스의 모든 핸들을 열람 - 세 번째 이야기
12194정성태3/16/202020999오류 유형: 607. PostgreSQL - Npgsql.NpgsqlException: sorry, too many clients already
12193정성태3/16/202017919개발 환경 구성: 485. docker - SAP Adaptive Server Enterprise 컨테이너 실행 [1]
12192정성태3/14/202019947개발 환경 구성: 484. docker - Sybase Anywhere 16 컨테이너 실행
12191정성태3/14/202021054개발 환경 구성: 483. docker - OracleXE 컨테이너 실행 [1]
12190정성태3/14/202015634오류 유형: 606. Docker Desktop 업그레이드 시 "The process cannot access the file 'C:\Program Files\Docker\Docker\resources\dockerd.exe' because it is being used by another process."
12189정성태3/13/202021236개발 환경 구성: 482. Facebook OAuth 처리 시 상태 정보 전달 방법과 "유효한 OAuth 리디렉션 URI" 설정 규칙
12188정성태3/13/202026024Windows: 169. 부팅 시점에 실행되는 chkdsk 결과를 확인하는 방법
12187정성태3/12/202015604오류 유형: 605. NtpClient was unable to set a manual peer to use as a time source because of duplicate error on '...'.
12186정성태3/12/202017403오류 유형: 604. The SysVol Permissions for one or more GPOs on this domain controller and not in sync with the permissions for the GPOs on the Baseline domain controller.
... 61  62  63  64  65  66  67  68  [69]  70  71  72  73  74  75  ...