lock & lock

개발을 하다보면 Lock에 대해서 생각해볼 일이 많다. 일단 Lock이 뭔지 기본 개념부터 알아보자. 추후에 어디에서 발생하는지 알아보고 각 경우에 대해서 어떻게 처리하고 대응하는지 알아보자.

Lock

컴퓨터과학에서 Lock(or mutex - 상호 배제)이라고 하면 synchronization primitive(동기화의 기본요소)이다. 여러 쓰레드의 실행이 있을 때 자원(resource)에 대한 접근을 제한하는 메커니즘이다. 락은 동시성 제어 정책을 시행하도록 만들어진 디자인이고, 가능한 다양한 방법을 통해 서로 다른 애플리케이션에 대해 여러 고유한 구현이 존재한다.

Granularity

lock의 단위를 소개 하기 전에 lock에 대한 3가지 컨셉을 먼저 이해하는게 좋다.

Database locks

먼저 데이터베이스에서의 트랜잭션에 대해서 먼저 알아보자.

데이터베이스에서의 락은 Transactions의 동기성을 보장한다. 즉, tx 프로세싱이 동시에 만들어질 때, two-phase locking을 이용해 동시적 실행이 순차적 실행처럼 실행되게 도와준다.

two-phase locking

동시성 제어와 관련된 내용이다. 데이터베이스와 트랜잭션 프로세싱에서, two-phase locking(2PL)은 연속성을 보장하는 동시성 제어 방식이다. 이건 다른 이름으로는 데이터베이스 트랜잭션 스케줄이라고도 불립니다. Locks에 관련된 프로토콜은 locks을 이용하여, 한 트랜잭션의 라이프 사이클동안 다른 트랜잭션이 같은 데이터를 접근 하는 것을 막습니다. 2PL 프로토콜에 의해 락은 2가지 단계로 적용 및 제거됩니다.

Expanding phase: locks are acquired and no locks are released.
Shrinking phase: locks are released and no locks are acquired.

기본 프로토콜에서는 공유 및 배타적 잠금의 두 가지 유형의 잠금을 사용하는데 기본 프로토콜을 개선하면 더 많은 잠금 유형을 사용할 수 있다. 프로세스를 잠ㄱ느ㅡㄴ 잠김 방식을 사용하면, 2PL은 프로세스를 차단하는 잠금을 사용하여 둘 이상의 트랜잭션을 상호 차단하여 교착 상태에 빠질 수 있다.

그러나 데드락 같은 부작용이 발생할 수 있다. 데드락은 트랜잭션 사이에서 락 순서를 지정해 방지하거나 waits-for graphs를 통해 확인할 수 있다. 데이터베이스 동기화를 위해 lock의 대안으로 글로벌 타임스탬프를 사용하는 방식으로 데드락 같은 것도 있다.

데이터베이스에서 여러 동시 사용자의 작업을 관리하는 데 사용되는 메커니즘이 있다. 그 목적은 손실된 업데이트 및 더티 읽기를 방지하는 것이다. 두 가지 유형의 잠금은 비관적 잠금과 낙관적 잠금이다.

낙관적 잠금을 사용하는 위치: 데이터에 대한 경합이 낮거나 데이터에 대한 읽기 전용 액세스가 필요한 환경에 적합한다. 낙관적 동시성은 .NET에서 광범위하게 사용되어 장기간 데이터 행을 잠글 수 없는 모바일 및 연결이 끊긴 애플리케이션의 요구 사항을 해결한다. 또한 레코드 잠금을 유지하려면 데이터베이스 서버에 대한 지속적인 연결이 필요하며 이는 연결이 끊긴 응용 프로그램에서는 불가능하다.

데이터베이스 락을 좀 더 깊게 이해해보기 위해 Recording lock에 대해서 알아보자.

Recording lock

Recording lock은 데이터베이스에서 동시에 데이터에 접근하는 것을 막는 기술이다. 일관된 결과를 얻기 위함이다. 간단한 방법으로, 레코드를 수정하고 있을 때 다른 사용자가 변경하지 못하게 하도록 하는 것이다. 데이터베이스 매니지먼트 이론에서, locking은 isolation을 구현하는데 사용된다. ACID에서 말하는 I이다.

Use of Locks

Exclusive locks

이름에서 알 수 있듯이, 일반적으로 레코드에 write하기 위한 목적으로 단일 엔티티가 독점적으로 보유한다. locking schema가 목록으로 표시되는 경우, 보유자 목록(holder list)에는 항목이 하나만 보여진다. Exclusive locks는 락이 필요한 다른 엔티티가 처리되지 않도록 효과적으로 차단하므로 아래 상황을 주의해야한다.

lock이 가능한 짧은 시간 동안 유지되도록 해야한다. 그러지 않으면 Deadlock에 빠질 수 있다.

lock을 가지고 있지 않은 사람은 라운드 로빈 방식으로 처리되는 목록이나 FIFO 큐에 보관될 수 있다. 이렇게 하면 가능한 모든 waiter가 락을 가지고 푸는데 같은 기회를 가진다.

Shared locks

Exclusive locks와 다르게 Shared locks는 보유자 목록(holder list)에 여러 항목이 포함될 수 있다. 이를 사용하면 모든 보유자가 잠금을 ㅎ재ㅔ할 때까지 레코드를 변경할 수 없음을 알고 모든 보유자가 내용을 read할 수 있다.

동일한 엔터티에 대한 잠금 요청이 대기열에 있는 경우 공유 잠금이 허용되면 대기열에 있는 모든 공유 잠금도 허용될 수 있다. 큐에서 배타적 잠금이 다음에 발견되면 모든 공유 잠금이 해제될 때까지 기다려야 한다. 배타적 잠금과 마찬가지로 이러한 공유 잠금은 가능한 한 최소 시간 동안 유지되어야 한다.

Python GlobalInterpreterLock

Overview

Python은 동시성 프로그램이 힘들다는 특성이 있는데 이는 GIL 때문이다. GIL은 언제나 단 하나의 쓰레드만 동작하도록 한다. 한번에 하나의 쓰레드만 실행가능하기 때문에, 쓰레드가 여러 프로세스를 사용하는게 불가능하다. 하지만 걱정할 필요는 없다. 다른 대체 수단들이 있긴 하다.

Thread-safety

파이썬 쓰레드는 같은 메모리를 공유한다. 동시에 여러 쓰레드가 실행되면, 우리는 공유 데이터에 접근하는 쓰레드가 뭔지 알 수 없다. 그러므로, 데이터 접근의 결과는 스케줄링 알고리즘에 의존한다.쓰레드들이 데이터를 접근/변경하기 위해 racing을 할 때, 이 알고리즘이 어떤 쓰레드가 실행될 지 결정한다. CPython에서, GIL은 여러 쓰레드가 한번에 파이썬 바이트코드를 실행하는 것을 막는 mutex(lock)이다. race conditions를 방지하고, 쓰레드의 안정성을 보장하는 역할을 한다. 간단히 말해서, 이 mutex CPython의 메모리 매니지먼트가 안전하지 않기 때문에 필수적이다.

Race condition 설명

a = 2
threadA: a = a + 2
threadB: a = a * 3

thread A -> threadB로 순서로 실행될 때랑 threadB -> threadA로 실행될 때의 결과는 다르다. 심지어 두 thread가 a=2의 값으로 실행될 수도 있다. Race condition이란 시스템의 행동이 시간이나 다른 사건들에 의해 연속성에 의존하는 시스템의 상태를 의미하게 된다. 이것들은 몇몇의 개발자들도 익숙하지 않은 개념이긴 하다. 또한 이거는 랜덤하게, 예측할 수 없는 행동을 하는 경향도 있다. 디버깅하기도 힘들고. 이러한 이유들 때문에 파이썬이 GIL을 사용한다.

나중에 이어서 경험했던 Lock들에 대해서 살펴보도록 하자. (golang and MySQL)

Reference

Discuss this post here.

Published: 2021-09-13

Tagged: Database

Archive