01. 목표

더보기

01.1 전제조건

C언어를 웠고,

컴퓨터의 실행 구조와 메모리와 포인터의 사용법과 사용 이유,

그리고 스택과 같은 메모리 모델의 특성(스택 실행 순서와 함수 프레임의 FIFO)의 이해가 있다고 가정합니다.

 

01.2 용어

thread, 실행 순서 흐름을 한가닥의 실로 생각합니다.

쓰레드의 어원은 아직 명확하게 밝혀지지 않았습니다.

 

01.3 WPF 스레드 모델

UI Thread, 그리고 BackgroundWorker와 Dispatcher 이해

 

 

02. 메모리와 데이터

더보기

02.1 함수의 실행흐름

 

 

 

 

02.2 메모리 모델와 함수 실행 구조 (1)

함수는 하나의 순차적인 실행 순서(스레드)를가진다.  

 

 

 

 

02.3 메모리 모델와 함수 실행 구조 (2)

C언어에서는, 하나의 함수에서 다른 함수 내부 데이터(값)에 접근 할 수 없기 때문에

포인터 라는 메모리 주소값을 매개변수로 전달하고, 강제로 다른 함수 프레임의 내부에 접근했었다.

 

 

 

 

02.4 공유

좌-멀티 프로세스, 우-멀티 스레드

프로그램은 '데이터'와 '로직' 단 2가지로 이루어져 있습니다.

스택 영역은 함수가 실행되고, 내부에 데이터(값)이 존재한다.

스레드는 스택 영역을 공유하지 않는다.

 

 

 

 

02.5

Main 함수가 실행되고 있는, '스레드'가 1000ms 정지합니다.

 

03. Thread, Task 생성과 시작

더보기

03.1 예시1

30번 카운팅이 너무 오래 동작해서, 10번 카운팅을 할 수 없다고 가정합니다.

30번 카운팅이 READ 작업이라고 가정합니다.

10번 카운팅이 WRITE 작업이라면, READ 작업이 마무리 될 때까지 WRITE 작업은 불가능합니다.

 

 

 

 

03.2 예시 1 - 비동기 처리

// 비동기 실행 테스트를 위한 '함수'를 구현합니다. 30번 카운팅
private static void Run()
{
    for (int i = 0; i <= int.MaxValue; i++)
    {
        Console.WriteLine(" 비동기 <2> " + i.ToString());
        Thread.Sleep(500);
    }
}

static void Main(string[] args)
{
    // [1] Thread 생성자에 Run을 지정 Thread 객체 생성
    Thread t1 = new Thread(new ThreadStart(Run));

    // [2] Thread 시작
    t1.Start();

    // Main 함수 스레드 실행, 10번 카운팅
    for (int i = 0; i <= 10; i++)
    {
        Console.WriteLine("메인<1> " + i.ToString());
        Thread.Sleep(1000);
    }

    // [3] Thread 끝날 때까지 대기
    t1.Join();

    // 아무 키나 누르면 종료
    Console.WriteLine("Press Any key...");
    Console.ReadLine();
}

 

 

 

 

03.3 스레드 사용법

// 모두 동일하다.
Thread t1 = new Thread(new ThreadStart(Run));
Thread t1 = new (new ThreadStart(Run));
Thread t1 = new Thread(Run);
Thread t1 = new Thread(delegate() { Run(); });
Thread t1 = new Thread(() => Run());

 

// Run 메서드를 입력받아
// ThreadStart 델리게이트 타입 객체를 생성한 후
// Thread 클래스 생성자에 전달
Thread t1 = new Thread(new ThreadStart(Run));           
t1.Start();


// 컴파일러가 Run() 메서드의 함수 프로토타입으로부터
// ThreadStart Delegate 객체를 추론하여 생성함
Thread t2 = new Thread(Run);
t2.Start();


// 익명메서드(Anonymous Method)를 사용하여 쓰레드 생성
Thread t3 = new Thread(delegate()
{
    Run();
});
t3.Start();


// 람다식 (Lambda Expression)을 사용하여 쓰레드 생성
Thread t4 = new Thread(() => Run());
t4.Start();


// 간략한 표현
new Thread(() => Run()).Start();

 

 

 


03.4

Thread

→ Thread pool

C# 4.0 Task

C# 5.0 async / await 로 이어진다

 

언제나 그렇듯이 각각의 기술은 단점을 보완하고, 개발을 편리하기 위해 발전해왔다.

C# 에서는 Thread 보다 편리한 Task와 async / await 위주로 살펴본다.

 

 

 

 

03.5 Thrad → Task 변경점

// 비동기 실행 테스트를 위한 '함수'를 구현합니다. 30번 카운팅
private static void Run()
{
    for (int i = 0; i <= 30; i++)
    {
        Console.WriteLine(" 비동기 <2> " + i.ToString());
        Task.Delay(500).Wait();
    }
}

static void Main(string[] args)
{
    // [1] Task 생성자에 Run을 지정 Task 객체 생성
    Task t1 = new Task(new Action(Run));

    // [2] Task 시작
    t1.Start();

    // Main 함수 스레드 실행, 10번 카운팅
    for (int i = 0; i <= 10; i++)
    {
        Console.WriteLine("메인<1> " + i.ToString());
        Task.Delay(1000).Wait();
    }

    // [3] Task 끝날 때까지 대기
    t1.Wait();

    // 아무 키나 누르면 종료
    Console.WriteLine("Press Any key...");
    Console.ReadLine();
}

 

// 모두 동일하다.
Task t1 = new Task(new Action(Run));
Task t1 = new(Run);
Task t1 = new Task(() => Run());

 

04. Visual Studio Thread 

더보기

04.1 중단점

[03.5]의 소스코드에서, Main 시작점과 for 시작점에 중단점을 찍습니다.

 

 

 

 

04.2

디버깅을 시작합니다.

 

 

 

 

04.3

첫번째 중단점을 찍은 위치에서, 프로그램이 정지합니다.

 

 

 

 

04.4


 

 

 

 

04.5

 

 

 

 

04.6

string id = Thread.CurrentThread.ManagedThreadId.ToString();
Console.WriteLine($"  Thread {id} start");

 

05. 스레드 실행과 종료 시점 이해

더보기

05.1 스레드 학습 최우선 요점

- '호출한' 함수, '호출 당한' 함수 구분하는 방법

- 스레드를 호출한 곳에서, 1)반환값이 필요한 경우 vs 2)반환값이 필요 없는 경우, 구분과 이해

 

 

 

 

05.2  Case 1

"호출한 스레드"의 작업이 긴 경우 & 호출당한 스레드의 작업이 짧은 경우

static int Run(int a) // 비동기 테스트를 위한 '함수'를 구현합니다. 15번 카운팅
{
    for (int i = 0; i < 10; i++) 
    {
        a++;
        Console.WriteLine("비동기     <2> " + a);
        Task.Delay(1000).Wait();
    }
    return a;
}

static void TaskTestAsync()
{
    int cnt = 0;
    Console.WriteLine("카운팅 시작" + cnt);

    Task<int> task = Task<int>.Run(() => Run(cnt)); // [1] Task

    for (int i = 0; i < 5; i++) // Main 함수 스레드 실행, 5번 카운팅
    { 
        cnt++;
        Console.WriteLine("메인  <1> " + cnt);
        Task.Delay(200).Wait();
    }

    Console.WriteLine("Console.WriteLine<1> " + cnt);
    Console.WriteLine("Console.WriteLine<2> " + task.Result.ToString()); // 동기 대기
}

private static void Main(string[] args)
{
    TaskTestAsync();
}

 

 

 

 

05.2 Case 2

"호출한 스레드"의 작업이 짧은 경우 & 호출당한 스레드의 작업이 긴 경우

 

 

 

 

05.4 Case 2

"호출한 스레드"의 작업이 짧은 경우 & 호출당한 스레드의 작업 종료를 대기시킨다.

 

 

 

 

05.5 비교

 

 

[비교 1] 스레드가 동기화 된 경우 - 대기상태를 의도한 것인가?

보통은 스레드를 호출하고 실행시킨 곳에서, 스레드의 결과값이 필요하다면

반환값으로 로직이 실행되어야 하는 경우로, 대기상태가 되어야 한다.

 

 

[비교 2] 스레드가 비동기 된 경우 - 대기 없는 비동기 실행을 의도한 것인가?

보통은 스레드를 호출하고 실행시킨 함수는, 이후 로직이 계속 진행되어야 한다면

동기화하여 대기상태가 없어야 한다.

이 경우는, 스레드에서 반환값을 받을 수 없다.

 

비동기 반환 형식 - C# | Microsoft Learn

 

비동기 반환 형식 - C#

각 형식에 대한 코드 예제를 통해 C#에서 비동기 메서드의 가능한 반환 형식에 대해 알아봅니다.

learn.microsoft.com

 

 

06. join, wait // await, await

더보기

*스레드 학습 최우선 요점

- 호출한 함수를, 호출 당한 함수가 종료될 때까지,대기 상태로 만들것인가?

- join, wait 은 호출한 곳에서 사용하는 것이다.

- await 은 호출 당하는 로직에서, 호출한 곳으로 '실행 흐름'을 반환하고, "호출한 곳의 대기상태를 종료" 할 때 명시한다.

 

 

 

 

06.1 Thread.Sleep( )

join( ) 기능 포함

 

 

 

 

06.2 Task.Delay( )

 

 

 

 

06.3 Task.Delay.Wait( )

Thread의 Join 기능과 같다.

 

 

 

 

06.4 await Task.Delay( )

 

 

05.4

추가

 

07. 반환

더보기

07.1

 

 

08. 공유

더보기

08.1  

(주의)Task가 생성되고 시작되는 과정에서 cnt 가 0 이상의 숫자로 넘겨질 수도 있다.

- 변수 cnt 는 메인 스레드와 t1 스레드가 공유되지 못한다.

- 각각 별도의 스택 메모리에 서로 다른 데이터가 존재한다.

static int Run(int a) // 비동기 실행 테스트를 위한 '함수'를 구현합니다. 30번 카운팅
{
    for (int i = 0; i < 15; i++) {
        Console.WriteLine("비동기     <2> " + a);
        a++;
        Task.Delay(200).Wait();
    }
    return a;
}

static void TaskTest()
{
    int num = 0;

    Task<int> task = Task<int>.Run(() => Run(num)); // [1] Task 생성자에 Run을 지정 Task 객체 생성

    for (int i = 0; i < 5; i++) { // Main 함수 스레드 실행, 5번 카운팅
        Console.WriteLine("메인  <1> " + num);
        num++;
        Task.Delay(1000).Wait();
    }

    //await task; // [2] 
    Console.WriteLine("___________<2> " + task.Result.ToString());
}

private static void Main(string[] args)
{
    TaskTest();
}


09. WPF Diapatcher (1)

더보기

09.1

WPF는 Main Thread가 UI 컨트롤 관련 작업을 모두 수행합니다.

그렇기 때문에, 다른 Thread에서 UI 컨트롤을 담당하는 스레드에 접근하지 못합니다.

 

WPF 작업에 멀티 스레드를 적용하기 위해서는, Dispatcher 와 BackgroundWorker를 사용한다.

Dispatcher(System.Windows.Threading.Dispatcher 클래스의 인스턴스)는, UI Thread 작업 대기열을 관리한다.

 

별도의 Thread 작업이 UI Thread 에 반영되려면,

UI의 Thread 작업 순서를 관리하는 Dispatcher 대기열에 등록해야 한다.

 

WorkerThread >> [ Dispatcher Queue >> UI Thread ]단계로 진행해야한다.

 

 

 

 

09.2

    Title="DispatcherTest1" Height="200" Width="300">
<Grid>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="10"/>
        <ColumnDefinition Width="*"/>
        <ColumnDefinition Width="10"/>
    </Grid.ColumnDefinitions>
    <Grid.RowDefinitions>
        <RowDefinition Height="10"/>
        <RowDefinition Height="*"/>
        <RowDefinition Height="10"/>
        <RowDefinition Height="*"/>
        <RowDefinition Height="10"/>
    </Grid.RowDefinitions>
    <TextBox x:Name="textbox1"
             Grid.Column="1"
             Grid.Row="1"
             Grid.ColumnSpan="3"/>
    <Button x:Name="btn1"
            Content="btn1" 
            Grid.Column="1"
            Grid.Row="3"
            Click="Button_Click"/>
private void Button_Click(object sender, RoutedEventArgs e)
{
    Thread t1 = new Thread(new ThreadStart(UpdateTextbox));
    t1.Start();
}

void UpdateTextbox()
{
    textbox1.Text = "MutiThreading On";
}

 

 

 

 

09.3

 

 

  

 

09.4

void UpdateTextbox()
{
    this.Dispatcher.Invoke(() =>
    {
        textbox1.Text = "MutiThreading On";
    });
}

 

 

 

 

09.5

 

10. WPF Diapatcher (2)

더보기

10.1

        Title="DispatcherTest2" Height="200" Width="300">
<Grid>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="10"/>
        <ColumnDefinition Width="*"/>
        <ColumnDefinition Width="10"/>
        <ColumnDefinition Width="*"/>
        <ColumnDefinition Width="10"/>
    </Grid.ColumnDefinitions>
    <Grid.RowDefinitions>
        <RowDefinition Height="10"/>
        <RowDefinition Height="*"/>
        <RowDefinition Height="10"/>
        <RowDefinition Height="*"/>
        <RowDefinition Height="10"/>
    </Grid.RowDefinitions>
    <TextBox x:Name="textbox1"
         Grid.Column="1"
         Grid.Row="1"
         Grid.ColumnSpan="3"/>
    <Button x:Name="btn1"
        Content="btn1" 
        Grid.Column="1"
        Grid.Row="3"
        Click="Button_Click"/>
    <Button x:Name="btn2"
        Content="btn2" 
        Grid.Column="3"
        Grid.Row="3"
        Click="Button_Click"/>
</Grid>
private void Button_Click(object sender, RoutedEventArgs e)
{
    Task.Run(UpdateTextbox);
    btn2.Content= "btn1 Clicked";
}

void UpdateTextbox()
{
    for (int i = 0; i < 10; i++)
    {
        Task.Delay(500).Wait();
        this.Dispatcher.Invoke(() =>
        {
            textbox1.Text = $"Updating {i}";
        });
    }
}

 

 

 

 

10.2

 

 

 

 

10.3

 

11. WPF

더보기

11.1

WPF는 Main Thread가 UI 컨트롤 관련 작업을 모두 수행합니다.

그렇기 때문에, 다른  UI 컨트롤 Thread와 동기화된 작업이 길어지면, UI 는 멈춥니다.

 

 

 

 

11.2

 

WPF는 UI 컨트롤을 관리하는 Thread에, 사용자가 정의한 Thread의 접근이 불가능합니다.

Dispatcher를 사용해, UI Thread 작업 Queue에 추가하여 UI가 업데이트 되도록 합니다.

 

 

 

 

 

11.3 준비

        Title="DispatcherTest2" Height="200" Width="300">
<Grid>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="10"/>
        <ColumnDefinition Width="*"/>
        <ColumnDefinition Width="10"/>
        <ColumnDefinition Width="*"/>
        <ColumnDefinition Width="10"/>
    </Grid.ColumnDefinitions>
    <Grid.RowDefinitions>
        <RowDefinition Height="10"/>
        <RowDefinition Height="*"/>
        <RowDefinition Height="10"/>
        <RowDefinition Height="*"/>
        <RowDefinition Height="10"/>
    </Grid.RowDefinitions>
    <TextBox x:Name="textbox1"
         Grid.Column="1"
         Grid.Row="1"
         Grid.ColumnSpan="3"/>
    <Button x:Name="btn1"
        Content="btn1" 
        Grid.Column="1"
        Grid.Row="3"
        Click="Button_Click"/>
    <Button x:Name="btn2"
        Content="btn2" 
        Grid.Column="3"
        Grid.Row="3"
        Click="Button_Click"/>
</Grid>

 

 

 

 

11.4 UI와 동기화된 상태

private void Button_Click(object sender, RoutedEventArgs e)
{
    ReadMessageUpdate();
}

private void ReadMessageUpdate()
{
    string res = ReadMessage();

    this.Dispatcher.Invoke(() =>
    {
        textbox1.Text = $"Read Done {res}";
    });
}

private string ReadMessage()
{
    int i = 0;
    for (; i < 5; i++)
    {
        Task.Delay(500).Wait();
        this.Dispatcher.Invoke(() =>
        {
            btn2.Content = $"Reading {i}";
        });
    }
    return i.ToString();
}

 

 

 

 

11.5 UI와 비동기화된 상태 (1)

Task.Run(ReadMessageUpdate);

 

 

 

 

11.5 UI와 비동기화된 상태 (2)

private void Button_Click(object sender, RoutedEventArgs e)
{
    //ReadMessageUpdate();
    //Task.Run(ReadMessageUpdate);
    ReadMessageUpdateAsync1();
}
private async void ReadMessageUpdateAsync1()
{
    string res = await Task.Run(ReadMessageAsync);
    Debug.WriteLine($"Read Done {res}");

    this.Dispatcher.Invoke(() =>
    {
        textbox1.Text = $"Read Done {res}";
    });
}
async Task<string> ReadMessageAsync()
{
    string res = await Task.Run(ReadMessage);
    Debug.WriteLine($"Return Task {res}");
    return res;
}

private void ReadMessageUpdate()
{
    string res = ReadMessage();

    this.Dispatcher.Invoke(() =>
    {
        textbox1.Text = $"Read Done {res}";
    });
}

private string ReadMessage()
{
    int i = 0;
    for (; i < 5; i++)
    {
        Task.Delay(500).Wait();
        this.Dispatcher.Invoke(() =>
        {
            btn2.Content = $"Reading {i}";
        });
    }
    return i.ToString();
}