Race Condition(경쟁 상태) + Thread safety in iOS

soooooyeon·2021년 5월 2일
1

Race Condition(경쟁 상태)

  • 여러 스레드 or 프로세스가 한정된 공유 자원에 동시에(concurruntly) 접근하는 경우

  • 경쟁상태는 데이터의 불일치(inconsistency) 문제 를 야기할 수 있다

  • 경쟁 상태를 다루기 위해서는 동기화(synchronize) 되어야 함 — > 더 자세한 건 다음 토픽에서 다룸

  • 경쟁 상태의 예시

    • ex) 스레드 1과 스레드 2가 변수 x의 값을 동시에 다른 값으로 바꾸는 경우
  • critical section에서 race condition이 발생할 수 있음

  • critical section에는 여러 프로세스가 동시에 접근할 수 있기 때문

os에서 Race condition이 일어나는 경우

 → 주로, user mode일 때 보다(일반 프로세스가 cpu를 잡고 사용 할 경우) 커널모드일 때 race condition이 일어난다
 왜? 커널모드에서는, 커널에 있는 자원을 여러 프로세스가 공유할 수 있기 때문 

1. process가 system call을 해서, kernel mode로 수행중인데 context switch가 일어나는 경우

ex) 프로세스 a가 커널모드로 작업을 수행하면서, count 변수의 값을 읽어오고 증가시키려고 하는 와중에

context switch가 발생함. 프로세스 b에서도 count 변수의 값을 변경하게 됨.

이 때, 프로세스 b에 의해 변경된 count 값은 반영되지 않게 된다.

왜? 이미 커널에서는 (프로세스 a의 작업을 할 때) 변경되기 이전 count의 값을 가져온 상태이기 때문

(참고: 컴퓨터에서의 연산은, 메모리에 있는 값을 레지스터로 옮겨 계산하고 다시 레스터로 저장하는 방식으로 이루어짐)

해결 방안

  • 커널모드에서 수행 중일 때는 cpu를 뺏지 않는다 → 커널 모드에서 user 모드로 바뀔 때 cpu를 빼앗음

2. 커널모드로 작업 중에, 인터럽트가 발생한 경우


(출처: 반효경 교수님 KOCW 운영체제 강의)

해결 방안

  • 커널에서 공유 변수에 접근하고 있을 때에는, 인터럽트를 받지 아니함

3. multiprocessor에서, 커널에 있는 shared memory에 접근할 때

  • multiprocessor = cpu가 여러개
  • 2번 방법에서 사용한 disable interrupt로 해결되지 않음

해결방안

  • 한번에 하나의 CPU만이 커널에 접근할 수 있도록 함
  • 커널 내부에 있는 공유 데이터에 접근할 때마다 lock/unlock을 하는 방법

Thread-safe(스레드 안전)

  • 멀티 스레드 프로그래밍에서, 변수나 객체, 함수 등의 자원이 여러 스레드에 대해 동시에 접근되어도 정상적으로 프로그램이 동작하는 것

    → thread - safe하다고 해서 race condition으로부터 자유롭다는 뜻 아님.

    → race condition의 발생에도 불구하고, 프로그래머가 의도한 결과가 나온다면 thread-safe한 코드

    ex) 이 코드는 여러 스레드에 의해서 동시에 수행되어도 문제 없다 ! → thread safe한 코드

  • thread - safe의 개념이 좀 두루뭉술하고 주관적인 느낌이 있다고 함..

  • 여러 스레드가 실행될 때, 각 스레드가 어떤 자원을 이용하는지, 동시에 공유하는 자원은 무엇인지 알고 있는 것이 중요


Race condition in iOS

iOS 환경에서 발생할 수 있는 race condition에 대해 조사하던 중 아주 좋은 글을 발견했다.
race condition이 일어날 수 있는 사례인데
https://sihyungyou.github.io/iOS-race-condition-in-swift/
정성껏 번역도 해두셔서, 덕분에 쉽게 이해할 수 있었다 :) 👍🏻

해당 글에서 제시한 사례에서 짚어볼 점은 크게 두가지.
1. 특정 함수에 대한 요청이 여러 번 있을 경우(같은 스레드에서)

  • 아직 첫번째 요청에 대한 작업이 끝나지 않았을 때, 두번째 요청이 들어오면 요청마다 결과가 달라질 수 있는데 이는 사용자가 기대한 결과가 아닐 수 있다.
  1. 멀티 스레드 환경에서 각각의 스레드가 같은 함수를 동시에 요청했을 경우

-> queueing과 GCD를 이용해 race condition 문제를 어느정도 해결 할 수 있다.
(단, 모든 상황에서, thread-safe를 고려하고자 하는 것은 over engineering일 수 있다. 얻어지는 결과보다 개발 cost가 더 커질 수 있기 때문)

내가 프로젝트하면서 직접 겪은 Race Condition

  • 인턴 프로젝트를 진행하며 경험했던 일이다.
    당시 테이블뷰를 이용해 내 주변의 식당들 정보를 띄워줘야 했는데, 셀에 이미지뷰가 포함되어 있어 스크롤을 내릴 때마다 서버로부터 이미지를 받아와야 했다.
  • 그런데, 스크롤을 내릴 때마다 각 셀의 이미지뷰에 해당 셀과는 관련 없는 이미지 여러장이 먼저 로딩되고 그 다음에 제대로된 이미지가 로딩되는 현상이 발생했다.
  • 원인은 tableView의 dequeuereusablecell 즉, 테이블 뷰에서는 메모리 사용량을 최적화 하기 위해 한정된 갯수의 cell 객체 만을 큐에 넣어두고 이들을 반복해서 재사용하는데, 특정 셀에 대한 이전 작업이 마무리 되기 전에, 그 다음 작업에 대한 요청이 들어오기 때문에 생기는 현상이었다.
  • 즉, 하나의 cell에 대해 여러 요청이 들어오게 되는 race condition 현상이었다.


(출처: 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 현상을 해결할 수 있었다.

profile
Junior iOS developer

0개의 댓글