동시성
글 32개
UniText 의 병렬은 세 조건을 모두 넘긴 프레임에만 켜진다
멀티코어 텍스트라는데 프로파일러의 워커는 대개 조용합니다. UniText 는 글자 합 500 초과, 컴포넌트 3개 이상, 논리 코어 3개 이상일 때만 상주 워커에 일을 나누고, 그 밖에는 메인 스레드에서 차례로 돕니다.
UniTask 의 에디터 할당은 코드 최적화가 만든다
에디터에서 UniTask 호출마다 찍히는 48 B 는 꺼진 코드 최적화가 상태 머신을 클래스로 만들어 생깁니다. 최적화를 켜면 0 이고, 그 뒤에도 남는 할당은 첫 대량 실행과 정상 경로 밖의 호출입니다.
UniTask 스트림은 당길 때만 값을 준다
UniTask 스트림은 소비자가 당길 때만 값을 주므로 바쁜 동안 온 값은 저절로 버려집니다. 유니티 메시지를 스트림으로 바꾸는 트리거는 도는 중에 목록이 바뀌어도 한 번씩 건네지만, 한 발행 안에 둘 이상 구독하면 하나가 떨어집니다.
UniTask 는 깨운 쪽 스레드에서 이어진다
UniTask 에는 원래 스레드로 되돌려 주는 장치가 없어, await 다음 줄은 완료를 알린 쪽 스레드에서 돕니다. 메인으로 돌아갈 자리는 예외 경로까지 코드에 직접 적고, 코루틴을 await 하면 UniTask 가 깨우는 쪽이 됩니다.
UniTask 의 예외는 꺼내 가야 보고된다
WhenAll 로 묶은 로딩이 둘 실패해도 catch 에는 하나만 오고, 버린 UniTask 의 예외는 한참 뒤에 찍힙니다. 완료는 처음 한 번만 이기고, 예외는 누군가 GetResult 로 꺼내 가야 보고되기 때문입니다.
UniTask 의 취소는 예외로 끝난다
UniTask 의 취소는 토큰을 받은 대기가 OperationCanceledException 으로 끝나는 일입니다. finally 는 돌지만 한 번마다 수백 바이트가 들고, 즉시 취소나 SuppressCancellationThrow 로 끝나면 대기 객체가 풀로 돌아가지 않습니다.
UniTask 의 대기는 무엇을 세느냐만 다르다
UniTask 의 대기는 모두 매 프레임 한 번 아직인가를 묻고 무엇을 세는지만 다릅니다. 그래서 Yield 는 같은 프레임에 끝날 수 있고, Delay 는 일시정지에 멈추거나 조금 일찍 끝납니다.
UniTask 는 PlayerLoop 에서 await 를 이어 준다
UniTask 는 첫 씬 전에 PlayerLoop 단계마다 맨 앞과 맨 뒤, 열여섯 자리에 큐와 러너를 꽂습니다. await 뒤 코드는 그 자리에서 메인 스레드로 이어지고, 언제 깨어나는지는 고른 타이밍이 정합니다.
UniTask 는 다 쓴 대기 객체를 풀에서 다시 쓴다
UniTask 대기를 반복해도 GC 가 없는 것은 다 쓴 대기 객체를 타입별 풀에 돌려 다시 쓰기 때문입니다. 풀은 할당도 락도 없고, 경합하면 기다리지 않고 포기하며, 가장 바빴던 순간만큼 자라서 줄지 않습니다.
UniTask 는 완료와 이어 부르기를 딱 한 번씩만 한다
시간 만료와 취소가 겹치거나 완료가 워커 스레드에서 와도 await 뒤 코드는 정확히 한 번 돕니다. 대기 객체 안의 구조체 하나가 원자적 연산으로 이것을 지키고, 할당은 없습니다.
UniTask 는 기다릴 때만 풀에서 빌려 GC 를 만들지 않는다
UniTask 의 async 메서드는 await 없이 끝나면 아무것도 만들지 않고, 정말 기다릴 때만 풀에서 러너를 빌렸다가 돌려줍니다. 이 0 은 최적화 컴파일에서만 성립하고, IL2CPP 에서는 반환이 프레임 끝으로 밀립니다.
UniTask 는 풀에서 빌린 객체로 await 를 처리한다
UniTask 값은 풀에서 빌린 대기 객체를 가리키는 참조 하나와 번호 하나입니다. 복사는 공짜지만 await 가 끝나면 그 객체는 풀로 돌아가므로 한 번만 기다릴 수 있습니다. 번호 검사와 Preserve 를 봅니다.
UniTask 는 코루틴의 빈칸을 메우고 Task 의 GC 를 없앤다
코루틴은 결과를 돌려주지 못하고, 멈추면 finally 가 돌지 않습니다. Task 로 옮기면 풀리지만 부를 때마다 GC 가 생깁니다. UniTask 는 async/await 를 그대로 쓰면서 그 할당과 유니티에 필요 없는 컨텍스트를 버렸습니다.
VitalRouter 의 Sequential 은 다른 클래스와도 줄을 선다
대사 Presenter 에만 Sequential 을 붙였는데 인벤토리 Presenter 와 함께 한 줄로 섭니다. VitalRouter 의 순서 제어는 발행 사이를 줄 세우는 인터셉터이고, 줄의 범위는 그 인스턴스를 나눠 갖는 범위이기 때문입니다.
VitalRouter 의 연속 발행은 백그라운드 스레드에서 멈출 수 있다
기다리는 구독자가 둘 이상인 커맨드를 백그라운드 스레드에서 연달아 발행하면 PublishAsync 가 영영 끝나지 않을 수 있습니다. VitalRouter 2.8.0 의 두 줄 순서가 원인이고, 유니티 메인 스레드에서는 걸리지 않았습니다.
MessagePipe 의 병렬 발행은 앞이 던지면 뒤를 시작하지 않는다
MessagePipe 의 PublishAsync 는 기본이 병렬이지만, 핸들러를 차례로 시작하다 곧바로 실패한 핸들러를 만나면 뒤는 시작하지 않습니다. async 여도 첫 await 전에 던지면 같고, 늦게 실패하면 나머지를 기다리지 않고 끝납니다.
MessagePipe 의 버퍼드와 키 브로커는 일반 브로커에 규칙 하나를 얹는다
MessagePipe 의 버퍼드 브로커와 키 브로커는 일반 구독 목록을 그대로 쓰고 규칙 하나만 바꿉니다. 마지막 값은 버퍼드 발행자만 적고, 키 목록은 비는 순간 지웁니다. 그래서 일반 발행은 재생되지 않고, 키 구독은 매번 할당합니다.
MessagePipe 의 동시 해제는 남의 구독을 지운다
같은 MessagePipe 구독을 두 스레드가 동시에 Dispose 하면 해제 검사가 락 밖이라 둘 다 통과합니다. 목록은 칸 번호만 보고 지우므로 KeyNotFoundException 이 나거나, 그사이 칸을 받은 다른 구독이 조용히 사라집니다.
MessagePipe 는 구독자를 칸 번호가 고정된 배열에 둔다
MessagePipe 의 발행이 락 없이 할당 0 으로 도는 것도, 호출 순서가 구독 순서와 다른 것도, 999명이 빠져도 1,024칸을 훑는 것도 모두 칸 번호를 바꾸지 않는 구독 목록에서 나옵니다.
취소는 부탁이지 명령이 아니다
스레드를 강제로 죽이는 방법은 없습니다. 대신 "이제 그만해 주세요" 를 전달하고 받는 쪽이 협조합니다.
자물쇠가 없어도 데드락은 걸린다
화면이 얼어붙었는데 예외도 로그도 없습니다. lock 은 한 줄도 없습니다. .Result 한 줄이면 충분합니다.
await 는 기다림을 넘길 뿐 일을 넘기지 않는다
무거운 계산에 async 를 붙였는데 화면이 그대로 멈춥니다. 안에 await 를 하나 넣어도 마찬가지입니다. 재보면 이유가 보입니다.
async 는 스레드를 만들지 않는다
스레드가 여럿일 때 생기는 문제와 async 는 다른 이야기입니다.
끝이 없는 큐는 메모리가 대신 끝을 낸다
넣는 쪽이 꺼내는 쪽보다 빠르면 큐가 자랍니다. 상한을 안 정하면 남은 메모리가 상한이 됩니다.
소켓에 둘이 동시에 쓰면 프레임이 섞인다
자물쇠로 못 막는 자리가 있습니다. 기다리게 하는 대신 줄을 세워 한 명만 쓰게 만듭니다.
false sharing 은 공유한 적이 없어서 false 다
스레드마다 자기 카운터를 줬는데 병렬화가 오히려 느려집니다. 락도 없고 겹쳐 쓴 값도 없습니다. 두 카운터가 같은 64바이트 캐시 라인에 앉아 있어서 그렇습니다. false sharing 이 무엇이고 어떻게 확인하고 어떻게 고치는지를 직접 재본 숫자로 봅니다.
한 스레드만 만지면 잠글 것이 없다
자물쇠는 부딪혔을 때 처리하는 도구입니다. 애초에 안 부딪히게 짜면 자물쇠가 필요 없습니다.
내가 처음인지 물어보려면 CompareExchange 다
lock 은 들어가도 되는지만 알려 줍니다. 딱 한 번만 일어나야 하는 일에는 다른 도구가 필요합니다.
조건 변수는 락을 놓고 기다렸다가 다시 잡는다
락을 쥔 채로 조건을 기다리면 아무도 그 조건을 바꿔 줄 수 없습니다. 조건 변수는 락을 놓는 것과 기다리는 것을 한 번에 합니다. Monitor.Wait 과 Pulse 가 그것이고, if 로 검사하면 100번 중 100번 깨집니다.
lock 은 순서를 만들고 대가를 받는다
여러 줄을 묶어 지키는 유일한 방법인데, 걸어 놓고 잘못 쓰면 프로그램이 멈춰 섭니다.
count++ 는 한 번에 일어나지 않는다
한 줄짜리 코드가 기계에서는 세 걸음입니다. 그 사이에 남이 끼어들면 더한 값이 사라집니다.
스레드를 직접 만들지 않고 일을 맡긴다
잡 시스템은 워커 스레드를 미리 띄워 두고 일감을 나눠 줍니다. 스레드를 안 만드는 것이 핵심이고, 안전 장치가 나머지 절반입니다.