Thread
전체 목차: Thread 개념/필요성/장점/오버헤드
C#에서 스레드를 만들고 사용하는 다양한 방법
- Thread 클래스
- Thread Pool
- Task
- Async IO
- async,await 키워드
Thread란?
- 코드를 실행하는 실행 흐름
- 프로세스 생성시 한개의 스레드가 생성 (주스레드-Primary Thread)
- 사용자가 추가로 생성할수있다.
왜 스레드를 만드는가?
- 응답성이 좋은 UI프로그램. 주스레드에서는 사용자의 이벤트대기. 시간이 오래걸리는 작업은 다른 스레드로 진행한다.
- 성능좋은 프로그램: CPU가 4개라면 4개의 스레드를 사용하는것이 가장 좋다.
스레드의 오버헤드
- CPU가 하나만 있다고 가정.
- 2개의 스레드가있다면 왔다갔다하면서 실행. (왼쪽/오른쪽 스레드)
- 스레드 생성시 OS는 내부적으로 Thread Kernel Object를 생성. 다양한 레지스터를 기록. 왼쪽을 수행을 하다가 오른쪽으로 옮길때는 cpu의 모든상태를 기록. 그리고 이동. 이러한과정을 context switch라고 한다. 이 context switch가 너무 일어나면 실제 작업보다 더 오래걸리는 오버헤드가 생김.
- 또한 스레드 하나 생성시 thread kernel object 이외에도 Thread Environment Block, Stack 메모리도 잡힘. 보통 스레드가 만들어지면 메모리사용량에대한 오버헤드도 잡힌다.
Thread클래스
- System.Threading namespace 필요
- Thread를 생성하는 방법
- Thread t =new Thread(Foo);
- t.Start();
using System; using System.Threading; class Program { public static void Foo() { for (int i = 0; i < 10000; i++) Console.Write("1"); } public static void Main() { Thread t = new Thread(Foo); t.Start(); for (int i = 0; i < 10000; i++) { Console.Write("2"); } } }
Output: 1과 2가 번갈아가면서 출력됨을 알수있다. 22222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222122222222111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111122222222...
Thread 로 실행할 메소드 모양
- Thread클래스의 생성자 모양
- public Thread(ParameterizedThreadStart start);
- public Thread(ThreadStart start);
- public Thread(ParameterizedThreadStart start,int maxStackSize);
- public Thread(ThreadStart start,int maxStackSize);
- 스레드로 수행할 메소드(delegate)모양
- delegate void ThreadStart()
- delegate void ParameterizedThreadStart(object? obj)
- 메소드의 모양이 다른경우, 람다 표현식을 사용해서 전달
- stack크기를 전달하지않거나 0을 전달하면 실행파일 헤더(PE)에 기록된 스택크기(1메가)사용
using System; using System.Threading; class Program { public static void F1() { Console.WriteLine($"F1"); } public static void F2(object? obj) { Console.WriteLine($"F2 : {obj.ToString()}"); } public static void F3(object obj) { Console.WriteLine($"F3 : {obj.ToString()}"); } public static void F4(string msg) { Console.WriteLine($"F4 : {msg}"); } public static void F5(int a, int b) { Console.WriteLine($"F5 : {a}, {b}"); } public static void Main() { Thread t1 = new Thread(F1); t1.Start(); Thread t2 = new Thread(F2); t2.Start("Hello"); Thread t3 = new Thread(F3); t3.Start("Hello"); //Thread t4 = new Thread(F4); // error Thread t4 = new Thread(() => F4("Hello")); t4.Start(); Thread t5 = new Thread((arg) => F4((string)arg)); t5.Start("Hello"); Thread t6 = new Thread(() => F5(1,2)); t6.Start(); } }
정리
- 인자가 없는 메소드
- object 또는 object? 를 인자로 가지는 메소드
- 메소드모양이 다른경우 일반적으로 람다표현식을사용하여 전달한다.
스레드와 람다표현식(주의할점)
using System; using System.Threading; class Program { static void Foo(int n) { Console.WriteLine(n); } public static void Main() { for (int i = 0; i < 20; i++) { //Foo(i); //이렇게 하면 중복되는 현상이 일어난다. 람다표현식이 지역변수를 캡쳐 //Thread t = new Thread(() => Foo(i)); int temp = i; Thread t = new Thread(() => Foo(temp)); t.Start(); } } }
주석한 부분처럼 했을때 Output: 2 2 4 7 3 9 4 6 6 9 11 12 13 14 14 16 17 18 19 20 => 람다표현식이 지역변수를 캡쳐하는경우가 생김.
Thread 클래스의 다양한 멤버
- t1.ManagedThread :스레드 ID 얻기
- t1.isAlive :스레드가 아직 실행중인가
- t1.Name: 스레드 이름. 한번만 설정할수있다.
- t1.isThreadPoolThread :Thread Pool 강좌 참조
- t1.IsBackground :백그라운드 스레드 여부
- t1.Join :스레드 종료시까지 대기
- Thread.CurrentThread: 현재 스레드의 참조 반환
- Thread.Sleep() :스레드 대기
using System; using System.Threading; class Program { public static void Foo() { // 자신의 참조가 필요하면 Thread t = Thread.CurrentThread; Console.WriteLine($"{t.ManagedThreadId}"); Console.WriteLine("Foo"); Thread.Sleep(2000); } public static void Main() { Thread t1 = new Thread(Foo); t1.Start(); t1.Name = "AAA"; //이름은 한번만 설정가능하다. //t1.Name = "BBB"; // runtime rror Console.WriteLine($"{t1.IsAlive}"); Console.WriteLine($"{t1.ManagedThreadId}"); t1.Join(); } }
백그라운드 스레드
- 프로세스 종료조건
- 프로세스 내의 모든 Foreground 스레드가 종료될때.
- 즉 자신의 스레드가 종료되는 조건이 안되었어도 Foreground 스레드가 모두 종료되면 그냥 종료된다. (완전한 처리후의 종료가 보장되지않을수있다. )
using System; using System.Threading; class Program { public static void Foo(string s, int ms) { Console.WriteLine($"{s} Start"); Thread.Sleep(ms); Console.WriteLine($"{s} Finish"); } public static void Main() { Thread t1 = new Thread(() => Foo("A", 3000)); t1.IsBackground = false; // foreground t1.Start(); Thread t2 = new Thread(() => Foo("B", 9000)); t2.IsBackground = true; // background t2.Start(); Thread t3 = new Thread(() => Foo("C", 7000)); t3.IsBackground = false; // foreground t3.Start(); Thread t4 = new Thread(() => Foo("D", 5000)); t4.IsBackground = true; // background t4.Start(); // 주 스레드가 종료!! //Foreground thread가 모두 종료되는 7초뒤에 프로세스가 종료된다. 9초짜리 백그라운드 스레드는 종료가 보장되지않는다. } }
Output: A Start C Start B Start D Start A Finish D Finish C Finish
=> 9초짜리 B스레드는 종료가 보장되지않는다. 백그라운드스레드라서 포어그라운드 스레드가 7초뒤에 모두 종료되는순간 그냥 종료된다.
<br>
<br>
Cooperative Cancellation
- 스레드가 수행하는 작업을 취소하고싶다.
- 스레드를 강제로 종료하면 안된다.
- 두 스레드간의 약속된 방식이 필요하다.
- 예를 들어 변수를 하나 두고, 스레드에서 취소요청이 있는지 확인.=>c#에서는 공통적인 규칙을 만듬
- CancellationToken(System.Threading.CancellationToken)
- CancellationTokenSource ->이안에 token 이 들어있음.
- 협력적 취소.(Cooperative Cancellation)
예시소스코드 (Count라는 메소드를 실행하는 스레드를 취소해보기)
using System; using System.Threading; class Program { public static void Count(int cnt) { for (int i = 0; i < cnt; i++) { Console.WriteLine(i); Thread.Sleep(200); } } public static void Main() { Thread t = new Thread(o => Count(1000)); t.Start(); } }
using System; using System.Threading; class Program { public static void Count(CancellationToken token, int cnt) { for (int i = 0; i < cnt; i++) { if ( token.IsCancellationRequested) { Console.WriteLine("Cancelling"); break; } Console.WriteLine(i); Thread.Sleep(200); } if (token.IsCancellationRequested) { Console.WriteLine("Cancelled"); } else Console.WriteLine("Finish Count"); } public static void Main() { CancellationTokenSource cts = new CancellationTokenSource(); //Thread t = new Thread(o => Count(cts.Token, 1000)); //CancellationToken.None: 절대취소할수없는 토큰 Thread t = new Thread(o => Count(CancellationToken.None, 1000)); t.Start(); Console.ReadLine(); cts.Cancel(); } }
정리
- 스레드가 수행하는 메소드에서
- cancellationToken을 인자로 받아야한다.
- 작업을 수행하면서 취소요청이 왔는지 주기적으로 확인해야한다.
- 스레드를 생성할때
- cancellationTokenSource객체를 생성한후
- Thread메소드에 cancellationToken을 전달
- 취소하고싶을때 TokenSource의 Cancel()메소드 호출
using System; using System.Threading; class Program { public static void Count(CancellationToken token, int cnt) { for (int i = 0; i < cnt; i++) { if (token.IsCancellationRequested) { Console.WriteLine("Cancelling"); break; } Console.WriteLine(i); Thread.Sleep(200); } if (token.IsCancellationRequested) { Console.WriteLine("Cancelled"); } else Console.WriteLine("Finish Count"); } public static void Main() { CancellationTokenSource cts = new CancellationTokenSource(); CancellationTokenRegistration m1 = cts.Token.Register(() => Console.WriteLine("Cancelled 1")); cts.Token.Register(() => Console.WriteLine("Cancelled 2")); m1.Dispose(); // 등록된 함수 제거. Thread t = new Thread(o => Count(cts.Token, 1000)); t.Start(); cts.CancelAfter(2000); Console.ReadLine(); //cts.Cancel(); } }
취소 메시지를 전달하는 방법 cts.Cancel();cts.CancelAfter(시간);Callback 함수 등록 cancellationToken 의 Register메소드를 사용해서 취소 발생시 호출될 메소드 등록가능
using System; using System.Threading; class Program { public static void Count(CancellationToken token, int cnt) { for (int i = 0; i < cnt; i++) { if (token.IsCancellationRequested) { Console.WriteLine("Cancelling"); break; } Console.WriteLine(i); Thread.Sleep(200); } if (token.IsCancellationRequested) { Console.WriteLine("Cancelled"); } else Console.WriteLine("Finish Count"); } public static void Main() { CancellationTokenSource cts1 = new CancellationTokenSource(); cts1.Token.Register(() => Console.WriteLine("Cancel 1")); CancellationTokenSource cts2 = new CancellationTokenSource(); cts2.Token.Register(() => Console.WriteLine("Cancel 2")); CancellationTokenSource cts = CancellationTokenSource.CreateLinkedTokenSource(cts1.Token, cts2.Token); Thread t = new Thread(o => Count(cts.Token, 1000)); t.Start(); Console.ReadLine(); cts2.Cancel(); } }
thread Token 2개->1개로 링크시킬수있다. 이경우 1개의 토큰만 취소요청이와도 스레드가 취소된다.이런식으로 취소조건을 여러개를 사용할수있다.
<br> <br> <br>
Thread Pool
- 스레드 생성시 참고사항
- 스레드 생성 및 파괴에는 오버헤드가 있다.
- 스레드생성시 생성되는것들(Thread Kernal Object/Thread Environmment Block/Stack)
- 스레드 생성/파괴를 반복하는것보다 하나의 스레드를 대기/실행하도록 하는것이 좋다.
- 몇개의 스레드를 만들것인가??
- ThreadPool
using System; using System.Threading; public static class Program { private static void Foo(object arg) { Console.WriteLine($"Foo : {arg}, {Thread.CurrentThread.ManagedThreadId}"); Thread.Sleep(1000); Console.WriteLine($"{Thread.CurrentThread.IsThreadPoolThread}"); Console.WriteLine("Finish Foo"); Console.ReadLine(); } public static void Main() { //사용자가 직접만든 스레드 //Thread t = new Thread(Foo); //t.Start("Hello"); //t.Name = "AA"; //직접만드는게 아니라 thread pool이용. //ThreadPool.QueueUserWorkItem(Foo, "Hello"); ThreadPool.QueueUserWorkItem(Foo); // arg 에 null Console.ReadLine(); } }
Thread Pool에있는 스레드의 특징
- 항상 Background Thread 이다.
- Name필드를 설정할수없다.
- Block 되는 코드는 사용하면 성능이 떨어진다.
- Thread.CurrentThread.IsThreadPoolThread 속성으로 조사가능.
using System; using System.Threading; public static class Program { private static void Foo(object arg) { Console.WriteLine($"Foo : {arg}, {Thread.CurrentThread.ManagedThreadId}"); Thread.Sleep(1000); Console.WriteLine($"{Thread.CurrentThread.IsThreadPoolThread}"); Console.WriteLine("Finish Foo"); Console.ReadLine(); //이렇게 block되는 코드는 pool 로사용하지않는게 좋다. } public static void Main() { //Thread t = new Thread(Foo); //t.Start("Hello"); //t.Name = "AA"; //ThreadPool.QueueUserWorkItem(Foo, "Hello"); ThreadPool.QueueUserWorkItem(Foo); // arg 에 null Console.ReadLine(); } }
<br>
<br>
<br>
Task
- 스레드가 종료되는걸 대기할때.