1. 코루틴(COROUTINE) 이란?
위 그림은 일반적인 메소드의 동작을 그림으로 표현한 것이다.
메소드를 호출하면 해당 메소드의 동작을 처리하고 다시 메소드를 호출한 부분으로 돌아간다.
일반적인 메소드라면 위와 같이 동작하는게 일반적인 원칙이다.
그리고 이렇게 동작하는 Foo를 예전에 Sub-Routine이라고 부른적이 있다.
그렇다면 코루틴은 어떤식으로 동작을 할까?
Co-Routine은 위와 같이 동작한다.
메소드를 호출하게 되면 메소드의 내용을 전부 실행하는게 아닌 일부만을 실행하고
다시 메소드를 호출한 부분으로 돌아온다.
또 코드가 실행되던 중 Foo( ); 를 만나게 되면 이번엔 Foo( ) 메소드의 처음 부분이 아닌
이전에 끊겼던 부분부터 이어서 실행을 하게 된다.
한번에 Foo( )의 모든 동작을 수행하는게 아닌 조금씩 나누어서 실행하게 된다.
언뜻보면 멀티스레드와 비슷해보이지만 아니다.
싱글스레드에서 대등하게 왔다갔다하며 동작을 하는데 이러한 것을 Co-Routine이라 한다.
코루틴은 C#뿐 아니라 여러 언어에서 유행하는 기법이다.
① C#과 COROUTINE
=> C# 언어는 COROUTINE을 완벽하게 지원하지 않음
=> Collection의 열거자(Enumerator)를 만들 때 COROUTINE 개념을 지원
=> LINQ 등이 COROUTINE 개념을 사용해서 작성되어 있음
2. COROUTINE을 사용한 열거자 만들기 #1
using System;
using System.Collections;
using System.Collections.Generic;
class Program {
static void Main() {
LinkedList<int> st = new LinkedList<int>();
st.AddFirst(10);
st.AddFirst(20);
st.AddFirst(30);
st.AddFirst(40);
st.AddFirst(50);
IEnumerator<int> e = st.GetEnumerator();
While(e.MoveNext()) {
Console.WriteLine(e.Current);
}
}
}
LinkedList를 만들었고 Add가 아닌 AddFirst로 요소를 넣었기에 아래 그림과 같은 모양이 됐을것이다.
그리고 열거자를 이용해 모든 요소를 출력해보는 코드이다.
결과는 50부터 10까지 출력될 것이다.
어렵지 않은 코드인데, 이 코드에서 LinkedList를 직접 만들어보자.
Generic으로 만들게 되면 복잡하므로 그냥 Int버전으로 만들어보자.
이름은 IntLinkedList로 하고 열거자도 Generic이 아닌 일반버전을 쓴다.
using System;
using System.Collections;
using System.Collections.Generic;
class Program {
static void Main() {
IntLinkedList st = new IntLinkedList();
st.AddFirst(10);
st.AddFirst(20);
st.AddFirst(30);
st.AddFirst(40);
st.AddFirst(50);
IEnumerator e = st.GetEnumerator();
While(e.MoveNext()) {
Console.WriteLine(e.Current);
}
}
}
이제 IntLinkedList를 구현해야 한다.
먼저 Node가 필요할텐데 Node는 데이터가 하나 가지고 있고 자기 다음을 가리키면 된다.
using System;
using System.Collections;
using System.Collections.Generic;
class Node {
public int data;
public Node next;
public Node(int d, Node n) { data = d; next = n; }
}
class Program {
static void Main() {
IntLinkedList st = new IntLinkedList();
st.AddFirst(10);
st.AddFirst(20);
st.AddFirst(30);
st.AddFirst(40);
st.AddFirst(50);
IEnumerator e = st.GetEnumerator();
While(e.MoveNext()) {
Console.WriteLine(e.Current);
}
}
}
Node클래스를 만들었다.
data를 가져야하니 int형 변수와 다음을 가리킬 Node를 만들고
생성자를 이용해 초기화 할 수 있게까지만 했다.
다음으로 IntLinkedList를 만들어야 한다.
IntLikedList는 첫 번째 노드를 가리키는 변수를 만들고
요소를 추가하기 위한 AddFirst를 만들어야 한다.
using System;
using System.Collections;
using System.Collections.Generic;
class Node {
public int data;
public Node next;
public Node(int d, Node n) { data = d; next = n; }
}
class IntLinkedList {
public Node head = null;
public void AddFirst(int data) {
head = new Node(data, head);
}
}
class Program {
static void Main() {
IntLinkedList st = new IntLinkedList();
st.AddFirst(10);
st.AddFirst(20);
st.AddFirst(30);
st.AddFirst(40);
st.AddFirst(50);
IEnumerator e = st.GetEnumerator();
While(e.MoveNext()) {
Console.WriteLine(e.Current);
}
}
}
잠깐 동작원리를 보면
22번 라인의 IntLinkedList st = new IntLinkedList(); 를 실행하면
heap에 null로 초기화 된 head가 생길 것이고 st가 stack에 생성되어 head를 가리키는 그림 1이 된다.
그런 다음 24번 라인의 st.AddFirst(10); 을 하게 되면
AddFirst메소드에서 new Node(data, head); 로 새로운 Node객체를 만드는데 data = 10과 head로 초기화 하게 된다.
근데 head는 초기값이 null이므로 그림 2와 같이 노드가 생기게 된다.
그리고 head = new Node(data ,head); 이므로 생성된 객체의 참조를 다시 head에 집어 넣었으므로
그림 3과 같은 모양이 된다.
그림 4는 AddFirst(20)메소드를 통해 new Node(data, head); 까지 실행되며 진행중인 모습이고
st.AddFirst(50); 까지 끝나게 되면 그림5과 같은 모양이 된다.
자, 다시 코드로 돌아가보면 이게 열거자를 통해 요소를 꺼내야 하는데
GetEnumerator(); 메소드가 없기에 에러가 난다.
① 모든 컬렉션은 열거자(Enumerator)를 꺼낼 수 있어야 한다.
=> IEnumerable(IEnumerable<T>) 인터페이스를 구현해야 한다.
using System;
using System.Collections;
using System.Collections.Generic;
class Node {
public int data;
public Node next;
public Node(int d, Node n) { data = d; next = n; }
}
class IntLinkedList {
public Node head = null;
public void AddFirst(int data) {
head = new Node(data, head);
}
public IEnumerator GetEnumerator() {
return new 열거자;
}
}
class Program {
static void Main() {
IntLinkedList st = new IntLinkedList();
st.AddFirst(10);
st.AddFirst(20);
st.AddFirst(30);
st.AddFirst(40);
st.AddFirst(50);
IEnumerator e = st.GetEnumerator();
While(e.MoveNext()) {
Console.WriteLine(e.Current);
}
}
}
이제 GetEnumerator() 메소드까지 만들었고 남은일은 열거자를 만들기만 하면 된다.
3. COROUTINE을 사용한 열거자 만들기 #2
using System;
using System.Collections;
using System.Collections.Generic;
class Node {
public int data;
public Node next;
public Node(int d, Node n) { data = d; next = n; }
}
class IntLinkedList {
public Node head = null;
public void AddFirst(int data) {
head = new Node(data, head);
}
public IEnumerator GetEnumerator() {
return new 열거자;
}
}
class Program {
static void Main() {
IntLinkedList st = new IntLinkedList();
st.AddFirst(10);
st.AddFirst(20);
st.AddFirst(30);
st.AddFirst(40);
st.AddFirst(50);
IEnumerator e = st.GetEnumerator();
While(e.MoveNext()) {
Console.WriteLine(e.Current);
}
}
}
이제 열거자를 만들어야 한다. 먼저 그림으로 보면 아래와 같다.
IEnumerator e = st.GetEnumerator();
를 통해 열거자 객체가 만들어지고 열거자는 노드를 이동하며 값을 꺼내면 된다.
위의 그림은 열거자가 3개를 가르키고 있지만.. 이동한다는 의미를 나타내기 위해 저렇게 했다.
한번에 하나만 가리킨다...
이제 코드로 돌아가서 열거자를 만들어야 하는데
위의 그림처럼 Node를 가리켜야 하므로 Node가 필요하다.
Node는 처음 위치를 가르킬 head(생성자)와 이동을 할 current 두 개를 만들기로 한다.
그리고 열거자가 갖고 있어야할 멤버함수와 속성을 만들어야 한다.
① 모든 열거자를 MoveNext(), Reset(), Current가 있어야 한다.
=> IEnumerator(IEnumerator<T>) 인터페이스를 구현해야 한다.
using System;
using System.Collections;
using System.Collections.Generic;
class Node {
public int data;
public Node next;
public Node(int d, Node n) { data = d; next = n; }
}
// 열거자
class IntLinkedListEnumerator : IEnumerator {
public Node head = null;
public Node current = null;
public IntLinkedListEnumerator(Node n) { head = n; }
public object Current => current.data;
public bool MoveNext() {
if (current == null)
current = head;
else
current = current.next;
return current != null;
}
public void Reset() {
current = null;
}
}
class IntLinkedList : IEnumerable {
public Node head = null;
public void AddFirst(int data) {
head = new Node(data, head);
}
public IEnumerator GetEnumerator() {
return new 열거자;
}
}
class Program {
static void Main() {
IntLinkedList st = new IntLinkedList();
st.AddFirst(10);
st.AddFirst(20);
st.AddFirst(30);
st.AddFirst(40);
st.AddFirst(50);
IEnumerator e = st.GetEnumerator();
While(e.MoveNext()) {
Console.WriteLine(e.Current);
}
}
}
13~32번 라인의 코드를 잘 살펴보자.
14번 : 생성자로 초기화 및 MoveNext() 메소드에서 current를 초기화 시켜주는 head를 선언한다.
15번 : 노드 간 이동하며 값을 전달해주는 current를 선언한다.
17번 : 생성자로 처음 만들 때 초기화 시켜준다.
19번 : Current 속성 구현. 값을 꺼내준다.
21~27번 : MoveNext() 멤버함수 구현. current가 null이라면 head로 초기화 시켜주고
아니라면 다음 노드를 가리킨다.
MoveNext()는 끝 노드에서는 false를 반환해야 하므로 null이 아닐 땐 true, null일땐 false를 반환한다.
29~31번 : Reset() 멤버함수 구현. current를 null로 초기화 시키면 MoveNext시 다시 head부터 시작하게 된다.
이제 완성되었다. 만든 열거자를 사용하면 된다.
using System;
using System.Collections;
using System.Collections.Generic;
class Node {
public int data;
public Node next;
public Node(int d, Node n) { data = d; next = n; }
}
// 열거자
class IntLinkedListEnumerator : IEnumerator {
public Node head = null;
public Node current = null;
public IntLinkedListEnumerator(Node n) { head = n; }
public object Current => current.data;
public bool MoveNext() {
if (current == null)
current = head;
else
current = current.next;
return current != null;
}
public void Reset() {
current = null;
}
}
class IntLinkedList : IEnumerable {
public Node head = null;
public void AddFirst(int data) {
head = new Node(data, head);
}
public IEnumerator GetEnumerator() {
return new IntLinkedListEnumerator(head);
}
}
class Program {
static void Main() {
IntLinkedList st = new IntLinkedList();
st.AddFirst(10);
st.AddFirst(20);
st.AddFirst(30);
st.AddFirst(40);
st.AddFirst(50);
IEnumerator e = st.GetEnumerator();
While(e.MoveNext()) {
Console.WriteLine(e.Current);
}
}
}
다시 실행해보면 아무문제 없이 50 -> 10까지 잘 출력되는 것을 볼 수 있다.
여기까지 아직 코루틴이 나온것은 아니고 컬렉션을 만들고 열거자를 만드는 과정이였다.
다음에 위의 코드를 코루틴을 이용해 변경해보도록 하자.
'프로그래밍 > C#' 카테고리의 다른 글
[C#] 코루틴 (COROUTINE) (3) (0) | 2020.05.05 |
---|---|
[C#] 코루틴 (COROUTINE) (2) (0) | 2020.05.05 |
[C#] Collection Method (0) | 2020.04.27 |
[C#] 열거자 (Enumerator) (0) | 2020.04.26 |
[C#] Collection과 Interface (0) | 2020.04.26 |