여러 스레드 or 프로세스가 한정된 공유 자원에 동시에(concurruntly) 접근하는 경우
경쟁상태는 데이터의 불일치(inconsistency) 문제 를 야기할 수 있다
경쟁 상태를 다루기 위해서는 동기화(synchronize) 되어야 함 — > 더 자세한 건 다음 토픽에서 다룸
경쟁 상태의 예시
critical section에서 race condition이 발생할 수 있음
critical section에는 여러 프로세스가 동시에 접근할 수 있기 때문
→ 주로, user mode일 때 보다(일반 프로세스가 cpu를 잡고 사용 할 경우) 커널모드일 때 race condition이 일어난다
왜? 커널모드에서는, 커널에 있는 자원을 여러 프로세스가 공유할 수 있기 때문
ex) 프로세스 a가 커널모드로 작업을 수행하면서, count 변수의 값을 읽어오고 증가시키려고 하는 와중에
context switch가 발생함. 프로세스 b에서도 count 변수의 값을 변경하게 됨.
이 때, 프로세스 b에 의해 변경된 count 값은 반영되지 않게 된다.
왜? 이미 커널에서는 (프로세스 a의 작업을 할 때) 변경되기 이전 count의 값을 가져온 상태이기 때문
(참고: 컴퓨터에서의 연산은, 메모리에 있는 값을 레지스터로 옮겨 계산하고 다시 레스터로 저장하는 방식으로 이루어짐)
(출처: 반효경 교수님 KOCW 운영체제 강의)
멀티 스레드 프로그래밍에서, 변수나 객체, 함수 등의 자원이 여러 스레드에 대해 동시에 접근되어도 정상적으로 프로그램이 동작하는 것
→ thread - safe하다고 해서 race condition으로부터 자유롭다는 뜻 아님.
→ race condition의 발생에도 불구하고, 프로그래머가 의도한 결과가 나온다면 thread-safe한 코드
ex) 이 코드는 여러 스레드에 의해서 동시에 수행되어도 문제 없다 ! → thread safe한 코드
thread - safe의 개념이 좀 두루뭉술하고 주관적인 느낌이 있다고 함..
여러 스레드가 실행될 때, 각 스레드가 어떤 자원을 이용하는지, 동시에 공유하는 자원은 무엇인지 알고 있는 것이 중요
iOS 환경에서 발생할 수 있는 race condition에 대해 조사하던 중 아주 좋은 글을 발견했다.
race condition이 일어날 수 있는 사례인데
https://sihyungyou.github.io/iOS-race-condition-in-swift/
정성껏 번역도 해두셔서, 덕분에 쉽게 이해할 수 있었다 :) 👍🏻
해당 글에서 제시한 사례에서 짚어볼 점은 크게 두가지.
1. 특정 함수에 대한 요청이 여러 번 있을 경우(같은 스레드에서)
-> queueing과 GCD를 이용해 race condition 문제를 어느정도 해결 할 수 있다.
(단, 모든 상황에서, thread-safe를 고려하고자 하는 것은 over engineering일 수 있다. 얻어지는 결과보다 개발 cost가 더 커질 수 있기 때문)
(출처: https://fluffy.es/solve-duplicated-cells/)
import UIKit
class ThumbnailCollectionViewCell: UICollectionViewCell {
@IBOutlet weak var thumbnailImageView: UIImageView?
var dataTask: URLSessionDataTask?
override func prepareForReuse() {
super.prepareForReuse()
thumbnailImageView?.image = nil
dataTask?.cancel()
dataTask = nil
}
}
func getImage(from url: URL, cell: ThumbnailCollectionViewCell, _ completion: @escaping (Data?) -> Void) {
cell.dataTask = networkManager.request(url, { [weak self] (data: Result<Data, NetworkError>) in
guard let `self` = self else { return }
switch data {
case .success(let data):
self.image = data
case .failure(let error):
print(error.localizedDescription)
DispatchQueue.main.async {
if (error as NetworkError) != .cancelError {
Toast.showToast(message: Setting.networkFailureString)
}
}
}
DispatchQueue.main.async {
completion(self.image)
}
}, nil)
cell.dataTask?.resume()
}
위의 코드에서 볼 수 있듯이, 각 셀은 dataTask를 프로퍼티로 가지고 있고 해당 cell이 reuse 될 때마다 dataTask에 값을 할당해주고, dataTask.resume() 함수를 통해 서버로부터 이미지를 받아오는 네트워킹 작업을 하게 된다.
이 때, 이전 요청이 끝나기 전 다음 요청이 들어오게 될 경우 더 이상 이전 요청에 대한 이미지를 받아올 필요가 없으므로 prepareForReuse() 함수에서 기존의 dataTask 작업을 취소하도록 하는 코드를 넣어주었다.
이를 통해, 특정 cell은 하나의 요청만을 수행하도록 하였고 race condition 현상을 해결할 수 있었다.