🚀
Incheol's TECH BLOG
  • Intro
  • Question & Answer
    • JAVA
      • JVM
      • String, StringBuffer, StringBuilder
      • JDK 17일 사용한 이유(feat. JDK 8 이후 훑어보기)
      • 스택 오버 플로우(SOF)
      • 블럭킹 | 논블럭킹 | 동기 | 비동기
      • 병렬처리를 이용한 이미지 리사이즈 개선
      • heap dump 분석하기 (feat. OOM)
      • G1 GC vs Z GC
      • JIT COMPILER
      • ENUM
      • STATIC
      • Thread(쓰레드)
      • hashCode()와 equals()
      • JDK 8 특징
      • break 와 continue 사용
      • STREAM
      • Optional
      • 람다와 클로저
      • Exception(예외)
      • Garbage Collector
      • Collection
      • Call by Value & Call by Reference
      • 제네릭(Generic)
    • SPRING
      • Spring 특징
      • N+1 문제
      • 테스트 코드 어디까지 알아보고 오셨어요?
      • 테스트 코드 성능 개선기
      • RestTemplate 사용시 주의사항
      • 동시성 해결하기(feat. TMI 주의)
      • redisson trylock 내부로직 살펴보기
      • DB 트래픽 분산시키기(feat. Routing Datasource)
      • OSIV
      • @Valid 동작 원리
      • mybatis @Builder 주의사항
      • 스프링 클라우드 컨피그 갱신 되지 않는 이슈(feat. 서비스 디스커버리)
      • ImageIO.read 동작하지 않는 경우
      • 카프카 transaction 처리는 어떻게 해야할까?
      • Spring Boot 특징
      • Spring 5 특징
      • JPA vs MyBatis
      • Filter와 Interceptor
      • 영속성 컨텍스트(Persistence Context)
      • @Transactional
      • @Controlleradvice, @ExceptionHandler
      • Spring Security
      • Dispatcher Servlet
      • @EnableWebMvc
      • Stereo Type(스테레오 타입)
      • AOP
      • JPA Repository 규칙
    • DATABASE
      • Database Index
      • SQL vs NoSQL
      • DB 교착상태
      • Isolation level
      • [MySQL] 이모지 저장은 어떻게 하면 좋을까?
      • SQL Hint
      • JOIN
    • INFRA
      • CLOUD COMPUTING
      • GIT
      • DOCKER
      • 카프카 찍먹하기 1부
      • 카프카 찍먹하기 2부 (feat. 프로듀서)
      • 카프카 찍먹하기 3부 (feat. 컨슈머)
      • JENKINS
      • POSTMAN
      • DNS 동작 원리
      • ALB, NLB,ELB 차이는?
      • 카프카 파티션 주의해서 사용하자
      • DEVOPS
      • JWT
      • OSI 7 Layer
      • MSA
      • 서비스 디스커버리는 어떻게 서비스 등록/해제 하는걸까?
      • 핀포인트 사용시 주의사항!! (feat 로그 파일 사이즈)
      • AWS EC2 도메인 설정 (with ALB)
      • ALB에 SSL 설정하기(feat. ACM)
      • 람다를 활용한 클라우드 와치 알림 받기
      • AWS Personalize 적용 후기… 😰
      • CloudFront를 활용한 S3 성능 및 비용 개선
    • ARCHITECTURE
      • 객체지향과 절차지향
      • 상속보단 합성
      • SOLID 원칙
      • 캡슐화
      • DDD(Domain Driven Design)
    • COMPUTER SCIENCE
      • 뮤텍스와 세마포어
      • Context Switch
      • REST API
      • HTTP HEADER
      • HTTP METHOD
      • HTTP STATUS
    • CULTURE
      • AGILE(Feat. 스크럼)
      • 우리는 성장 할수 있을까? (w. 함께 자라기)
      • Expert Beginner
    • SEMINAR
      • 2022 INFCON 후기
        • [104호] 사이드 프로젝트 만세! - 기술만큼 중요했던 제품과 팀 성장기
        • [102호] 팀을 넘어서 전사적 협업 환경 구축하기
        • [103호] 코드 리뷰의 또 다른 접근 방법: Pull Requests vs. Stacked Changes
        • [105호] 실전! 멀티 모듈 프로젝트 구조와 설계
        • [105호] 지금 당장 DevOps를 해야 하는 이유
        • [102호] (레거시 시스템) 개편의 기술 - 배달 플랫폼에서 겪은 N번의 개편 경험기
        • [102호] 서버비 0원, 클라우드 큐 도입으로 해냈습니다!
  • STUDY
    • 오브젝트
      • 1장 객체, 설계
      • 2장 객체지향 프로그래밍
      • 3장 역할, 책임, 협력
      • 4장 설계 품질과 트레이드 오프
      • 5장 책임 할당하기
      • 6장 메시지와 인터페이스
      • 7징 객체 분해
      • 8장 의존성 관리하기
      • 9장 유연한 설계
      • 10장 상속과 코드 재사용
      • 11장 합성과 유연한 설계
      • 12장 다형성
      • 13장 서브클래싱과 서브타이핑
      • 14장 일관성 있는 협력
      • 15장 디자인 패턴과 프레임워크
      • 마무리
    • 객체지향의 사실과 오해
      • 1장 협력하는 객체들의 공동체
      • 2장 이상한 나라의 객체
      • 3장 타입과 추상화
      • 4장 역할, 책임, 협력
    • JAVA ORM JPA
      • 1장 JPA 소개
      • 2장 JPA 시작
      • 3장 영속성 관리
      • 4장 엔티티 매핑
      • 5장 연관관계 매핑 기초
      • 6장 다양한 연관관계 매핑
      • 7장 고급 매핑
      • 8장 프록시와 연관관계 관리
      • 9장 값 타입
      • 10장 객체지향 쿼리 언어
      • 11장 웹 애플리케이션 제작
      • 12장 스프링 데이터 JPA
      • 13장 웹 애플리케이션과 영속성 관리
      • 14장 컬렉션과 부가 기능
      • 15장 고급 주제와 성능 최적화
      • 16장 트랜잭션과 락, 2차 캐시
    • 토비의 스프링 (3.1)
      • 스프링의 이해와 원리
        • 1장 오브젝트와 의존관계
        • 2장 테스트
        • 3장 템플릿
        • 4장 예외
        • 5장 서비스 추상화
        • 6장 AOP
        • 8장 스프링이란 무엇인가?
      • 스프링의 기술과 선택
        • 5장 AOP와 LTW
        • 6장 테스트 컨텍스트 프레임워크
    • 클린코드
      • 1장 깨끗한 코드
      • 2장 의미 있는 이름
      • 3장 함수
      • 4장 주석
      • 5장 형식 맞추기
      • 6장 객체와 자료 구조
      • 9장 단위 테스트
    • 자바 트러블슈팅(with scouter)
      • CHAP 01. 자바 기반의 시스템에서 발생할 수 있는 문제들
      • CHAP 02. scouter 살펴보기
      • CHAP 03. scouter 설정하기(서버 및 에이전트)
      • CHAP 04. scouter 클라이언트에서 제공하는 기능들
      • CHAP 05. scouter XLog
      • CHAP 06. scouter 서버/에이전트 플러그인
      • CHAP 07. scouter 사용 시 유용한 팁
      • CHAP 08. 스레드 때문에(스레드에서) 발생하는 문제들
      • CHAP 09. 스레드 단면 잘라 놓기
      • CHAP 10. 잘라 놓은 스레드 단면 분석하기
      • CHAP 11. 스레드 문제
      • CHAP 12. 메모리 때문에 발생할 수 있는 문제들
      • CHAP 13. 메모리 단면 잘라 놓기
      • CHAP 14. 잘라 놓은 메모리 단면 분석하기
      • CHAP 15. 메모리 문제(Case Study)
      • CHAP 24. scouter로 리소스 모니터링하기
      • CHAP 25. 장애 진단은 이렇게 한다
      • 부록 A. Fatal error log 분석
      • 부록 B. 자바 인스트럭션
    • 테스트 주도 개발 시작하기
      • CHAP 02. TDD 시작
      • CHAP 03. 테스트 코드 작성 순서
      • CHAP 04. TDD/기능 명세/설계
      • CHAP 05. JUnit 5 기초
      • CHAP 06. 테스트 코드의 구성
      • CHAP 07. 대역
      • CHAP 08. 테스트 가능한 설계
      • CHAP 09. 테스트 범위와 종류
      • CHAP 10. 테스트 코드와 유지보수
      • 부록 A. Junit 5 추가 내용
      • 부록 C. Mockito 기초 사용법
      • 부록 D. AssertJ 소개
    • KOTLIN IN ACTION
      • 1장 코틀린이란 무엇이며, 왜 필요한가?
      • 2장 코틀린 기초
      • 3장 함수 정의와 호출
      • 4장 클래스, 객체, 인터페이스
      • 5장 람다로 프로그래밍
      • 6장 코틀린 타입 시스템
      • 7장 연산자 오버로딩과 기타 관례
      • 8장 고차 함수: 파라미터와 반환 값으로 람다 사용
      • 9장 제네릭스
      • 10장 애노테이션과 리플렉션
      • 부록 A. 코틀린 프로젝트 빌드
      • 부록 B. 코틀린 코드 문서화
      • 부록 D. 코틀린 1.1과 1.2, 1.3 소개
    • KOTLIN 공식 레퍼런스
      • BASIC
      • Classes and Objects
        • Classes and Inheritance
        • Properties and Fields
    • 코틀린 동시성 프로그래밍
      • 1장 Hello, Concurrent World!
      • 2장 코루틴 인 액션
      • 3장 라이프 사이클과 에러 핸들링
      • 4장 일시 중단 함수와 코루틴 컨텍스트
      • 5장 이터레이터, 시퀀스 그리고 프로듀서
      • 7장 스레드 한정, 액터 그리고 뮤텍스
    • EFFECTIVE JAVA 3/e
      • 객체 생성과 파괴
        • 아이템1 생성자 대신 정적 팩터리 메서드를 고려하라
        • 아이템2 생성자에 매개변수가 많다면 빌더를 고려하라
        • 아이템3 private 생성자나 열거 타입으로 싱글턴임을 보증하라
        • 아이템4 인스턴스화를 막으려거든 private 생성자를 사용하라
        • 아이템5 자원을 직접 명시하지 말고 의존 객체 주입을 사용하라
        • 아이템6 불필요한 객체 생성을 피하라
        • 아이템7 다 쓴 객체 참조를 해제하라
        • 아이템8 finalizer와 cleaner 사용을 피하라
        • 아이템9 try-finally보다는 try-with-resources를 사용하라
      • 모든 객체의 공통 메서드
        • 아이템10 equals는 일반 규약을 지켜 재정의하라
        • 아이템11 equals를 재정의 하려거든 hashCode도 재정의 하라
        • 아이템12 toString을 항상 재정의하라
        • 아이템13 clone 재정의는 주의해서 진행해라
        • 아이템14 Comparable을 구현할지 고려하라
      • 클래스와 인터페이스
        • 아이템15 클래스와 멤버의 접근 권한을 최소화하라
        • 아이템16 public 클래스에서는 public 필드가 아닌 접근자 메서드를 사용하라
        • 아이템17 변경 가능성을 최소화하라
        • 아이템18 상속보다는 컴포지션을 사용하라
        • 아이템19 상속을 고려해 설계하고 문서화하라. 그러지 않았다면 상속을 금지하라
        • 아이템20 추상 클래스보다는 인터페이스를 우선하라
        • 아이템21 인터페이스는 구현하는 쪽을 생각해 설계하라
        • 아이템22 인터페이스 타입을 정의하는 용도로만 사용하라
        • 아이템23 태그 달린 클래스보다는 클래스 계층구조를 활용하라
        • 아이템24 멤버 클래스는 되도록 static으로 만들라
        • 아이템25 톱레벨 클래스는 한 파일에 하나만 담으라
      • 제네릭
        • 아이템26 로 타입은 사용하지 말라
        • 아이템27 비검사 경고를 제거하라
        • 아이템28 배열보다는 리스트를 사용하라
        • 아이템29 이왕이면 제네릭 타입으로 만들라
        • 아이템30 이왕이면 제네릭 메서드로 만들라
        • 아이템31 한정적 와일드카드를 사용해 API 유연성을 높이라
        • 아이템32 제네릭과 가변인수를 함께 쓸 때는 신중하라
        • 아이템33 타입 안전 이종 컨테이너를 고려하라
      • 열거 타입과 애너테이션
        • 아이템34 int 상수 대신 열거 타입을 사용하라
        • 아이템35 ordinal 메서드 대신 인스턴스 필드를 사용하라
        • 아이템36 비트 필드 대신 EnumSet을 사용하라
        • 아이템37 ordinal 인덱싱 대신 EnumMap을 사용하라
        • 아이템38 확장할 수 있는 열거 타입이 필요하면 인터페이스를 사용하라
        • 아이템 39 명명 패턴보다 애너테이션을 사용하라
        • 아이템40 @Override 애너테이션을 일관되게 사용하라
        • 아이템41 정의하려는 것이 타입이라면 마커 인터페이스를 사용하라
      • 람다와 스트림
        • 아이템46 스트림에는 부작용 없는 함수를 사용하라
        • 아이템47 반환 타입으로는 스트림보다 컬렉션이 낫다
        • 아이템48 스트림 병렬화는 주의해서 적용하라
      • 메서드
        • 아이템49 매개변수가 유효한지 검사하라
        • 아이템50 적시에 방어적 본사본을 만들라
        • 아이템53 가변인수는 신중히 사용하라
        • 아이템 54 null이 아닌, 빈 컬렉션이나 배열을 반환하라
        • 아이템56 공개된 API 요소에는 항상 문서화 주석을 작성하라
      • 일반적인 프로그래밍 원칙
        • 아이템56 공개된 API 요소에는 항상 문서화 주석을 작성하라
        • 아이템57 지역변수의 범위를 최소화하라
        • 아이템 60 정확한 답이 필요하다면 float와 double은 피하라
      • 예외
        • 아이템 73 추상화 수준에 맞는 예외를 던지라
        • 아이템 74 메서드가 던지는 모든 예외를 문서화하라
      • 동시성
        • 아이템78 공유 중인 가변 데이터는 동기화해 사용하라
        • 아이템79 과도한 동기화는 피하라
        • 아이템 80 스레드보다는 실행자, 태스크, 스트림을 애용하라
      • 직렬화
        • 아이템 87 커스텀 직렬화 형태를 고려해보라
    • Functional Programming in Java
      • Chap 01. 헬로, 람다 표현식
      • Chap 02. 컬렉션의 사용
      • Chap 03. String, Comparator, 그리고 filter
      • Chap 04. 람다 표현식을 이용한 설계
      • CHAP 05. 리소스를 사용한 작업
      • CHAP 06. 레이지
      • CHAP 07. 재귀 호출 최적화
      • CHAP 08. 람다 표현식의 조합
      • CHAP 09. 모든 것을 함께 사용해보자
      • 부록 1. 함수형 인터페이스의 집합
      • 부록 2. 신택스 오버뷰
    • 코틀린 쿡북
      • 2장 코틀린 기초
      • 3장 코틀린 객체지향 프로그래밍
      • 4장 함수형 프로그래밍
      • 5장 컬렉션
      • 6장 시퀀스
      • 7장 영역 함수
      • 9장 테스트
      • 10장 입력/출력
      • 11장 그 밖의 코틀린 기능
    • DDD START!
      • 1장 도메인 모델 시작
      • 2장 아키텍처 개요
      • 3장 애그리거트
      • 4장 리포지터리와 모델구현(JPA 중심)
      • 5장 리포지터리의 조회 기능(JPA 중심)
      • 6장 응용 서비스와 표현 영역
      • 7장 도메인 서비스
      • 8장 애그리거트 트랜잭션 관리
      • 9장 도메인 모델과 BOUNDED CONTEXT
      • 10장 이벤트
      • 11장 CQRS
    • JAVA 8 IN ACTION
      • 2장 동작 파라미터화 코드 전달하기
      • 3장 람다 표현식
      • 4장 스트림 소개
      • 5장 스트림 활용
      • 6장 스트림으로 데이터 수집
      • 7장 병렬 데이터 처리와 성능
      • 8장 리팩토링, 테스팅, 디버깅
      • 9장 디폴트 메서드
      • 10장 null 대신 Optional
      • 11장 CompletableFuture: 조합할 수 있는 비동기 프로그래밍
      • 12장 새로운 날짜와 시간 API
      • 13장 함수형 관점으로 생각하기
      • 14장 함수형 프로그래밍 기법
    • 객체지향과 디자인패턴
      • 객체 지향
      • 다형성과 추상 타입
      • 재사용: 상속보단 조립
      • 설계 원칙: SOLID
      • DI와 서비스 로케이터
      • 주요 디자인 패턴
        • 전략패턴
        • 템플릿 메서드 패턴
        • 상태 패턴
        • 데코레이터 패턴
        • 프록시 패턴
        • 어댑터 패턴
        • 옵저버 패턴
        • 파사드 패턴
        • 추상 팩토리 패턴
        • 컴포지트 패턴
    • NODE.JS
      • 1회차
      • 2회차
      • 3회차
      • 4회차
      • 6회차
      • 7회차
      • 8회차
      • 9회차
      • 10회차
      • 11회차
      • 12회차
      • mongoose
      • AWS란?
    • SRPING IN ACTION (5th)
      • Chap1. 스프링 시작하기
      • Chap 2. 웹 애플리케이션 개발하기
      • Chap 3. 데이터로 작업하기
      • Chap 4. 스프링 시큐리티
      • Chap 5. 구성 속성 사용하기
      • Chap 6. REST 서비스 생성하기
      • Chap 7. REST 서비스 사용하기
      • CHAP 8 비동기 메시지 전송하기
      • Chap 9. 스프링 통합하기
      • CHAP 10. 리액터 개요
      • CHAP 13. 서비스 탐구하기
      • CHAP 15. 실패와 지연 처리하기
      • CHAP 16. 스프링 부트 액추에이터 사용하기
    • 스프링부트 코딩 공작소
      • 스프링 부트를 왜 사용 해야 할까?
      • 첫 번째 스프링 부트 애플리케이션 개발하기
      • 구성을 사용자화 하기
      • 스프링부트 테스트하기
      • 액추에이터로 내부 들여다보기
    • ANGULAR 4
      • CHAPTER 1. A gentle introduction to ECMASCRIPT 6
      • CHAPTER 2. Diving into TypeScript
      • CHAPTER 3. The wonderful land of Web Components
      • CHAPTER 4. From zero to something
      • CHAPTER 5. The templating syntax
      • CHAPTER 6. Dependency injection
      • CHAPTER 7. Pipes
      • CHAPTER 8. Reactive Programming
      • CHAPTER 9. Building components and directives
      • CHAPTER 10. Styling components and encapsulation
      • CHAPTER 11. Services
      • CHAPTER 12. Testing your app
      • CHAPTER 13. Forms
      • CHAPTER 14. Send and receive data with Http
      • CHAPTER 15. Router
      • CHAPTER 16. Zones and the Angular magic
      • CHAPTER 17. This is the end
    • HTTP 완벽 가이드
      • 게이트웨이 vs 프록시
      • HTTP Header
      • REST API
      • HTTP Method 종류
        • HTTP Status Code
      • HTTP 2.x
  • REFERENCE
    • TECH BLOGS
      • 어썸데브블로그
      • NAVER D2
      • 우아한 형제들
      • 카카오
      • LINE
      • 스포카
      • 티몬
      • NHN
      • 마켓컬리
      • 쿠팡
      • 레진
      • 데일리 호텔
      • 지그재그
      • 스타일쉐어
      • 구글
      • 야놀자
    • ALGORITHM
      • 생활코딩
      • 프로그래머스
      • 백준
      • 알고스팟
      • 코딜리티
      • 구름
      • 릿코드
Powered by GitBook
On this page
  • 원자성 위반
  • 원자성의 의미
  • 스레드 한정
  • 스레드 한정의 개요
  • 액터
  • 액터의 역할
  • 액터 생성
  • 액터를 사용한 기능 확장
  • 액터 상호 작용에 대한 추가 정보
  • 상호 배제
  • 상호 배제의 이해
  • 상호 배제와 상호 작용
  • 휘발성 변수
  • 스레드 캐시
  • @Volatile
  • @Volatile이 스레드 안전 카운터 문제를 해결하지 못하는 이유
  • @Volatile을 사용하는 경우
  • 원자적 데이터 구조
  • 요약

Was this helpful?

  1. STUDY
  2. 코틀린 동시성 프로그래밍

7장 스레드 한정, 액터 그리고 뮤텍스

코틀린 동시성 프로그래밍 7장을 요약한 내용입니다.

원자성 위반

원자성 위반은 동시성 오류 유형이다. 이 유형의 오류는 정확한 동기화 없이 객체의 상태를 동시에 수정할 때 발생한다. 원자성 위반은 코틀린에서도 발생할 수 있지만 오류를 피할 수 있도록 디자인하는 데 도움이 되는 기본형을 제공한다.

원자성의 의미

소프트웨어 실행의 관점에서, 연산(operation)이 단일하고 분할할 수 없을 때 이 연산을 원자적(atomic)이라 한다.

동시성 애플리케이션을 실행하게 되면 공유 상태를 수정하는 코드 블록이 다른 스레드의 변경 시도와 겹치면서 이런 문제가 발생한다.

private var counter = 0

fun increment() {
		counter ++
}

수차적으로 실행하면, counter 값에 대해 걱정할 필요 없이 incremental()을 원하는 만큼 호출할 수 있다. 그러나 여기에 동시성을 추가하면 내부의 많은 것들이 바뀐다.

var counter = 0

fun asyncIncrement(by: Int) = async(CommonPool[1]) {
		for (i in 0 until by) {
				counter++
		}
}

CommonPool을 CoroutineContext로 사용해 요청한 횟수만큼 counter를 늘리고 있다.

fun main(args: Array<String>) = runBlocking {
		val workerA = asyncIncrement(2000)
		val workerB = asyncIncrement(100)

		workerA.await()
		workerB.await()

		print("counter [$counter]")
}

실행 후 counter의 값이 가끔 2100보다 낮다는 것을 알 수 있다.

counter++를 수행하는 코드가 원자적이지 않아서 발생한다. 이것이 실제로 의미하는 바는 asyncIncrement 내부의 여러 for 루프 중 여러 사이클이 counter값을 오직 한 번만 바꿨다는 것이다.

코드 블록을 원자적으로 만들려면 블록 안에서 발생하는 어떤 메모리 엑세스도 동시에 실행되지 않도록 해야 한다.

스레드 한정

스레드 한정의 개요

스레드 한정은 이름에서 알 수 있듯이, 공유 상태에 접근하는 모든 코루틴을 단일 스레드에서 실행되도록 한정하는 것을 의미한다. 상태가 더 이상 스레드 간에 공유되지 않으며 하나의 스레드만 상태를 수정한다는 뜻이다. ( 이 방법은 애플리케이션의 성능에 부정적인 영향을 미치지 않는다는 점에 한해서만 유용하다.)

액터

스레드 한정은 앞에서 언급한 시나리오에서는 잘 작동하지만 앱의 여러 다른 부분에서 공유 상태를 수정해야 하거나, 원자 블록에 더 높은 유연성을 원하는 시나리오의 경우 이를 확장하는 방법이 필요하다.

액터의 역할

액터는 두 가지 강력한 도구의 조합이다. 상태 엑세스를 단일 스레드로 한정하고 다른 스레드가 채널로 상태 수정을 요청할 수 있다. 액터를 사용하면 값을 안전하게 업데이트할 수 있을 뿐만 아니라 이를 위한 강력한 커뮤니케이션 매커니즘도 갖추게 된다.

액터 생성

여러 다른 스레드에서 counter를 안전하게 수정해야 한다고 가정해보자.

private var counter = 0
private val context = newSingleThreadContext("counterActor")

fun getCounter() = counter

이제 카운터의 값을 캡슐화했으므로 수신된 각 메시지에 따라 값을 증가시키는 액터를 추가하기만 하면 된다.

val actorCounter = actor<Void?>(context) {
		for (msg in channel) {
				counter++
		}
}

전송된 메시지를 실제로는 사용하지 않기 때문에, 단순히 액터의 유형을 Void?로 설정하면 호출자가 null을 보낼 수 있다.

fun main(args: Array<String>) = runBlocking {
		val workerA = asyncIncrement(2000)
		val workerB = asyncIncrement(100)

		workerA.await()
		workerB.await()

		print("counter [$counter]")
}
fun asyncIncrement(by: int) = async(CommonPool) {
		for (i in 0 until by) {
				actorCounter.send(null)
		}
}

액터를 사용한 기능 확장

액터는 사용하면 채널이 전체 코루틴을 원자적으로 유지하면서 더 높은 유연성을 허용한다는 점이 가장 커다란 장점이다.

액터를 사용해 카운터를 늘리거나 줄일 수 있도록 만들어보자

enum class Action {
		INCREASE,
		DECREASE
}

그런 다음에 액터와 코루틴을 업데이트해 이런 액션을 연산에 매핑할 수 있다.

var actorCounter = actor<Action>(context) {
		for (msg in channel) {
				when(msg) {
						Action.INCREASE -> counter++
						Action.DECREASE -> counter--
				}
		}
}

fun asyncDecrement(by: Int) = async {
		for (i in 0 until by) {
				actorCounter.send(Action.DECREASE)
		}
}

메인 스레드는 CommonPool을 사용해 카운터를 동시에 증가 및 감소시키는 코루틴을 생성하며, 코루틴은 액션과 함께 메시지를 액터에 보내서 이를 수행한다.

액터 상호 작용에 대한 추가 정보

클라이언트 관점에서 액터는 단순히 송신 채널이다. 그러나 구현 관점에서 액터의 수행방식을 고민해 볼 필요가 있다.

버퍼드 액터

액터는 다른 송신 채널과 동일하게 버퍼링될 수 있다. 기본적으로 모든 액터는 버퍼링되지 않는다. 메시지가 수신될 때까지 발신자를 send()에서 일시 중단한다. 버퍼링된 액터(Buffered actors)를 생성하려면 capacity 매개변수를 빌더에 전달해야 한다.

fun main(args: Array<String>) {
		val bufferedPrinter = actor<String>(capacity = 10) {
				for (msg in channel) {
						println(msg)
				}
		}

		bufferedPrinter.send("hello")
		bufferedPrinter.send("hello")

		bufferedPrinter.close()
}

CoroutineContext를 갖는 액터

counter 예제를 해결한 것처럼 액터를 생성할 때 CoroutineContext를 전달할 수 있다.

val dispatcher = newFixedThreadPoolContext(3, "pool")
val actor = actor<String>(dispatcher) {
		for (msg in channel) {
				println("Running in ${Thread.currentThread().name}")
		}
}

for (i in 1..10) {
		actor.send("a")
}

상호 배제

지금까지는 코드 블록의 모든 메모리 액세스가 단일 스레드에서 발생하도록 보장함으로써 원자성 위반을 회피했다. 두 개의 코드 블록이 동시에 실행되는 것을 피할 수 있는 또 다른 방법이 있다. 바로 상호 배제다.

상호 배제의 이해

상호 배제란 한 번에 하나의 코루틴만 코드 블록을 실행할 수 있도록 하는 동기화 매커니즘을 말한다.

코틀린 뮤텍스의 가장 중요한 특징은 블록되지 않는다는 점이다. 실행 대기 중인 코루틴은 잠금을 획득하고 코드 블록을 실행할 수 있을 때까지 일시 중단된다. 코루틴은 일시 중단되지만 일시 중단 함수를 사용하지 않고 뮤텍스를 잠글 수 있다.

뮤텍스 생성

뮤텍스를 만들려면 Mutex 클래스의 인스턴스만 생성하면 된다.

var mutext = Mutext()

fun asyncIncrement(by: Int) = async {
		for (i in 0 until by) {
				mutex.withLock {
						counter++
				}
		}
}

한 번에 하나의 코루틴만 잠금을 보유하고, 잠금을 시도하는 다른 코루틴을 일시 중단 함으로써 카운터에 대한 모든 증분이 동기화 되도록 한다. 따라서 다음과 같이 몇 번을 호출하더라도 counter의 증분 중 어느 것도 유실되지 않는다.

상호 배제와 상호 작용

대개 withLock()을 사용하는 것만으로도 충분하다. 잠금 및 잠금 해제에 대한 상세 제어가 필요하면 lock, unlock()을 사용할 수 있다.

val mutext = Mutext()

mutext.lock() // 잠금이 이미 설정된 경우 일시 중단된다. 
print("I am now an atomic block")
mutext.unlock() // 이것은 중단되지 않는다. 

isLocked 속성을 사용해 뮤텍스가 현재 잠겨 있는지 확인할 수 있다.

뮤텍스를 잠글 수 있는지 여부를 나타내는 불리언(Boolean)을 반환하는 tryLock()을 사용하기도 한다.

val mutext = Mutext()

mutext.lock() 
val lockedByMe = mutext.tryLock() // false
mutext.unlock() 

휘발성 변수

휘발성 변수는 구현하려는 스레드 안전(thread-safe) 카운터와 같은 문제를 해결하지 못한다. 그러나 휘발성 변수는 일부 시나리오에서 스레드 간에 정보를 공유해야 할 때 간단한 솔루션으로 사용될 수 있다.

스레드 캐시

JVM에서 각 스레드는 비휘발성 변수의 캐시된 사본을 가질 수 있다. 이 캐시는 항상 변수의 실제 값과 동기화되지는 않는다. 한 스레드에서 공유 상태를 변경하면 캐시가 업데이트될 때까지 다른 스레드에서는 볼 수 없다.

@Volatile

변수의 변경사항을 다른 스레드에 즉시 표시하기 위해 다음 예제에서 @Volatile 주석을 사용할 수 있다.

@Volatile
var shutdownRequested = false

@Volatile은 Kotlin/JVM에서만 사용할 수 있다. 휘발성(volatility)을 보장하는 기능을 JVM의 기능을 사용하기 때문에, 다른 플랫폼에서는 사용할 수 없다.

@Volatile이 스레드 안전 카운터 문제를 해결하지 못하는 이유

그러나 이전의 예제를 실행하면 실제 일부 변경사항이 유실되는 것을 확인할 수 있다. 이런 결과가 나온데에는 두 가지 이유가 있다.

  • 다른 스레드가 읽거나 수정하는 동안 스레드의 읽기가 발생할 때 : 두 스레드는 모두 같은 데이터로 시작해 동일한 증분을 만든다. 둘 다 카운터를 X에서 Y로 변경하므로 한 증분씩 유실된다.

  • 다른 스레드가 수정한 후 스레드의 읽기가 발생하지만, 스레드의 로컬 캐시가 업데이트되지 않았을 때: 스레드는 로컬 캐시가 제때 업데이트되지 않아서 다른 스레드가 Y로 설정한 후에도 카운터의 값을 X로 읽을 수 있다. 두 번째 스레드는 카운터의 값을 증가시키지만 오래된 값으로 시작했기 때문에 이미 변경한 내용을 덮어 쓴다.

@Volatile을 사용하는 경우

휘발성 변수를 사용해서 더 나은 코드를 작성하려면 두 가지 시나리오 전제가 참이어야 한다.

  • 변수 값의 변경은 현재 상태에 의존하지 않는다.

  • 휘발성 변수는 다른 변수에 의존하지 않으며, 다른 변수도 휘발성 변수에 의존하지 않는다.

첫 번째 전제는 스레드 안전 카운터와 같은 시나리오를 배제하는 데 도움이 된다. 반면에 두 번째 전제는 휘발성 변수의 의존성 때문에 일관성 없는 상태가 생성되는 것을 피할 수 있도록 도와준다. 이전 사례는 두 번째 전제를 따르지 않기 때문에 안전하지 않다.

class DataProcessor {
		@Volatile
		private var shutdownRequested = false

		fun shutdown() {
				shutdownRequested = true
		}

		fun process() {
				while (!shutdownRequested) {
						// process away
				}
		}
}

해당 예제에서는 두 전제 모두 유효하다.

  • shutdown()에서 작성된 shutdownRequested의 수정은 변수 자체의 현재 상태에 의존하지 않는다. 항상 true로 설정된다.

  • 다른 변수는 shutdownRequested에 의존하지 않으며, 다른 변수에도 의존하지 않는다.

원자적 데이터 구조

원자성은 원자적 데이터 구조로서 기본적으로 원자적 연산을 제공하는 데이터 구조다.

val counter = AtomicInteger()
counter.incrementAndGet() // 원자적이므로 스레드 안전 카운터를 쉽게 구현할 수 있다. 

요약

  • 공유 상태를 가지면 동시성 코드에서 문제가 될 수 있다. 스레드의 캐시와 메모리의 액세스의 원자성으로 인해 다른 스레드에서 수행한 수정 사항이 유실될 수 있다. 상태의 일관성을 해치는 원인이 된다.

  • 스레드 동기화를 할 수 있는 두 가지 방법이 있다. 하나의 스레드만 상태와 상호 작용하도록 보장해서 쓰기가 아닌 읽기 전용으로만 공유할 수 있게 하는것과, 코드 블록을 원자적으로 만들기 위해서 잠금을 사용해 코드 블록을 실행하려는 모든 스레드의 동기화를 강제하는 것이다.

  • CoroutineContext를 하나의 스레드로 된 디스패처와 함께 사용해 코루틴의 실행을 단일 스레드에서 실행되도록 강제한다.

  • 액터는 송신 채널과 코루틴의 쌍이다. 액터를 단일 스레드로 한정해 메시지를 기반으로 하기 보다는 강력한 동기화 매커니즘을 구축할 수 있다. 원하는 스레드에서 메시지를 보내 변경을 요청할 수 있지만, 변경은 특정 스레드에서 실행될 것이다.

  • 액터는 특히 코루틴의 스레드 제한과 쌍을 이뤄 이용하면 좋다. 예를 들어 액터가 스레드 풀에서 실행하도록 액터가 사용할 CoroutineContext를 지정할 수 있다.

  • 액터는 코루틴이기 때문에 여러 방식으로 시작할 수 있다.

  • 잠금을 사용해 코루틴을 동기화하기 위해 뮤텍스를 사용할 수 있다. 이렇게 하면 코루틴이 동기화된 작업을 수행할 수 있도록 기다리는 동안 코루틴을 일시중단할 수 있다.

  • JVM은 스레드의 캐시에 저장되지 않는 변수인 휘발성 변수를 제공한다. 휘발성 변수는 수정될 때 새 값은 이전 값에 의존하지 않으며 휘발성 변수의 상태는 다른 속성에 의존하지 않거나 영향을 미치지 않는 경우다.

  • 원자적 변수는 단순한 경우에 유용하지만 공유되는 상태가 하나 이상의 여러 변수인 경우 확장하기가 어려울 것이다.

  • 우리는 중요한 원칙인 정보 은닉을 실천에 옮겼다. 카운터의 구현을 숨겨서 향후에 뮤텍스, 원자적 변수, 또는 액터 없는 스레드 제한을 사용하도록 그것을 바꿀 수 있다.

Previous5장 이터레이터, 시퀀스 그리고 프로듀서NextEFFECTIVE JAVA 3/e

Last updated 4 years ago

Was this helpful?