C# 코딩연습 멀티쓰레드와이벤트 2009-05-18 김태현 (kimgwajang@hotmail.com)
I. 서 제블로그 1 의카테고리중에.NET Tip of The Day' 라는것이있는데, 동명의사이트 2 에실린유용한닷넷팁들을번역하여소개하는포스팅이모여있습니다. 이중 Correct event invocation 3 라는제목의포스트가있는데, 멀티쓰레드환경에서이벤트를호출하는올바른방법에관해서설명을하고있습니다. 원문의설명자체는간단합니다. 이벤트를호출하는통상적인코드는보통아래와같은데, 1 public event EventHandler SelectedNodeChanged; 2 3 protected virtual void OnSelectedNodeChanged(object sender, EventArgs e) 4 { 5 if (SelectedNodeChanged!= null) 6 SelectedNodeChanged(this, e); 7 } 코드 1 멀티쓰레드환경에서는이코드가문제를일으킬수있기때문에, 아래와같이변경하는것이 ( 멀티쓰레드환경에서의 ) 올바른이벤트호출 이라는것입니다. 1 public event EventHandler SelectedNodeChanged; 2 3 protected virtual void OnSelectedNodeChanged(object sender, EventArgs e) 4 { 5 EventHandler handler = SelectedNodeChanged; 6 7 if (handler!= null) 8 handler(this, e); 9 } 코드 2 이코드를이해하기위해서는몇가지배경지식이필요한데, 이번 C# 코딩연습에서는이를 다루어보고자합니다. 1 http://kimgwajang.tistory.com 2 http://dotnettipoftheday.org 3 http://dotnettipoftheday.org/tips/correct-event-invocation.aspx 1
II. 불변 (immutable) 객체 불변객체란한번생성되면그상태가변하지않는객체를말합니다. 4 닷넷프레임웍에도몇 가지불변객체들이있는데, 대표적으로 System.String 형을들수있습니다. 여기서돌발퀴즈! 1 string a = " 원본 "; 2 string b = a; 3 4 a = " 수정 "; 5 6 Console.WriteLine(b); 코드 3 화면에 원본 이찍힐까요, 수정 이찍힐까요? 결과는아래와같습니다. 원본계속하려면아무키나누르십시오... 조금이상하지않습니까? 아시다시피 string 은참조형 5 입니다. 그래서 1 번에서 a 는메모리의 어딘가에생성된 원본 이라는데이터에대한참조입니다. 그림 1 2 번라인에서 b 역시참조인데, 이의값은 a 가가리키는값, 즉 원본 입니다. 4 http://en.wikipedia.org/wiki/immutable_object 5 값형과참조형을구분하고각각의성격을정확하게파악하는것은닷넷에서너무너무중요한일입니다. 박싱과언박싱이제네릭과어떻게연결되는지를이해하는수준이되면기초가탄탄하다고할수있을것같습니다. 2
그림 2 이상태에서 4 번라인에서 a 가가리키는값을 수정 이라고변경했습니다. 그림 3 이상태에서 b 의값을출력하면당연히 수정 이나와야할텐데, 결과는 원본 이찍혔습니다. 무엇이잘못된것일까요? String 형이일반적인참조형이라면이설명이맞습니다만, 닷넷프레임웍의설계자들은 String 을특수한참조형인불변객체로구현하였습니다. 불변객체는생성되고나면값이변하지않습니다. 하지만그림 2 와같이 b 가 a 와같은객체를가리킨다면그림 3 과같이 b 를이용해 a 의값을변경할수있게됩니다. 이를막기위해불변객체는 2 번라인과같이참조가복사 ( 얕은복사 ) 될때내부적으로객체자체를복사 ( 깊은복사 ) 하여버립니다. 즉 2 번라인이실행되고나면그림 2 가아니라아래와같은상태가됩니다. 3
그림 4 b 가 a 와같은참조를가지는것이아니라 a 가참조하는객체 (100 번지 ) 를복사하여새로만든 후 (200 번지 ) b 는이객체를참조하게됩니다. 따라서처음생성된 100 번지에있는객체에 접근할수있는참조는여전히 a 밖에없게됩니다. 이상태에서 4 번라인이실행되어 a 가참조하는객체를 수정 으로변경하면그림 3 이아니라 아래와같은모습이됩니다. 그림 5 메모리를조금더소비하지만성능을높일수있다는이유와멀티쓰레드환경에서쓰레드에 안전한객체를만들수있다는등의이유로닷넷프레임웍에는다수의불변객체가구현되어 있습니다. 오늘의주제인대리자역시불변객체입니다. 코드를볼까요? 01 private static void Main(string[] args) 02 { 03 Action action1 = new Action(Print1); 04 Action action2 = action1; 05 06 action1 = null; 07 08 action2(); 4
09 } 10 11 public static void Print1() 12 { 13 Console.WriteLine("1"); 14 } 코드 4 1 이출력될까요? 아니면널참조예외가발생할까요? 1 계속하려면아무키나누르십시오... Action 은 Func 과더불어닷넷프레임웍 2.0 에서추가된대리자입니다. 사용 자정의대리자형식을만들기보다는 Action 혹은 Func 대리자를사용하실 것을추천합니다. 참고로 Action 은제네릭과제네릭이아닌두가지형식이 각각존재합니다. 3 번라인에서 Print1 을가리키는대리자객체를만들고그에대한참조를 action1 에지정합니다. 4 번라인에서는 action2 에 aciton1 을복사하는데, 이때 Action 은대리자이고대리자는불변형이기때문에 action1 이가리키는참조를단순히 action2 에할당 ( 복사 ) 하는것이아니고, action1 이참조하는것과동일한객체를새로만들고 action2 에는그참조를지정합니다. 그림 4 와동일한상황이되겠습니다. 6 번라인에서 action1 이참조하는값을 null 이라고하면, ( 위그림에서의주소를예로들자면 ) action1 과 action2 는각각 100 번지와 200 번지에있는서로다른객체를가리키고있는데, 100 번지에있는객체만이해제되기때문에 200 번지에있는대리자객체를실행하는것은널참조예외를일으키지않습니다. 물론불변객체는닷넷에만있는개념은아닙니다. 여러가지언어에서불변객체의개념과 구현은오랫동안발전되어왔습니다. 그만큼불변객체에대해서는깊이있는연구와학습이 필요할테지만, 여기서는단한문장만기억하고넘어가시기바랍니다. 대리자는불변객체입니다. 5
III. 멀티쓰레드에서의이벤트호출 이번에는멀티쓰레드환경에서의이벤트호출에대해서살펴봅시다. 01 internal class Program 02 { 03 private static void Main(string[] args) 04 { 05 Person person = new Person(); 06 person.agechanged += PrintAge; 07 person.increaseage(); 08 } 09 10 public static void PrintAge(object sender, EventArgs e) 11 { 12 Person person = (Person) sender; 13 Console.WriteLine(person.Age); 14 } 15 } 16 17 public class Person 18 { 19 public int Age { get; private set; } 20 21 public event EventHandler AgeChanged; 22 23 protected virtual void OnAgeChanged(object sender, EventArgs e) 24 { 25 if (AgeChanged!= null) 26 AgeChanged(this, e); 27 } 28 29 public void IncreaseAge() 30 { 31 Age++; 32 33 OnAgeChanged(this, new EventArgs()); 34 } 35 } 코드 5 Person 클래스의 AgeChanged 이벤트에 PrintAge 라는이벤트핸들러를등록시키고 IncreaseAge 메서드를호출합니다. IncreaseAge 메서드가실행되면나이를 1 살증가시키고 OnAgeChanged 라는호출하는데, OnAgeChanged 내부에서는 AgeChanged 에등록된이벤트핸들러가있는지 (AgeChanged!= null) 검사한후, 있다면이벤트핸들러를실행합니다. 6
그런데이코드는멀티쓰레드환경에서는문제를일으킬수있습니다. 바로 25 번라인에서 26 번라인으로넘어가는순간이문제가될수있는데요. 25 번라인에서 AgeChanged 가널인지를검사합니다. 널이아님을확인한후 26 번라인을실행하려는데, 다른쓰레드가 AgeChanged 에등록된이벤트핸들러를해제해버렸습니다. 그리고다시원래쓰레드로전환되어 26 번라인을실행하면이제 AgeChanged 는널이기때문에널참조예외가발생합니다. 이를코드로표현하면다음과같습니다. 01 protected virtual void OnAgeChanged(object sender, EventArgs e) 02 { 03 if (AgeChanged!= null) 04 { 05 // 다른쓰레드로전환 06 AgeChanged -= Program.PrintAge; 07 // 원래쓰레드전환 08 09 AgeChanged(this, e); 10 } 11 } 코드 6 여기서 6 번라인은다른쓰레드에의해서실행되었음을시뮬레이션하는코드입니다. 이문제를해결하기위해서는일반적인패턴은다음과같습니다. 01 protected virtual void OnAgeChanged(object sender, EventArgs e) 02 { 03 EventHandler handler = AgeChanged; 04 05 if (handler!= null) 06 { 07 // 다른쓰레드로전환 08 AgeChanged -= Program.PrintAge; 09 // 원래쓰레드전환 10 11 handler(this, e); 12 } 13 } 코드 7 7
먼저확실히짚어둘것은, AgeChanged 의형식은 EventHandler 라는것입니다. AgeChanged 가 이벤트라는것은문법적인구분이며, AgeChanged 의형은확실히대리자인 EventHandler 입니다. ( 코드 5 의 21 번라인 ) 3 번라인에서 handler 라는새로운대리자변수를만들고기존 AgeChanged 객체의값을할당합니다. 이는 Action action2 = action1; 와동일한코드입니다. 위에서살펴봤듯이, 이때 handler 에는 AgeChanged 로부터복사된새로운대리자객체의참조가담기게됩니다. 즉, AgeChanged 와 handler 는각각의객체를따로가지고있다는것입니다. 그렇다면나머지코드는이해하기쉽습니다. 8 번라인에서의시뮬레이션과같이 AgeChanged 에등록되어있던이벤트핸들러가해제되더라도 AgeChanged 가아닌 handler 에대해널검사를하고이벤트를발생시키기때문에이제널참조예외를걱정할필요가없게된것입니다. 8