Thread-Unsafe의 예제
멀티쓰레드는 필드의 내용을 동시에 엑세스하여 잘못된 결과를 만들거나 출력할 수 있는데, 이를 Thread Unsafe 하다고 한다. 아래 예제는 여러 개의 쓰레드가 Thread-Safe하지 않은 메서드를 호출하는 예를 보여주고 있다. 10개의 쓰레드가 counter라는 필드를 동시에 쓰거나 읽는 샘플로서 한 쓰레드가 counter변수를 변경하고 읽기 전에 다른 쓰레드가 다시 counter변수를 변경할 수 있기 때문에 불확실한 결과를 출력하게 된다.
예제
using System;
using System.Threading;
namespace MultiThrdApp
{
class MyClass
{
private int counter = 1000;
public void Run()
{
// 10개의 쓰레드가 동일 메서드 실행
for (int i = 0; i < 10; i++)
{
new Thread(UnsafeCalc).Start();
}
}
// Thread-Safe하지 않은 메서드
private void UnsafeCalc()
{
// 객체 필드를 모든 쓰레드가
// 자유롭게 변경
counter++;
// 가정 : 다른 복잡한 일을 한다
for (int i = 0; i < counter; i++)
for (int j = 0; j < counter; j++) ;
// 필드값 읽기
Console.WriteLine(counter);
}
}
}
using System.Threading;
namespace MultiThrdApp
{
class MyClass
{
private int counter = 1000;
public void Run()
{
// 10개의 쓰레드가 동일 메서드 실행
for (int i = 0; i < 10; i++)
{
new Thread(UnsafeCalc).Start();
}
}
// Thread-Safe하지 않은 메서드
private void UnsafeCalc()
{
// 객체 필드를 모든 쓰레드가
// 자유롭게 변경
counter++;
// 가정 : 다른 복잡한 일을 한다
for (int i = 0; i < counter; i++)
for (int j = 0; j < counter; j++) ;
// 필드값 읽기
Console.WriteLine(counter);
}
}
}
C# lock 키워드
C#의 lock 키워드는 특정 블럭의 코드(Critical Section이라 부른다)를 한번에 하나의 쓰레드만 실행할 수 있도록 해준다. lock()의 파라미터에는 임의의 객체를 사용할 수 있는데, 주로 object 타입의 private 필드를 지정한다. 즉, private object obj = new object() 와 같이 private 필드를 생성한 후, lock(obj)와 같이 지정한다.
특히, 클래스 객체를 의미하는 this를 사용하여 lock(this)와 같이 잘못 사용할 수 있는데, 이는 의도치 않게 데드락을 발생시키거나 Lock Granularity를 떨어뜨리는 부작용을 야기할 수 있다. 즉, 불필요하게 객체 전체 범위에서 lock을 걸어 다수의 메서드가 공유 리소스를 접근하는데 Lock Granularity를 떨어뜨리는 효과가 있으며 (주: 이 경우 필드나 속성들이 잠긴다는 의미는 아니며, 이들 필드나 속성을 계속 읽고 쓸 수 있음) , 또한 만약 외부에서 해당 클래스 객체를 lock 하고 그 객체 안의 메서드를 호출한 경우 그리고 그 메서드안에서 다시 lock(this)를 하는 경우, 이미 외부에서 잡혀있는 lock이 풀리기를 메서드 내에서 계속 기다릴 것이므로 데드락이 발생할 수 있다.
그리고 Critical Section 코드 블록은 가능한 한 범위를 작게하는 것이 좋은데, 이는 필요한 부분만 Locking한다는 원칙에 따른 것이다.
특히, 클래스 객체를 의미하는 this를 사용하여 lock(this)와 같이 잘못 사용할 수 있는데, 이는 의도치 않게 데드락을 발생시키거나 Lock Granularity를 떨어뜨리는 부작용을 야기할 수 있다. 즉, 불필요하게 객체 전체 범위에서 lock을 걸어 다수의 메서드가 공유 리소스를 접근하는데 Lock Granularity를 떨어뜨리는 효과가 있으며 (주: 이 경우 필드나 속성들이 잠긴다는 의미는 아니며, 이들 필드나 속성을 계속 읽고 쓸 수 있음) , 또한 만약 외부에서 해당 클래스 객체를 lock 하고 그 객체 안의 메서드를 호출한 경우 그리고 그 메서드안에서 다시 lock(this)를 하는 경우, 이미 외부에서 잡혀있는 lock이 풀리기를 메서드 내에서 계속 기다릴 것이므로 데드락이 발생할 수 있다.
그리고 Critical Section 코드 블록은 가능한 한 범위를 작게하는 것이 좋은데, 이는 필요한 부분만 Locking한다는 원칙에 따른 것이다.
예제
using System;
using System.Threading;
namespace MultiThrdApp
{
class MyClass
{
private int counter = 1000;
// lock문에 사용될 객체
private object lockObject = new object();
public void Run()
{
// 10개의 쓰레드가 동일 메서드 실행
for (int i = 0; i < 10; i++)
{
new Thread(SafeCalc).Start();
}
}
// Thread-Safe 메서드
private void SafeCalc()
{
// 한번에 한 쓰레드만 lock블럭 실행
lock (lockObject)
{
// 필드값 변경
counter++;
// 가정 : 다른 복잡한 일을 한다
for (int i = 0; i < counter; i++)
for (int j = 0; j < counter; j++) ;
// 필드값 읽기
Console.WriteLine(counter);
}
}
//출력 예:
// 1001
// 1002
// 1003
// 1004
// 1005
// 1006
// 1007
// 1008
// 1009
// 1010
}
}
using System.Threading;
namespace MultiThrdApp
{
class MyClass
{
private int counter = 1000;
// lock문에 사용될 객체
private object lockObject = new object();
public void Run()
{
// 10개의 쓰레드가 동일 메서드 실행
for (int i = 0; i < 10; i++)
{
new Thread(SafeCalc).Start();
}
}
// Thread-Safe 메서드
private void SafeCalc()
{
// 한번에 한 쓰레드만 lock블럭 실행
lock (lockObject)
{
// 필드값 변경
counter++;
// 가정 : 다른 복잡한 일을 한다
for (int i = 0; i < counter; i++)
for (int j = 0; j < counter; j++) ;
// 필드값 읽기
Console.WriteLine(counter);
}
}
//출력 예:
// 1001
// 1002
// 1003
// 1004
// 1005
// 1006
// 1007
// 1008
// 1009
// 1010
}
}