May 02, 2021, TIL (Today I Learned) - WaitUntil, ARC

Inwoo Hwang·2021년 8월 26일
0
post-thumbnail

수업내용


동시성 vs 병렬

동시성 프로그래밍: 단일 코어에서 여러 개의 thread를 활용하여 시간을 잘게 쪼개서 일을 진행하는 것이기에 동시에 실행되는 것 처럼 보인다. 앱 같은 경우 여러개의 작업(데이터 압축, 처리, 테이블뷰 등등)을 실행하기 위해

병렬 프로그래밍: 여러개의 프로세스를 같은 작업을 여러 프로세스에서 나눠서 하는 것. 유사하거나 같은 작업을 여러개로 나눠서 실행하는 것. 코어를 여러개를 가지고 있다고 해서 무조건 병렬 프로그래밍은 아니다. 물론 멀티코어 환경에서만 병렬처리가 가능하다.

멀티코어 환경은 물리적 환경이므로 동시성 프로그래밍과 상관없다?

멀티코어가 물리적 환경인 것은 맞다. 하지만 동시성 프로그래밍 환경에서 멀티코어인 환경에서는 특정 코어에서 코어를 잘 활용할 수 있도록 일을 잘 분배 해 준다. (GCD의 역할) GCD는 여러 개의 동작이 동시에 일어나게 하는 것 처럼 보이게 하기 위해 여러 개의 코어를 이용할 수 있다.

스위프트 타입의 모든 프로퍼티는 Thread-safe하다

여러 개의 thread가 하나의 공유된 자원을 접근할 때 공간을 잠구고 그것을 사용한 다음에 해당 공간을 해제시켜줘야 thread-safe하다. 우리가 어떤 타입 프로퍼티 값을 사용해야 하는 상황일 때 여러 개의 thread가 있는 상황에서 특정 프로퍼티에 접근하려 할 때 A작업에서 해당 프로퍼티를 가져가려 하는데 B에서 해당 프로퍼티를 수정하고 있다면? 동시에 일어나게 된다면 문제가 발생할 수 있다[앱이 죽어버린다.]

Thread-safe == atomic:

Atomic vs Non-Atomic

Atomic: 모든 프로퍼티는 atomic하다 == 여러 thread에서 특정 프로퍼티를 가져가면 안된다는 개념. 다른 thread에서 특정 프로퍼티에 접근할 때 상관 없거나 아니면 해당 프로퍼티가 사용 중이라면 해당 프로퍼티를 접근하지 못하게 방지되어 있다. 외부로부터 방해 받지 않을 수 있다.

Non-atomic: 여러 thread 에서 특정 프로퍼티를 동시에 가져가도 상관 없다는 개념

두 개 이상의 프로세스나 스레드가 하나의 데이터를 공유할 때 데이터가 동기화되지 않는 상황을 뜻하는 용어로 알맞은 것은?

Race Condition: 경쟁 상태란 둘 이상의 입력 또는 조작의 타이밍이나 순서 등이 결과값에 영향을 줄 수 있는 상태를 말한다. 따라서 하나의 데이터를 공유할 때 이런 동시작업이 이뤄지면 각 작업의 결과가 해당 데이터에게 종합적으로 영향을 미치는 것이 아니라 동작 중 하나는 데이터에 정상적으로 동기화 되지 않을 수 있다.

ARC(Automatic Reference Counting)

  • Q1 : 아래 코드의 각 알파벳에 해당하는 개체는 각각 메모리의 어느 영역에 위치하며, 위치와 크기는 언제 결정될까?
var globalVaribale = 100		// A

struct Person {
  static let koreanWord = "사람"		// B (B-1 : koreanWord / B-2 : "사람")
  var name: String		// C
}

var yagom = Person(name: "야곰")		// D (D-1 : yagom / D-2 : "야곰")

func printName(_ person: Person) {	// E - person
  let name = person.name		// F - name
  print(name)
}

class Academy {
  static let koreanWord = "아카데미"	// G (G-1 : koreanWord / G-2 : "아카데미")
  var name: String			// H
  
  init(name: String) {
    self.name = name
  }
}

var academy = Academy(name: "야곰 아카데미")	// I (I-1 : academy / I-2 : "야곰 아카데미")

메모리 영역에 대한 가벼운 이해

  • 코드 영역: 코드 자체를 저장한다.
  • 데이터 영역: 전역 변수, static 변수 등이 저장, 프로그램 시작시 생성, 꺼지면 없어진다.
  • 스택 영역: 지역 변수, 매개변수, 값 타입의 데이터를 저장하는 영역 -> 컴파일 시 할당된다.
  • 힙 영역: 프로그램이 실행되는 동안 동적으로 사용되는 데이터 -> Run 타임 시 할당된다.
  • Q2 : ARC는 무엇인가?

    • 참고) https://sujinnaljin.medium.com/ios-arc-뿌시기-9b3e5dc23814

      strong, weak, unowned

      • 자동으로 메모리 관리를 해줌 (힙에 할당된 인스턴스의 메모리)
      • 객체에 대한 참조 카운트를 관리하고 0이 되면 자동으로 메모리 해제
      • run time에 계속 실행되는게 아니라 compile time(build 할 때)에 실행
      • 하지만 retain cycle 유의 해야한다.
  • Q3 : ARC 이전의 메모리 관리는 어땠을까?

    • MRC (*Manual Reference Counting)*
      • retain : retain count(= reference count) 증가를 통해 현재 Scope에서 객체가 유지되는것을 보장
      • release : retain count(= reference count)를 감소시킴. retain 후에 필요 없을 때 release
  • Q4 : ARC를 이해해야 하는 이유는 무엇무엇이 있을까?

    • 매번 전달할 때마다 값을 복사해 전달하는 값 타입과는 달리 참조 타입은 하나의 인스턴스가 참조를 통해 여러 곳에서 접근하기 때문에 언제 메모리에서 해제되는지가 중요한 문제이다.

      인스턴스가 적절한 시점에 메모리에서 해제되지 않으면 한정적인 메모리 자원을 낭비하게 되며, 이는 성능의 저하로 이어지게 된다.ㅇ

      작성해야하는 코드양이 줄어들고, 프로그램의 안정성 증가한다.

      속도 향상을 위한 리팩토링 과정에서 ARC에 대한 이해 없이는 ...

  • Q5 : 언제 구조체를 선택하고 언제 클래스를 선택해야할까?

[warning]: 틀린 부분 엄청 많을 것 같습니다.....

  • Q1 : 아래 코드의 각 알파벳에 해당하는 개체는 각각 메모리의 어느 영역에 위치하며, 위치와 크기는 언제 결정될까?

​ A: 데이터 영역에 저장된다. , 프로그램이 시작될 때 결정된다.

​ B-1: 데이터 영역에 저장된다 , (이 프로퍼티는 Person객체가 생성될 때 이 프로퍼티의 위치와 크기가 결정된다.)

​ B-2: 데이터 영역에 저장? (koreanWord의 초기값, koreanWord가 생성될 때 해당 프로퍼티의 위치와 크기가 결정된다??!?!.)

​ C: 스택 영역에 저장?, (yagom 객체가 생성될 때 해당 프로퍼티의 위치와 크기가 결정된다.)

​ D-1: 스택 영역에 저장된다, (스택 포인터로 Person의 프로퍼티를 복사한 뒤)

​ D-2: 스택영역에 저장[파라미터], (yagom 객체가 생성될 때 해당 프로퍼티의 위치와 크기가 결정된다.)

​ E: 스택 영역에 저장[파라미터], (printName이 호출될 때 해당 프로퍼티의 위치와 크기가 결정된다.)

​ F: 스택 영역[지역변수], (printName이 호출될 때 해당 프로퍼티의 위치와 크기가 결정된다.)

​ G-1: 데이터 영역 (이 프로퍼티는 Academy객체가 생성될 때 이 프로퍼티의 위치와 크기가 결정된다.)

​ G-2: 데이터 영역 (이 프로퍼티는 Academy객체가 생성될 때 이 프로퍼티의 위치와 크기가 결정된다.)

​ H: 스택 영역? Heap 영역?(academy 객체가 생성될 때 해당 프로퍼티의 위치와 크기가 결정된다.)

​ I-1: 스택영역[heap을 가리키는 포인터가 저장], (academy 객체가 생성될 때 해당 프로퍼티의 위치와 크기가 결정된다.)

​ I-2: 스택 영역[파라미터], (academy 객체가 생성될 때 해당 프로퍼티의 위치와 크기가 결정된다.)

  • Q2 : ARC는 무엇인가?

. 자동으로 참조 횟수를 관리하기 때문에 사용하지 않는 인스턴스 를 메모리에서 해제해줍니다.

  • Q3 : ARC 이전의 메모리 관리는 어땠을까?

​ 개발자가 직접 매번 메모리를 해제해줘야해서 불편함이 있었을 것 같다

  • Q4 : ARC를 이해해야 하는 이유는 무엇무엇이 있을까?

​ 메모리 누수를 막기 위해 강한참조와 약한 참조를 적절히 사용해야 하는데 이를 이해하기 위해서는 ARCㄹ를 잘 이해해야 합니다.

  • Q5 : 언제 구조체를 선택하고 언제 클래스를 선택해야할까?

​ 데이터의 크기를 모르거나 스택에 저장하기에는 큰 데이터의 경우에는 class에 데이터를 할당하는 것이 낫다.

그 외엔 스택에 할당하면 된다. 스텍에 너무 큰 데이터를 저장하게 되면 stackoverflow!!!!

https://babbab2.tistory.com/25

http://tcpschool.com/c/c_memory_structure

https://zeddios.tistory.com/1213#recentComments

https://jusung.gitbook.io/the-swift-language-guide/language-guide/10-properties

https://developer.apple.com/videos/play/wwdc2016/416/

학습내용


궁금증 1: waitUntilFinished를 main thread에서 사용하면 안되는데 이런 경우 어떻게 하지?

비동기적인 작업을 모두 끝낸 뒤 해당 작업의 요약본을 print함수를 통해 출력해야 하는데 이 경우 main thread를 차단(block)한 뒤 모든 작업이 끝난 뒤 main thread의 차단을 풀어 print 메서드를 실행 시킬 수 있다.

func serveClient() {
  while true {
    bankWindow.maxConcurrentOperationCount = 3
    var workTime = Double.zero
    
    displayMenu()
    
    let menuNumber = inputMenuNumber()
    
    switch menuNumber {
      case 1:
      var clientWaitLine: [Client] = []
      let totalCustomer = customerNumber()
      for waitNumber in 1...totalCustomer {
        let randomClientAttribute = acceeptRandomClient()
        let client = Client()
        client.waitingNumber = waitNumber
        client.creditRate = creditRatings[randomClientAttribute.0]
        client.typeOfWork = typeOfWorks[randomClientAttribute.1]
        
        assignPriority(client)
        clientWaitLine.append(client)
        
        client.completionBlock = {
          guard let typeOfTask = client.typeOfWork else { return }
          workTime += typeOfTask.duration
        }
      }
      bankWindow.addOperations(clientWaitLine, waitUntilFinished: true)
      // 위 작업이 끝나기 전까지 현재 쓰레드(main thread)는 차단된다.
      print("업무가 마감되었습니다. 오늘 업무를 처리한 고객은 총 \(totalCustomer)명이며, 총 업무시간은\(String(format: "%.2f", workTime))초 입니다.")
      continue
      case 2:
      exit(0)
      default:
      print("잘못된 입력")
      continue
    }
  }
}

물론 현재는 콘솔앱을 통해 작업 중이라서 큰 문제는 생기지 않지만 앱이라면 얘기가 달라진다.

iOS 앱에서 "메인 쓰레드는 1/60초에 한번씩 화면을 다시 그리는 역할을 하고 있기 때문에 오래걸리는 작업(네트워크 통신)과 같은 일을 시키면 안된다(버벅임의 원인이 된다.)" - Allen

UI업데이트 하는 작업을 메인쓰레드에서 하기 때문에 waitUntilFinished를 통해 아무런 while true문(메인 쓰레드에서 진행하는 작업)에서 waitUntilFinished를 사용하게 되면 메인쓰레드를 잠시 차단시키게 된다.

부분적 해결법: serial queue

let serialQueue = DispatchQueue(label: "serial")   //  시리얼큐 만들기

func serveClient() {
bankManager.maxConcurrentOperationCount = 1

serialQueue.async {    // ⭐️ 시리얼큐에서 while true문 실행
	while true {
		// 고객 업무
		}
	}
}

메인 thread를 멈추지 않으려면 간단하다 serialQueue를 하나 만들어서 비동기적으로 while true문을 보내주면 된다. 그럼 이 while true문에서 실행되는 waitUntilFinished는 메인쓰레드가 아닌 다른 쓰레드를 차단하게 된다.

풀리지 않는 부분

그런데 이렇게 보내고 나면 아무것도 실행되지 않고 프로그램이 종료되어 버립니다.

시리얼큐로 보내는 순간 main() 메인함수에서 작업이 끝나버리기 때문이다. 비동기적으로 작업을 실행하기 때문에 메인쓰레드에서는 main 함수의 모든 작업이 끝났다고 인식 해 버리는 것입니다. 😥

그렇다고 serialQueue.sync {}로 동기적으로 비동기적 작업을 보낸다 하더라도 메인쓰레드에서 시리얼 큐로 보내는 일을 기다리기 때문에 결론적으로는 mainQueue가 해당 작업이 끝날 때까지 block상태가 되는 것이다.

그래서 내린 결론은....일단 콘솔앱에서는 이렇게 실행해도 되지만 앱을 구현할 때는 로직을 바꿔줘야 할 것 같습니다....그래도 waitUntilFinished의 개념을 조금 더 이해한 것 같아서 기쁩니다; ㅎㅎ

궁금증2: Async 와 self의 관계?

갑 캡쳐인가 뭔가 때문에 약한 참조가 필요한 것 같은데...다시 공부 해 봐야겠어요!

궁금증3: qualityOfService vs queuePriority

둘 다 operation의 우선순위를 정할 수 있게 해주는데 어떤게 다른지 잘 모르겠어요.. 찾아봐야 하는데 프로젝트의 늪에...

https://developer.apple.com/documentation/foundation/nsoperation/1411204-queuepriority

https://developer.apple.com/documentation/foundation/nsoperation/1413553-qualityofservice

프로젝트 진행: 은행창구 Step2

은행창구3

은행창구 콘솔로그1

profile
james, the enthusiastic developer

0개의 댓글