게임 서버 프로그래밍

Lock: 멀티쓰레딩 환경에서의 동기화

유니얼 2024. 3. 18. 17:41
728x90

C# 게임 서버 만들기

멀티쓰레딩 프로그래밍은 현대 소프트웨어 개발에서 매우 중요한 부분입니다. 이는 애플리케이션의 성능을 향상시키고, 자원을 효율적으로 사용할 수 있도록 도와줍니다. 그러나 멀티쓰레딩 환경에서는 데이터의 동시 접근으로 인한 경쟁 상태(race condition)가 발생할 수 있습니다. 이러한 문제를 해결하기 위한 기본적인 도구 중 하나가 바로 lock입니다. 이 글에서는 lock의 개념, 사용법, 그리고 멀티쓰레딩 프로그래밍에서의 중요성에 대해 알아보겠습니다.

Lock의 개념

lock은 특정 코드 영역을 한 번에 하나의 쓰레드만 실행할 수 있도록 보호하는 C#의 키워드입니다. 이를 통해 공유 자원에 대한 동시 접근을 방지하고, 데이터의 일관성 및 무결성을 유지할 수 있습니다. lock은 내부적으로 모니터(Monitor)를 사용하여 구현됩니다.

Lock 사용법

lock을 사용할 때는 잠금을 적용할 객체가 필요합니다. 이 객체는 잠금의 대상이 되며, 일반적으로 private 필드로 선언됩니다. 다음은 lock을 사용하는 기본적인 패턴입니다.

private readonly object _lock = new object();

public void SafeUpdate()
{
    lock (_lock)
    {
        // 공유 자원에 대한 안전한 업데이트를 수행합니다.
    }
}

이 코드에서 _lock 객체는 잠금을 위해 사용되며, lock 문 내부에 있는 코드는 한 번에 하나의 쓰레드만 실행할 수 있습니다.

Lock의 중요성

멀티쓰레딩 환경에서 공유 자원에 대한 동시 접근은 데이터의 불일치를 초래할 수 있습니다. 예를 들어, 여러 쓰레드가 동시에 같은 데이터를 수정하려고 할 때, 최종 데이터 상태는 예측할 수 없게 됩니다. lock을 사용하여 이러한 영역을 보호함으로써, 어떤 쓰레드도 다른 쓰레드가 작업을 완료하기 전에 해당 자원을 수정할 수 없게 됩니다.

Lock 사용 시 주의 사항

  • 데드락(Deadlock): 두 개 이상의 쓰레드가 서로를 기다리게 되어, 영원히 진행할 수 없는 상태에 빠지는 것을 뜻합니다. lock 사용 시 서로 다른 잠금을 순환적으로 기다리지 않도록 주의해야 합니다.
  • 성능 저하: lock이 과도하게 사용될 경우, 쓰레드의 병렬 실행이 제한되어 성능이 저하될 수 있습니다. 따라서 필요한 최소한의 영역에만 lock을 적용하는 것이 좋습니다.
  • 잠금 객체 선택: lock을 위한 객체는 해당 잠금과 직접 관련된 private 객체를 사용하는 것이 좋으며, 공유 자원 자체나 typeof 표현을 사용하는 것은 피해야 합니다.

예제 코드

using System;
using System.Threading;

class BankAccount
{
    private int balance;
    private readonly object balanceLock = new object();

    public BankAccount(int initialBalance)
    {
        balance = initialBalance;
    }

    // 예금
    public void Deposit(int amount)
    {
        lock (balanceLock)
        {
            balance += amount;
            Console.WriteLine($"Deposited {amount}, New Balance: {balance}");
        }
    }

    // 출금
    public void Withdraw(int amount)
    {
        lock (balanceLock)
        {
            if (balance >= amount)
            {
                balance -= amount;
                Console.WriteLine($"Withdrew {amount}, New Balance: {balance}");
            }
            else
            {
                Console.WriteLine("Withdrawal attempt failed due to insufficient funds.");
            }
        }
    }

    // 현재 잔액 조회
    public int GetBalance()
    {
        lock (balanceLock)
        {
            return balance;
        }
    }
}

class Program
{
    static void Main(string[] args)
    {
        BankAccount account = new BankAccount(1000);

        // 출금을 시도하는 쓰레드
        Thread[] threads = new Thread[10];
        for (int i = 0; i < threads.Length; i++)
        {
            threads[i] = new Thread(() => account.Withdraw(100));
            threads[i].Start();
        }

        // 모든 쓰레드가 완료될 때까지 대기
        for (int i = 0; i < threads.Length; i++)
        {
            threads[i].Join();
        }

        Console.WriteLine($"Final Balance: {account.GetBalance()}");
    }
}

결론

lock은 멀티쓰레딩 환경에서 데이터의 일관성과 무결성을 유지하는 기본적이면서도 강력한 도구입니다. 올바르게 사용된다면 멀티쓰레딩 애플리케이션의 안정성을 크게 향상시킬 수 있습니다. 그러나 데드락의 위험과 성능 저하 문제를 고려하여 신중하게 사용해야 합니다. lock의 이해와 적절한 사용은 멀티쓰레딩 프로그래밍의 성공을 위해 필수적인 요소입니다.

반응형