[UMC] 6주차 : Thread
ios 6주차 워크북 (Thread)
📝 학습 목표
- Thread를 이해한다.
- Multi-Thread의 개념을 이해하고 활용할 수 있다.
✍🏻 수업 내용 정리
Thread란 무엇인가?
가닥 맥락
꿰다
ios의 기술적인 내용이 아니라 OS측면의 이야기
쉽게 말하자면 스레드는 ‘흐름’이다.
스레드를 이해하려면 우선 프로세스에 대해 이해 해야한다.
프로세스는 우리가 사용하는 프로그램, 실행중인 프로그램이다.
스레드는 프로세스 즉 실행중인 프로그램에서 실질적인 작업을 수행하는 주체, 프로그램의 흐름이다.
프로세스의 흐름이기에 프로세스 내부에 속하게 된다.
파일 세 개를 다운 받는다고 하면, 이 세 개를 다운받는 녀석이 스레드이다. (주체를 맡는 것이 스레드)
프로세스가 프로그램이다 보니, 코드와 데이터 파일로 이루어져 있는 것을 볼 수 있다.
프로세스 내에서 실질적인 작업을 담당하는 것이 스레드이다. 그림처럼 프로세스 내에 위치하고 있다. 프로세스의 작업을 수행하는 주체가 스레드이기 때문에 하나의 프로세스 안에는 최소 하나의 스레드가 존재한다.
오른쪽 그림처럼 스레드가 여러개 있으면 멀티 스레드, 하나 있으면 싱글 스레드 환경이라고 한다.
멀티 스레드 환경은 하나의 프로세스 내에 여러 스레드가 존재하는 거라, 각자 작업을 분담해서 수행하게 된다.
스레드는 고속도로처럼 생각하면 된다!
도로가 하는 일은 똑같다. 도로를 프로세스라고 생각하면
이 도로 안에 있는 차선이 바로 스레드이다. 차선 별로 역할이 정해져 있는 것처럼 (1차선은 추월로…등) 멀티 스레드일 땐 이렇게 역할이 부여된다.
아까와 달리 멀티 스레드 환경에서는 이렇게 역할이 분배된다.
하지만 결국 모든 프로세스의 마무리 시간은 똑같다.
(동시에 하지 않기 때문)
이렇게 멀티 스레드에서 동시에 작업을 수행하게 되면 이걸 바로 비동기처리를 한다고 말할 수 있다.
저 위에서 순차적으로 작업을 수행하는 것이 바로 동기(sync)라고 한다.
우리가 궁극적으로 배우고자 하는 건 비동기처리이다.
한 눈에 봐도 한 프로세스 사이클을 보다 빠르게 처리할 수 있다는 장점이 비동기에는 있기 때문이다.
하지만 동시에 처리함으로써 발생할 수 있는 문제들도 있기 때문에 항상 좋은 것은 아니다!
그래서 비동기 동기 별로 좋을 만한 상황을 파악하는 것이 중요하다.
은행에서 인출을 할 때, 가족 중 한사람과 내가 동시에 인출을 시도한다고 하자, 각자 2만원씩 인출한다면 동시에 처리가 되었을 때 -2만원이 되므로 문제가 발생할 수 있다. 이런 경우 한가지 스레드를 잠궈버리는 과정이 필요하게 된다. 이때는 동기처리가 필요한 것이다.
멀티 스레드 환경 개인은 일의 능률을 위한 것이 아니라, 비동기 처리를 했을 때 능률까지 적용이 되는 것이다.
실습
🎯 핵심 키워드
-
Thread
https://babbab2.tistory.com/63
쓰레드(Thread)
한 프로세스 내에서 동작되는 여러 실행의 흐름
쓰레드의 정의는 이럼
쓰레드는 프로세스가 아닌, 프로세스 내에서 동작되는 것이기 때문에
메모리 영역을 독립적으로 할당받지 못함!
이런 식으로 Code, Data, Heap 영역은 공유하고 Stack 영역만 독립적으로 할당받을 수 있음!!
(Stack 영역만 별도로 가지는 이유는 Stack 영역이 LIFO라서,
Stack이 쌓이면 프로세스가 섞인 채로 순서대로 나와 흐름에 방해를 주기 때문이라 함 ;ㅁ;)
따라서 쓰레드들 끼리는 힙 영역을 공유하여 같은 자원을 접근할 수 있지만,
각자의 스택 영역은 서로 접근할 수 없음
-
Process
운영체제로부터 시스템 자원을 할당받는 작업의 단위
프로세스의 개념은 위와 같음을 말하는 것임!!
따라서 이 프로세스들은
각각의 독립된 메모리 영역 (Code, Data, Stack, Heap)을 각자 할당 받음!
따라서 프로세스끼리는 서로의 변수나 자료구조에 대해 절대 접근할 수 없음
만약, A 프로세스가 B 프로세스 자원 접근하려고 하면,
프로세스간 통신(IPC)를 사용해야 함(파일, 소켓 등)
-
Multi-Thread
하나의 프로그램을 여러 개의 쓰레드로 구성하여,
각 쓰레드마다 하나의 작업(Task)씩 처리하도록 하는 것
윈도우, 리눅스 등 많은 운영체제가 멀티 프로세싱을 지원하지만,
이 멀티 쓰레드를 기본으로 하고 있음!!
그럼 왜 멀티 프로세싱이 아닌, 멀티 쓰레드가 기본인지 장단점을 살펴보자 :)
먼저 장점부터 보자면,
쓰레드 간 Code, Data, Heap 영역을 공유하기 때문에, Context Switching이 빠름
또한 프로세스를 생성하여 자원을 할당하는 것이 아니기 때문에,
생성/종료 시간도 프로세스보다 빠름
또 프로세스 간 통신이 까다로운 반면에, 쓰레드들은 stack 영역을 제외하고
나머지 영역을 공유하기 때문에 통신 방법이 훨씬 간단함
다만 단점도 존재함
설계가 까다로움. 왜냐면 Stack영역 빼고 공유를 하기 때문에,
A쓰레드가 접근하려는 힙 영역의 자원을 B가 갑자기 접근해서 바꿔버리는 등
자원 공유의 문제가 생기기 때문(동기화 문제)
또한 독립적이지 않아, 하나의 쓰레드에서 문제가 발생 시 전체 쓰레드가 영향을 받음
- valueChanged
값이 바뀌면 분기?
- touchUpInside
터치를 하면 분기?
-
비동기 처리
이제 sync와 async에대해 알아보자. sync는 동기 처리 메소드, async는 비동기 처리 메소드다. 동기 처리 메소드 sync는 주어진 작업이 완료될 때까지 다음 작업으로 넘어가지 않는다 .¹ 비동기 처리 메소드 async는 주어진 작업이 완료되든 말든, 작업 전달 후 곧 바로 다음 작업으로 넘어간다
-
Thread-Lock
https://m.blog.naver.com/PostView.naver?isHttpsRedirect=true&blogId=horajjan&logNo=220775728358
멀티스레드 프로그래밍 시, 락 메커니즘의 핵심은 하나의 작업이 이뤄지고 있을 때, 그 무엇도 해당 작업을 방해할 수 없게 하는 것이다. 예를 들어 스레드1이 객체를 얻어 출력하고 있을 때, 스레드 2는 스레드 1이 배열에서 객체를 꺼내어 완료할 때까지 기다려야 한다.
Lock을 얻는 가장 간단한 방법은 synchronized(objA)를 이용하는 것이다
@synchronized(anObj){ //여기서 장금 상태가 된다 }
그 다음으로 NSLock을 사용하는 방법이 있다. Lock 또는 tryLock 두 가지 방법을 사용할 수 있다. Lock 메소드는 락이 없는 스레드가 락을 얻을 때까지 중지하고 대기한다. 그러나 tryLock 메소드는 tryLock메소드가 NO를 반환하면 이는 다른 스레드가 락을 이미 갖고 있어 락을 획득할 수 없는 것을 의미한다.
https://m.blog.naver.com/PostView.naver?isHttpsRedirect=true&blogId=horajjan&logNo=220775728358
- Dispatch Queue
DispatchQueue
An object that manages the execution of tasks serially or concurrently on your app’s main thread or on a background thread.
iOS 8.0+iPadOS 8.0+macOS 10.10+Mac Catalyst 13.0+tvOS 9.0+watchOS 2.0+Xcode 8.0+
Declaration
class DispatchQueue : DispatchObject
Overview
Dispatch queues are FIFO queues to which your application can submit tasks in the form of block objects. Dispatch queues execute tasks either serially or concurrently. Work submitted to dispatch queues executes on a pool of threads managed by the system. Except for the dispatch queue representing your app’s main thread, the system makes no guarantees about which thread it uses to execute a task.
You schedule work items synchronously or asynchronously. When you schedule a work item synchronously, your code waits until that item finishes execution. When you schedule a work item asynchronously, your code continues executing while the work item runs elsewhere.
Important Attempting to synchronously execute a work item on the main queue results in deadlock.
Avoiding Excessive Thread Creation
When designing tasks for concurrent execution, do not call methods that block the current thread of execution. When a task scheduled by a concurrent dispatch queue blocks a thread, the system creates additional threads to run other queued concurrent tasks. If too many tasks block, the system may run out of threads for your app.
Another way that apps consume too many threads is by creating too many private concurrent dispatch queues. Because each dispatch queue consumes thread resources, creating additional concurrent dispatch queues exacerbates the thread consumption problem. Instead of creating private concurrent queues, submit tasks to one of the global concurrent dispatch queues. For serial tasks, set the target of your serial queue to one of the global concurrent queues. That way, you can maintain the serialized behavior of the queue while minimizing the number of separate queues creating threads.
📢 6주차 수업 후기
- 6주차 수업을 듣고 서로 느낀 점을 이야기해주세요!
- 다행히 전주에 공부했던 내용이 많이 나왔다.
- 덕분에 빠르게 이해할 수 있었다.
⚠️ 스터디간 주의사항
- 과제 피드백 기반 진행입니다 - 한명씩 본인의 과제를 발표하는 시간 그리고 해온 과제에 대한 피드백을 하는 시간 (ex:전 이렇게 생각해서 이런 부분 다르게 해왔는데 저것도 괜찮은 것 같아요!)이 무조건 기반이 되어야 합니다!
- 부가적으로 워크북에서 제공되는 키워드 혹은 강의에서 들은 디테일적인 부분에서 더 토의해봐도 좋을 것 같습니다.
✅ 실습 체크리스트
⚡ 트러블 슈팅
-
⚡이슈 No.1 (예시, 서식만 복사하시고 지워주세요.)
이슈
👉 앱 실행 중에 노래 다음 버튼을 누르니까 앱이 종료되었다.
문제
👉 노래클래스의 데이터리스트의 Size를 넘어서 NullPointException이 발생하여 앱이 종료된 것이었다.
해결
👉 노래 다음 버튼을 눌렀을 때 데이터리스트의 Size를 검사해 Size보다 넘어가려고 하면 다음으로 넘어가는 메서드를 실행시키지 않고, 첫 노래로 돌아가게끔 해결
참고 레퍼런스
- 링크
🤔 이것도 한 번 생각해봐요!
- Dispatch Queue의 옵션들에 대해 공부해보세요! (ex. main, global(), …)
- main
- global()
- 기타 옵션들에 대해서도 공부해보세요!
- Dispatch Queue의 serial, Concuerrent에 대해서도 공부해보세요!
standard mission
나만의 타이머 만들기!
인하대 수강신청 사이트를 흉내낸 앱을 만들어 보았다.
스레드 단원이라 스레드를 사용해야 하는데…
처음엔 멀티 스레드 2개를 이용해서 하나는 라벨 하나는 배경색을 바꾸리라 다짐했으나
그냥 이전에 배운 DispatchSourceTimer를 사용하는 쪽이 훨씬 쉽고
메인 스레드 내부에서 한 번에 해결할 수 있어 이걸로 했다.
(또 DispatchSourceTimer라는 GCD 객체를 여러분들께 소개하고 싶기도 했다)
😇Timer 클래스로 타이머 만들기
사실 타이머를 만드는 방법은 다양한데
Timer 클래스를 이용할 수도 있다(Timer 의 scheduledTimer() 이용)
Timer.scheduledTimer(withTimeInterval: 3, repeats: false) {
(timer) in
print("Timer Fire!")
} // 반복하지 않는 타이머
Timer.scheduledTimer(withTimeInterval: 1, repeats: true) {
(timer) in
print("Timer on every second")
} // 반복하는 타이머
iOS 13부터 추가된 Combine framework와 함께 Timer 클래스에도 publish() 함수가 추가됐다!
특정한 Loop 동안 반복하는 타이머를 만들기 좋다
let cancellable = Timer.publish(every: 1, on: .main, in: .common)
.autoconnect()
.sink(){
print("timer fired at \($0)")
}
publish(every:tolerance:on:in:options:)
Returns a publisher that repeatedly emits the current date on the given interval.
iOS 13.0+iPadOS 13.0+macOS 10.15+Mac Catalyst 13.0+tvOS 13.0+watchOS 6.0+Xcode 11.0+
Declaration
static func publish(
every interval: TimeInterval,
tolerance: TimeInterval? = nil,
on runLoop: RunLoop,
in mode: RunLoop.Mode,
options: RunLoop.SchedulerOptions? = nil
) -> Timer.TimerPublisher
**Parametersinterval
The time interval on which to publish events. For example, a value of 0.5
publishes an event approximately every half-second.tolerance
The allowed timing variance when emitting events. Defaults to nil
, which allows any variance.runLoop
The run loop on which the timer runs.mode
The run loop mode in which to run the timer.options
Scheduler options passed to the timer. Defaults to nil
.
Return Value
A publisher that repeatedly emits the current date on the given interval.
Discussion
The return type, [Timer.TimerPublisher](https://developer.apple.com/documentation/foundation/timer/timerpublisher)
, conforms to [ConnectablePublisher](https://developer.apple.com/documentation/combine/connectablepublisher)
, which means you must explicitly connect to the Timer
publisher to begin publishing events. You can do this with a call to [connect()](https://developer.apple.com/documentation/combine/connectablepublisher/connect())
, or by using [autoconnect()](https://developer.apple.com/documentation/combine/connectablepublisher/autoconnect())
to automatically connect when a subscriber attaches, as shown here:
cancellable = Timer.publish(every: 1, on: .main, in: .common) .autoconnect() .sink() { print ("timer fired: \($0)")}
🥨비동기 스레드 방식으로 타이머 만들기
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(1)) {
print("async After timeout!")
}
asyncAfter()로 비동기 요청을 하는 방식이다.
반복해서 호출하는 타이머라기 보다는 일정 시간동안 동작을 비동기 방식으로 delay하는 것에 가깝다. 일정한 주기에 맞춰서 반복하는 타이머로 사용하기는 무리가 있다. main 큐가 아니더라도 쉽게 다른 큐를 지정해서 백그라운드 스레드에서 동작할 수 있다는 장점이 있다
😎DispatchSourceTimer로 타이머 만들기
Protocol DispatchSourceTimer
A dispatch source that submits the event handler block based on a timer.
iOS 8.0+iPadOS 8.0+macOS 10.10+Mac Catalyst 13.0+tvOS 9.0+watchOS 2.0+Xcode 8.0+
Declaration
protocol DispatchSourceTimer
Overview
You do not adopt this protocol in your objects. Instead, use the [makeTimerSource(flags:queue:)](https://developer.apple.com/documentation/dispatch/dispatchsource/2300106-maketimersource)
method to create an object that adopts this protocol.
Topics
**Scheduling the Timer Trigger Conditions**
[func schedule(deadline: DispatchTime, repeating: DispatchTimeInterval, leeway: DispatchTimeInterval)](https://developer.apple.com/documentation/dispatch/dispatchsourcetimer/2920392-schedule)
Schedules a timer with the specified deadline, repeat interval, and leeway values.
[func schedule(deadline: DispatchTime, repeating: Double, leeway: DispatchTimeInterval)](https://developer.apple.com/documentation/dispatch/dispatchsourcetimer/2920395-schedule)
Schedules a timer with the specified deadline, repeat interval, and leeway values.
[func schedule(wallDeadline: DispatchWallTime, repeating: DispatchTimeInterval, leeway: DispatchTimeInterval)](https://developer.apple.com/documentation/dispatch/dispatchsourcetimer/2920391-schedule)
Schedules a timer with the specified time, repeat interval, and leeway values.
[func schedule(wallDeadline: DispatchWallTime, repeating: Double, leeway: DispatchTimeInterval)](https://developer.apple.com/documentation/dispatch/dispatchsourcetimer/2920390-schedule)
Schedules a timer with the specified time, repeat interval, and leeway values.
위는 Apple developer에 나와있는 DispatchSourceTimer의 정보이다
DispatchSourceTimer의 사용법은 비교적 간단하다. 타이머를 만들고(DispatchSource.makeTimerSource(flags: , queue:)),
스케줄을 지정하고(deadline과 repeating 지정),
이벤트 핸들러를 명시하고 (timer?.setEventHandler(handler: {
}) )
타이머를 시작하면 된다. 특히 원하는 스레드 큐에서 반복해서 동작하도록 할 수 있어서 백그라운드 타이머로 만들기 좋을 것 같지…만
그렇게 또 간단하기만 한 녀석은 아니다
Timer 생성자의 queue에는 원하는 작업이 UI와 관련되어 있다면 Main을 할당해주어야 한다(이건 저번주에 배운 바 있다)
바로 실행되어야 한다면 deadline에 .now()만 할당하면 되고, 3초 후에 실행되어야 한다면 .now() + 3 을 할당해주면 된다. 1초마다 반복되도록 repeating에는 1을 할당하였다.
Timer와 함께 연동될 EventHandler를 할당한다.
// 시작
timer?.resume()
// 일시정지
timer?.suspend()
// 종료
timer?.cancel()
timer = nil
Timer를 종료할 땐 Timer에 꼭 nil을 할당해서 메모리에서 해제시켜야 한다. 그렇지 않다면 화면을 벗어나도 Background에서 계속 동작하고 있다…혼자서 외롭게 말이다.
😄전체코드
import UIKit
import AudioToolbox
enum TimerStatus {
case start
case pause
case end
}
class ViewController: UIViewController {
@IBOutlet weak var timerLabel: UILabel!
@IBOutlet weak var commentView: UITextView!
@IBOutlet weak var commentText: UITextField!
@IBOutlet weak var registerButton: UIButton!
@IBOutlet weak var datePicker: UIDatePicker!
@IBOutlet weak var startButton: UIButton!
@IBOutlet weak var stopButton: UIButton!
@IBOutlet weak var contentCommentView: UITextView!
var duration = 60
var content: String = ""
var timerStatus: TimerStatus = .end
var timer: DispatchSourceTimer?
var currentSeconds = 0
override func viewDidLoad() {
super.viewDidLoad()
self.configureToggleButton()
// Do any additional setup after loading the view.
}
func setTimerInfoViewVisible(isHidden: Bool){
self.timerLabel.isHidden = isHidden
}
func startTimer(){
if self.timer == nil {
self.timer = DispatchSource.makeTimerSource(flags: [], queue: .main)
self.timer?.schedule(deadline: .now(), repeating: 1)
self.timer?.setEventHandler(handler: { [weak self] in
guard let self = self else { return } //Self를 Strong reference으로
self.currentSeconds -= 1
let hour = self.currentSeconds / 3600 //시
let minutes = (self.currentSeconds % 3600) / 60 //분
let seconds = (self.currentSeconds % 3600) % 60 //초
self.timerLabel.text = String(format: "%02d:%02d:%02d", hour, minutes, seconds)
self.turnColorView()
if self.currentSeconds <= 0 {
self.stopTimer()
self.view.backgroundColor = UIColor(red: 255.0, green: 255.0, blue: 255.0, alpha: 0.5)
AudioServicesPlaySystemSound(1024)
}
})
self.timer?.resume()
}
}
func configureToggleButton(){
self.startButton.setTitle("시작", for: .normal)
self.startButton.setTitle("일시정지", for: .selected)
}
@IBAction func tapRegisterButton(_ sender: UIButton) {
if(commentText.text != ""){
self.content = contentCommentView.text
content.append("\n" + commentText.text!)
contentCommentView.text = content
}
}
@IBAction func tapStartButton(_ sender: UIButton) {
self.duration = Int(self.datePicker.countDownDuration)
switch self.timerStatus {
case .end:
self.currentSeconds = self.duration
self.timerStatus = .start
self.setTimerInfoViewVisible(isHidden: false)
self.datePicker.isHidden = true
self.startButton.isSelected = true
self.stopButton.isEnabled = true
self.startTimer()
case .start:
self.timerStatus = .pause
self.startButton.isSelected = false
self.timer?.suspend()
case .pause:
self.timerStatus = .start
self.startButton.isSelected = true
self.timer?.resume()
}
}
func turnColorView(){
if(currentSeconds >= 60){
debugPrint(currentSeconds)
self.view.backgroundColor = UIColor(red: 1.0, green: 1.0, blue: 1.0, alpha: 1.0)
}
if(currentSeconds < 60 && currentSeconds >= 40){
debugPrint(currentSeconds)
self.view.backgroundColor = UIColor(red: 1.0, green: 0.749, blue: 0.749, alpha: 1.0)
}
if(currentSeconds < 40 && currentSeconds >= 20){
debugPrint(currentSeconds)
self.view.backgroundColor = UIColor(red: 1.0, green: 0.5294, blue: 0.5294, alpha: 1.0)
}
if(currentSeconds < 20 && currentSeconds >= 10){
debugPrint(currentSeconds)
self.view.backgroundColor = UIColor(red: 1.0, green: 0.3696, blue: 0.3686, alpha: 1.0)
}
if(currentSeconds < 10 && currentSeconds >= 5){
debugPrint(currentSeconds)
self.view.backgroundColor = UIColor(red: 1.0, green: 0.1882, blue: 0.1882, alpha: 1.0)
}
if(currentSeconds < 5 && currentSeconds > 0){
debugPrint(currentSeconds)
self.view.backgroundColor = UIColor(red: 1.0, green: 0.0, blue: 0.0, alpha: 1.0)
}
}
func stopTimer() {
if self.timerStatus == .pause {
self.timer?.resume()
}
self.timerStatus = .end
self.stopButton.isEnabled = false
self.setTimerInfoViewVisible(isHidden: true)
self.datePicker.isHidden = false
self.startButton.isSelected = false
self.timer?.cancel()
self.timer = nil //타이머 메모리 해제
}
@IBAction func tapStopButton(_ sender: UIButton) {
switch self.timerStatus {
case .start, .pause:
self.stopTimer()
default:
break
}
}
}
😈실행 결과
시간이 지남에 따라 화면 색이 변하고
원하는 말을 실시간으로 텍스트 뷰에 넣을 수 있으며
타이머 기능도 착실하게 들어갔다
댓글남기기