728x90
반응형
1. 핵심 정리
using System;
using System.IO;
class Program {
static void Main() {
int[] arr = new int[] {1, 2, 3, 4, 5};
FileStream fs = new FileStream("a.txt",
FileMode.CreateNew,
FileAccess.ReadWrite,
FileShare.None);
// ...
fs.Dispose();
}
}
C와 C++은 위와 같이 new를 통해 자원을 할당하면
반드시 자원을 반납해야 한다.
C#은 자동으로 메모리 관리를 해주기 때문에 new로 자원을 할당해도 반납하지 않아도 된다.
하지만 C#에서도 파일 같은 것을 오픈하면 Dispose를 통해 반납을 해주어야 한다.
왜 Dispose를 쓰고, 어떤 의미인지에 대해 알아보자.
① C#과 자원관리
2. Garbage Collector
using System;
using System.IO;
class Car {
public void Go() { Console.WriteLine("Car Go"); }
}
class Program {
static void Main() {
Car c1 = new Car();
Car c2 = new Car();
c2 = null;
c1.Go();
}
}
먼저 Dispose를 알기전 Garbage Collector에 대해 알아야 한다.
위의 코드로 살펴보자.
먼저 c1, c2의 두개의 객체를 만들었다.
그러면 아래 그림과 같이 메모리에 자원이 할당될 것이다.
여기서 13번 라인의 c2=null 을 수행하면
c2의 참조가 끊길 것이다.
그럼 원래 c2가 가리키던 객체는 어떠한 참조도 받지 않게 되었다.
그럼 메모리에서 자동으로 삭제되어야 하는데
즉시 삭제되지 않는다.
왜냐하면, 아직까지 Heap에는 충분한 공간이 있다.
그리고 메모리를 수집하려면 어떠한 일을 해야 한다는건데 당장 공간이 부족하지도 않은데
수집하려는 행동 자체가 성능저하가 될 수 있다.
그래서 메모리가 부족하거나, 사용자가 직접 Garbage Collection을 돌리거나 하는 등의 상황에서만
메모리가 수집된다.
이것을 확인해보려면 소멸자를 통해 확인해볼 수 있다.
C#에서는 흔히 소멸자를 finalizer라고 부른다.
using System;
using System.IO;
class Car {
public void Go() { Console.WriteLine("Car Go"); }
// finalizer
~Car() { Console.WriteLine("~Car"); }
}
class Program {
static void Main() {
Car c1 = new Car();
Car c2 = new Car();
c2 = null;
c1.Go();
}
}
소멸자를 추가한 코드이다.
C나 C++은 16번 라인의 c2 = null; 을 수행할 때 소멸자가 호출되어야 하지만
C#은 실행해보면 c1.Go() 이후 프로그램이 끝나고 소멸자가 호출되는걸 볼 수 있다.
실행 결과
Car Go
~Car
~Car
여기서 사용되지 않는 메모리를 즉시 해지하려면 강제로 메모리 수집기를 돌리면 된다.
GC가 Garbage Collector니까 GC의 멤버함수인 Collect()를 통해 수집할 수 있다.
코드에 c2 = null과 c1.Go 사이에 GC.Collect()를 추가해서 돌려보자.
하지만 예상과는 다르게 아직도 소멸자가 Go 이후에 호출된다.
왜냐하면 소멸자가 Collect()시 바로 호출되는게 아니라 다른 스레드를 만들어 수행되기 때문이다.
그래서 소멸자를 먼저 호출하려면 WaitForPendingFinalizers()라는 메소드를 통해 할 수 있다.
using System;
using System.IO;
class Car {
public void Go() { Console.WriteLine("Car Go"); }
// finalizer
~Car() { Console.WriteLine("~Car"); }
}
class Program {
static void Main() {
Car c1 = new Car();
Car c2 = new Car();
c2 = null;
GC.Collect();
GC.WaitForPendingFinalizers();
c1.Go();
}
}
실행 결과
~Car
Car Go
~Car
① 참조 변수가 더 이상 참조하지 않아도 메모리는 즉시 수집되지 않는다.
=> 특정 조건(메모리 부족, 강제로 수집 등)을 만족할 때 메모리 수집기가 동작한다.
3. Dispose()를 써야하는 이유
using System;
class XFileStream {
private IntPtr handle;
public XFileStream(string path) {
// Win32 API 인 CreateFile()을 사용해서 파일 오픈
}
~XFileStream() {
// Win32 API 인 CloseHandle()을 사용해서 파일 닫기
}
}
class Program {
static void Main() {
XFileStream xfs = new XFileStream("a.txt");
xfs = null;
}
}
간단하게 이해를 위한 FileStream을 흉내낸 XFileStream이란 클래스를 만들었다.
사용 방법은 XFileStream의 객체를 만들고 파일이름을 넘기면 해당 파일을 오픈한다고 하자.
그럼 XFileStream의 생성자에서 파일을 오픈하면 될텐데
C#에서 파일을 오픈하는 것은 C언어로 되어있는 Windows API인 CreateFile을 호출하는 수밖에 없다.
C#에서 C언어를 호출하는 기법을 사용하는 것이다. 이러한 기법을 플랫폼 인보크라고 부른다.
어쨋든 오픈한 파일을 어딘가에 handle을 보관하고 있다가 CloseHandle을 통해 닫아야한다.
닫지 않으면 이 파일이 열려있는 동안 다른 프로그램에서 이 파일에 접근할 수 없기 때문이다.
그래서 C하고 C++에선 가장 좋은 방법이 생성자 - 열기(할당) / 소멸자 - 닫기(반납) 였다.
그런데 위에서 본것과 같이 C#에선 더이상 사용되지 않는다고 바로 메모리가 수집되지 않는다.
결국 문제가 언제 소멸자가 호출되어 닫힐지 모른다는 것이다.
그래서 다른 곳에서 파일을 써야하는데 못 쓰는 상황이 생길 수 있다.
4.
using System;
using System.IO;
class Program {
public static void Foo() {
FileStream fs1 = new FileStream("a.txt", FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None);
}
static void Main() {
Foo();
Console.WriteLine("Finish Foo");
FileStream fs2 = new FileStream("a.txt", FileMode.Open, FileAccess.ReadWrite, FileShare.None);
}
}
Foo함수 내에서 파일을 만들고 오픈한다. 그리고 FileShare의 옵션이 None이므로 열고있는 동안 다른 프로세스에서 접근이 불가하다.
그리고 Foo함수가 다 끝나면 지역변수는 사용되지 않기에 파괴될 것이고
객체의 참조가 없으므로 나중에 GC가 돌게되면 메모리가 수집될 것이다.
하지만 앞선 내용처럼 GC는 바로 수집되는게 아닌 메모리가 부족하거나 강제로 돌려야 수집이 된다.
근데 Main에서 똑같은 파일을 다시 연다고 해보자.
메모리가 즉시 수집되었다면 똑같은 파일을 열어도 (Foo)생성 -> (Foo)닫기 -> (Main)열기라 문제가 없지만
메모리가 즉시 수집이 안되므로 실행해보면 Finish Foo까진 출력되지만 이후 예외를 던진다.
이걸 해결하려면 2번처럼 GC를 그냥 바로 돌려버리면 된다.
using System;
using System.IO;
class Program {
public static void Foo() {
FileStream fs1 = new FileStream("a.txt", FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None);
}
static void Main() {
Foo();
GC.Collect();
GC.WaitForPendingFinalizers();
Console.WriteLine("Finish Foo");
FileStream fs2 = new FileStream("a.txt", FileMode.Open, FileAccess.ReadWrite, FileShare.None);
}
}
위와 같이 GC를 강제로 돌면 문제를 해결할 수 있다.
근데 사용자가 GC를 직접 돌리는 코드는 좋지 못한 코드이다. (성능의 문제 등)
그래서 Foo()함수에서 Foo()함수가 끝나기 전에 Dispose()를 하는게 좋다.
using System;
using System.IO;
class Program {
public static void Foo() {
FileStream fs1 = new FileStream("a.txt", FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None);
fs1.Dispose();
}
static void Main() {
Foo();
Console.WriteLine("Finish Foo");
FileStream fs2 = new FileStream("a.txt", FileMode.Open, FileAccess.ReadWrite, FileShare.None);
}
}
파일을 오픈하면 Dispose를 안불러도 분명 언젠간 알아서 닫힐테지만
문제는 닫히는 시점이 객체를 사용하지 않게 될 때 즉시 수집되는게 아닌 언제 닫힐지 모른다는것이다.
그래서 명확하게 닫기 위해서 Dispose를 불러주는게 좋은 코드라 할 수 있다.
728x90
반응형
'프로그래밍 > C#' 카테고리의 다른 글
[C#] C#으로 네이버웍스 봇 만들기 (0) | 2023.11.01 |
---|---|
[C#] 쓰레기 수집기 (Garbage Collector) (0) | 2020.05.16 |
[C#] Task 클래스 (0) | 2020.05.10 |
[C#] 스레드 클래스 멤버 (0) | 2020.05.08 |
[C#] 스레드 개념 (Thread) (0) | 2020.05.08 |