운영체제를 보다가 오랜만에 Semaphore를 만났는데
예전에 C로 철학자 문제를 만들어본 적은 있지만 Swift에서는 아직 제대로 사용해본 적이 없더라고요!
"이걸로 뭔가 간단한걸 만들어보고 싶은데 뭘 만들지?" 고민하다가 정말 의미없는 무언가를 만들어내었습니다!
바로.....!!
버튼을 한번 누를 때마다 2초후에 값을 증가시키는 앱이에요!

버튼을 여러번 클릭한 경우, 단순히 2초 후에 한번에 올려주는 것이 아니라 2초마다 하나씩 하나씩 올려주는 작동을 합니다ㅎㅎ
영상에서 아래쪽은 버튼 누른 횟수가 증가하고, 위쪽은 전달된 이벤트가 2초의 텀을 두고 동기적으로 실행되는데요,
당연히 User Interaction등이 막히는 일이 있어서는 안되겠죠??

어떻게 만들지 한번 머릿속으로 구상해보세요!!
버튼을 누른 이벤트가 Queue에 쌓여서 하나씩 실행되어 여러 이벤트가 대기 시간을 포함해서 동기적으로 일어나도록 해줘야 할 것 같은데..!
여러가지 방법이 있겠지만, 저는 Semaphore를 이용해서 구현해보았습니다!
Semaphore를 사용하면 1. Task(프로세스, 스레드)의 개수를 제한해서 동시에 임계구역에 들어오지 못하게 하고 기다리게 하거나 2. 특정 조건에서만 임계구역에 들어올 수 있도록 쓰이기도 하는데요, (끝날 때까지 기다리게 해서 비동기적인 상황을 동기적인 상황으로 바꾼다던가요!ㅎㅎ)
사실 2번 방법처럼 버튼 이벤트에 Completion Handler을 붙여서 이벤트가 끝날 때까지 대기하게하는 방식으로 할 수도 있겠지만 (생각만 해봤어요), 이 방법은 요기 잘 설명 되어있기도 합니다!
Swift - GCD 세마포어(Dispatch Semaphore)
iOS 및 macOS 용 앱 개발, Emacs, Vim, Python 위주로 다루는 Seorenn 개인 블로그
seorenn.blogspot.com
저는 접근할 수 있는 태스크의 개수에 제한을 두는 1번 방법을 사용했어요!
semaphore value를 1로 줘서, 한번에 하나의 태스크만 임계구역 안에서 일어나게 했고, 임계구역에서는 2초를 기다리고 변수를 수정하는
작업을 진행하도록 했습니다.
밑에 그림처럼 버튼 이벤트들의 임계구역은 세마포어의 큐에 쌓여서 대기하고 있다가, 기존 이벤트가 semphore signal이 발생시키면 다음 이벤트가 들어오는 거죠!

아마 코드로 보면 더 잘 이해가 되실거에요!
먼저 초기값을 1로 가지는(한개의 태스크만 임계구역으로 들어올 수 있도록 허락하는) semaphore를 만들었고요
UI 바인딩을 didSet으로 해주었습니다. 이렇게 하면 변수 수정을 하는 것으로만 자동으로 UI가 수정되게 되죠!
// 초기값을 1로 가지는 Semaphore 생성
let semaphore = DispatchSemaphore(value: 1)
// 변수와 UI Binding
private var semaphoreCount: Int = 0 {
didSet {
DispatchQueue.main.async {
self.labelsView.semaphoreLabel.text = "\(self.semaphoreCount)"
}
}
}
private var clickCount: Int = 0 {
didSet {
DispatchQueue.main.async {
self.labelsView.clickCountLabel.text = "\(self.clickCount)"
}
}
}
그리고 버튼의 액션은 다음과 같이 만들어줬어요!
먼저
func IncreaseCounts() {
DispatchQueue.global(qos: .background).async {
self.clickCount += 1
self.semaphore.wait() // 임계구역 시작
sleep(2)
self.semaphoreCount += 1
self.semaphore.signal() // 임계구역 끝
}
}
전체 코드는 아래 첨부할게요!!
(코드 설명은 생략하겠슴다..ㅎㅎ 이것보단 더 의미있는 무언가를 만들어보셔요!)
ViewController
import UIKit
final class MainViewController: UIViewController {
private let labelsView = CustomLabelView()
private let buttonView = CustomButtonView()
let semaphore = DispatchSemaphore(value: 1)
private var semaphoreCount: Int = 0 {
didSet {
DispatchQueue.main.async {
self.labelsView.semaphoreLabel.text = "\(self.semaphoreCount)"
}
}
}
private var clickCount: Int = 0 {
didSet {
DispatchQueue.main.async {
self.labelsView.clickCountLabel.text = "\(self.clickCount)"
}
}
}
override func viewDidLoad() {
super.viewDidLoad()
setBViewLayout()
setAViewLayout()
buttonView.delegate = self
labelsView.semaphoreLabel.text = "\(semaphoreCount)"
labelsView.clickCountLabel.text = "\(clickCount)"
}
private func setAViewLayout() {
view.addSubview(buttonView)
buttonView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
buttonView.topAnchor.constraint(equalTo: labelsView.bottomAnchor),
buttonView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
buttonView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
buttonView.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: 0.5),
])
buttonView.backgroundColor = .secondarySystemBackground
}
private func setBViewLayout() {
view.addSubview(labelsView)
labelsView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
labelsView.topAnchor.constraint(equalTo: view.topAnchor),
labelsView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
labelsView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
labelsView.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: 0.5),
])
labelsView.backgroundColor = .secondarySystemBackground
}
}
extension MainViewController: CustomButtonViewDelegate {
func IncreaseCounts() {
DispatchQueue.global(qos: .background).async {
self.clickCount += 1
print("\(self.clickCount)")
self.semaphore.wait() // 임계구역 시작
sleep(3)
self.semaphoreCount += 1
self.semaphore.signal() // 임계구역 끝
}
}
}
Button이 있는 View
import UIKit
protocol CustomButtonViewDelegate: AnyObject {
func IncreaseCounts()
}
final class CustomButtonView: UIView {
private let userButton: UIButton = {
let newButton = UIButton()
newButton.setTitle("button", for: .normal)
newButton.backgroundColor = .orange
newButton.layer.cornerRadius = 7.0
return newButton
}()
var delegate: CustomButtonViewDelegate?
init() {
super.init(frame: .zero)
setButtonLayout()
userButton.addTarget(self, action: #selector(userButtonPressed), for: .touchUpInside)
}
@objc func userButtonPressed(_ sender: UIButton) {
if let delegate = delegate {
delegate.IncreaseCounts()
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setButtonLayout() {
self.addSubview(userButton)
userButton.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
userButton.widthAnchor.constraint(equalToConstant: 300),
userButton.heightAnchor.constraint(equalToConstant: 50),
userButton.centerXAnchor.constraint(equalTo: self.centerXAnchor),
userButton.centerYAnchor.constraint(equalTo: self.centerYAnchor),
])
}
}
Label들이 있는 View
import UIKit
final class CustomLabelView: UIView {
let semaphoreLabel: UILabel = {
var newLabel = UILabel()
newLabel.font = .systemFont(ofSize: 30, weight: .bold)
newLabel.textAlignment = .center
return newLabel
}()
let clickCountLabel: UILabel = {
var newLabel = UILabel()
newLabel.font = .systemFont(ofSize: 30, weight: .bold)
newLabel.textAlignment = .center
return newLabel
}()
init() {
super.init(frame: .zero)
setSemaphoreLabelLayout()
setClickCountLabelLayout()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setSemaphoreLabelLayout() {
self.addSubview(semaphoreLabel)
semaphoreLabel.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
semaphoreLabel.widthAnchor.constraint(equalToConstant: 300),
semaphoreLabel.heightAnchor.constraint(equalToConstant: 50),
semaphoreLabel.centerXAnchor.constraint(equalTo: self.centerXAnchor),
semaphoreLabel.centerYAnchor.constraint(equalTo: self.centerYAnchor),
])
}
private func setClickCountLabelLayout() {
self.addSubview(clickCountLabel)
clickCountLabel.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
clickCountLabel.widthAnchor.constraint(equalToConstant: 300),
clickCountLabel.heightAnchor.constraint(equalToConstant: 50),
clickCountLabel.centerXAnchor.constraint(equalTo: self.centerXAnchor),
clickCountLabel.topAnchor.constraint(equalTo: semaphoreLabel.bottomAnchor, constant: 50)
])
}
}
'iOS 개발 > Swift' 카테고리의 다른 글
[Swift] iOS는 화면을 어떻게 렌더링할까? (1) | 2023.10.04 |
---|---|
[Swift] Struct와 Class를 메모리 원리부터 자세하게 비교해보자 (2) | 2023.02.12 |
[ARC] 성능을 위해 unowned를 꼭 써야할까? (0) | 2023.02.03 |
[ARC] Lazy 변수 클로저에서 Unowned 캡처가 항상 안전할까? (0) | 2023.02.02 |
[ARC] 약한참조(Weak, Unowned)에 대해서 (0) | 2022.11.06 |
[Swift] 지정한 For-Loop 탈출하기 (0) | 2022.10.03 |
[Swift] Stride 함수를 사용하자 (0) | 2022.10.02 |
[Swift] appDelegate 참조 안전하게 하기 (0) | 2021.11.10 |