티스토리 뷰

C#

Task 관련 테스트

takim 2021. 7. 20. 19:54

Task의 유용성에 관하여

 

BackgroundWorker를 사용하다가 Task라는게 있어서 확인 해보니

보다 이해하기 쉽고 사용성도 편리하였습니다.

 

Task와 BackgroundWorker와 Thread, 셋 중에 어떤걸 쓰는게 좋을지에 관한 고민이 있었습니다.

그래서 테스트를 하나 해보고 그 내용에 대해서 정리 해봅니다.

 

 

BackgroundWorker와 Task의 차이

[출처 : MSDN ]

https://docs.microsoft.com/ko-kr/dotnet/api/system.componentmodel.backgroundworker?view=net-5.0

 

 

.NET 2.0부터 적용 대상이 되어 있어 2.0부터 적용 되었다고 보입니다.

 

마샬오브젝트에서부터 할당 받았으니 특정 메모리 영역에 new로써 만들어 둔것으로 예상 됩니다.

 

 

 

 

Task는 .NET 4.0부터 적용되기 때문에 4.0에 처음 적용 되었다고 보입니다.

 

상속이 상당히 깔끔합니다.

뭔가 기분이 좋네요. 쓰고 싶어집니다.

 

 

BackgroundWorker와 Task는 ThreadPool에서 Thread를 할당 받아 작업을 수행합니다.


그런데 Task가 추상화가 더 잘 되어 있어 유용하다고 판단됩니다.

 

제가 C#으로 비주얼 프로그래밍을 처음 하였을때는 2019년도인데 실무에 바로 사용 가능한 자료나 개념 설명이

BackgroundWorker가 많았던 것은 아무래도 오래 되었기 때문인 것 같습니다.

 

제가 처음에 BackgroundWorker를 사용하게 된 이유는

 

1. 안드로이드에서 Http 통신을 하기위해 Dowork, CompleteWork 등등 비슷한 용어가 있어 이해하기 쉬워서

 

2. 예제가 많이 있어 내가 어떻게 사용해야 하는 것인가에 대해 깊은 고민을 하지 않아도 되어서

 

3. 시간이 부족해서 검색하였을 때 나오는 것을 바로 사용 ( 이건 핑계입니다.. ) 

 

 

그런데 사용하다 1년 뒤인 2020년에 Task의 대한 내용을 알게되고 그렇다면 Thread와 Task의 성능차이는?

BackgroundWorker의 성능 차이는 ? 

이런 생각이 들었습니다.

 

 

 

아직까지는 doc를 파보면서 확인하는데 어려움을 느끼고 있습니다..

보기 어렵거나 하기 싫은 느낌보다는 내가 원하는 자료가 어디에 있는거지 하면서 많이 헤매고 있고

stackoverflow나 codeproject에 정리되어 있는 내용이 많이 있어서 doc 보는게 더욱 익숙해지지 않고 있습니다.

 

여튼 시간을 내어 차후에는 msdn의 내용으로 충분히 이해 할 수 있도록 노력 해 봐야겠습니다.

 

 

 

 

 

처리시간 확인

코드 안에서 Thread가 먼저 실행하거나 Task를 먼저 실행하는게 차이가 날 것 같아 나누었습니다.

Task를 먼저쓰냐 Thread를 먼저 쓰냐도 차이가 있지 않을까해서 차이를 두었습니다.

 

단위는 ms입니다.

 

 

 

1. Thread Run을 먼저 실행 한 경우

2. Task Run을 먼저 실행 한 경우 

3. Thread를 2000개 만들고 Thread를 먼저 실행 한 경우

 

4. Task를 100개 Run한 뒤에 Task를 먼저 실행 한경우

(해당 경우는 테스트 도중 실패 했습니다. 그래서 처리시간 확인 하지 않았습니다. )

 

 

- 1번 케이스 -

 

1번 케이스입니다.

4ms정도의 차이가 보입니다. 

> Task 우세

 

 

 

- 2번 케이스 -

 

2번 케이스입니다.

1ms 정도 차이가 납니다.

> Task 우세

 

 

- 3번 케이스 -

3번 케이스입니다.

152ms 정도 차이가 납니다.

> Task 우세

 

 

- 4번 케이스 -

 

 

먼저 Thread 2000개 만드는 것은 ttThread 작업을 2000개하고

 

Task 100개 만드는 것은 taskDump를 100번 실행하는 것입니다.

 

개수를 추정한 것은

workerThread와 asynchronous I/O threads 개수를 파악하고 정하였습니다.

 

비동기 Thread가 1000개여서 100개부터 테스트 진행 하려고 했습니다.

 

 

task 50개 만들었을 경우

 

Task를 50개를 만들고 '동시 실행' 버튼을 눌렀습니다. 

그런데 Thread는 정상 작동 하지만 Task는 작동하지 않았습니다.

 

그래서 Task 개수를 줄여 10개를 만들었습니다.

 

task 10개를 만들었을 경우

 

Task 10개를 만드니 361ms 만에 종료가 되었는데 시작을 360ms 늦게 시작한 것 처럼 느껴졌습니다.

 

그래서 Task를 먼저 만들고 실행하는 것은 시도하지 않았습니다.

 

ThreadPool에서 가용 가능한 Thread가 있을 경우 요청한 Task를 실행하는 것으로 보입니다.

 

 

 

우선

1번 케이스 4ms Task 우세

2번 케이스 1ms Task 우세

3번 케이스 152ms Task 우세

 

현재 자료로 보아 1번2번은 크게 의미가 없어 보입니다.

Task와 Thread가 처리되는 시간의 차이는 없다고 보는게 맞는 것 같습니다.

 

하지만 3번의 경우 Thread의 개수를 2000개로 만드니 150ms정도의 속도 차이를 나타냅니다.

 

Task를 10개 이상 만들경우 그 다음 제가 요청한 Task는 진행이 안되고 그 앞전 Task가 종료 되고 난 다음

ThreadPool에서 할당 가능한 Thread가 있다면 요청한 Task를 실행하는 것으로 보입니다.

 

 

여기서 드는 의문점이

제가 처음에 생각한 가설이 하나 있습니다.

 

Windows는

CPU 코어에 프로세스를 할당하고 그 프로세스 내부에서 Thread들의 시간 할당은 분할되어 순차적으로

처리 될 것이라고 판단 하였고, Task는 ThreadPool에서 활용 가능한 Thread를 할당 받는 것이니

비교적 프로세스에서 종속이 벗어나는게 아닌가 하는 것입니다.

 

처리가 명확히 언제 시작하고 언제 끝나는지 알아야 하는 작업의 경우 Thread를 사용할 것입니다.

그러나 Thread를 많이 쓰고 있는 경우, 해당 PC의 CPU 코어가 4개라고 가정 할 시

1번 코어에 해당 프로세스가 할당 되었지만 작업 한개 끝나고 다른 프로세스에서 시간을 넘겨 준 뒤

다시 코어에 해당 프로세스가 할당되고 다음 작업을 처리하는 게 아닌가 하는 것입니다.

 

그렇다면 Task는 프로세스 종속에서 벗어나서 CPU 코어 중 노는게 있다면 그곳에 할당 되어 일 처리가 되는게 아닌가

하는 것입니다.

 

음... 적고보니 무슨 말인지 이해가 안 갈수도 있을 것 같습니다.

 

간략히 하자면

 

 

 

1. CPU 코어에 할당하는 것은 프로세스 단위로 할당한다.

한 프로세스에서 1개의 job을 처리하고 다른 프로세스에 시간을 준다.

 

2. 한개의 프로세스 내부에 여러 Thread가 존재한다.

정확히 순차적으로 처리되진 않지만 내부에서 시간을 분할하여 처리한다.

 

3. Task의 경우 ThreadPool에서 Thread를 받아서 job을 처리한다.

이는 사용가능한 코어가 존재한다는 뜻이다.

만약 엄청 시간이 오래걸리는 Task가 존재 한다고 한다면, 몇번째 뒤 Task는 

간단한 작업임에도 불구하고 엄청난 시간이 걸릴 것이다.

 

 

 

 

 

정리

 

Task와 Thread와 BackgroundWorker는 기본적인 백그라운드 작업에는 차이점이 없습니다.

 

하지만, 어떻게 처리하느냐는 다릅니다.

 

먼저 비슷한 처리방식인

Task와 BackgroundWorker의 경우

조금더 최신이며, 마샬링 오브젝트를 상속받지 않는 Task를 쓰는게 더 나아 보입니다.

 

Task와 Thread의 경우

용도에 맞게 사용해야 할 것으로 보입니다.

 

다른 블로그나 커뮤니티에서 마지막에 하는 말을 보면

 

정확히 언제 시작되는지가 중요한 경우 Thread를 사용해야 한다고 하였는데

그 말이 맞는 것 같습니다.

 

왠만큼 빠른 처리가 아닌 경우에는 Task로 처리하여 반응을 원활히 하도록하고 

 

시간이 생명인 것들은 Thread를 사용하여 바로 시작 할 수 있도록 하는게 좋아보입니다.

 

현재까지 알아낸 바로는 그러한 차이점 밖에 없어 보입니다.

'내부 Thread를 많을 경우 Task를 써라?' 그런 경우는 없지 않을까 합니다.

 

 

아직도 풀리지 않는 의문점

 

정말로 코어에 프로세스를 할당하고 그 프로세스 내부에서 Thread들 끼리 시분할을 하여 

job 처리를 하는 것인가 하는게 남은 숙제입니다.

 

해당 부분은 무엇부터, 어떻게 알아봐야 할 지 막막한데 검색을 많이 해봐야 겠네요....

 

 

MainWindow.xaml.cs

 

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.Windows.Threading;

namespace TaskTest5
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window, INotifyPropertyChanged
    {
        private long Thread_Resultnumber = 0;
        private int Thread_numberToCompute = 0;
        private int Thread_highestPercentageReached = 0;

        private int Task_numberToCompute = 0;
        private int Task_highestPercentageReached = 0;

        private long Task_Resultnumber = 0;

        DispatcherTimer timer = new DispatcherTimer();

        bool timerCheck = false;

        Thread dumyThread;

        public event PropertyChangedEventHandler PropertyChanged;

        Stopwatch sw_Thread = new Stopwatch();
        Stopwatch sw_Task = new Stopwatch();

        bool systemClose = false;

        void OnPropertyChanged(string propertyName)
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
            }
        }

        private string thread_percentageText;
        public string Thread_percentageText
        {
            get
            {
                return thread_percentageText;
            }
            set
            {
                thread_percentageText = value;
                OnPropertyChanged("Thread_percentageText");
            }
        }

        private string task_percentageText;
        public string Task_percentageText
        {
            get
            {
                return task_percentageText;
            }
            set
            {
                task_percentageText = value;
                OnPropertyChanged("Task_percentageText");
            }
        }

        private string thread_processingTime;
        public string Thread_processingTime
        {
            get
            {
                return thread_processingTime;
            }
            set
            {
                thread_processingTime = value;
                OnPropertyChanged("Thread_processingTime");
            }
        }

        private string task_processingTime;
        public string Task_processingTime
        {
            get
            {
                return task_processingTime;
            }
            set
            {
                task_processingTime = value;
                OnPropertyChanged("Task_processingTime");
            }
        }


        public MainWindow()
        {
            InitializeComponent();

            this.DataContext = this;

            timer.Interval = TimeSpan.FromMilliseconds(100);    //시간간격 설정

            timer.Tick += new EventHandler(timer_Tick);          //이벤트 추가

            int workerThreadCount = 0;
            int aSyncThreadCount = 0;

            ThreadPool.GetAvailableThreads(out workerThreadCount, out aSyncThreadCount);

            MessageBox.Show("workerThredCount : " + workerThreadCount.ToString() + "\n" + "aSyncThreadCount" + aSyncThreadCount.ToString());

            //for (int i = 0; i < 2000; i++)
            //{
            //    Thread testThread = new Thread(new ThreadStart(ttThread));

            //    testThread.IsBackground = true;

            //    testThread.Start();
            //}

            for (int j = 0; j < 10; j++)
            {
                Task.Run(taskDump);
            }
        }

        private void Window_Loaded(object sender, RoutedEventArgs e)
        {
            timer.Start();
        }

        private void timer_Tick(object sender, EventArgs e)
        {
            try
            {
                if (timerCheck)
                {
                    if ((long)Thread_Resultnumber != (long)0)
                    {
                        dumyThread.Join();

                        timerCheck = false;

                        Thread_percentageText = "ThreadEnd";

                    }
                }else if((int)myUpDownControl.Value == 35)
                {
                    btn_bothStart.RaiseEvent(new RoutedEventArgs(Button.ClickEvent));
                }
            }
            catch (Exception ex)
            {
                MessageBox.Show(ex.ToString());
            }

        }

        public void ThreadProc()
        {
            try
            {
                Thread_Resultnumber = ComputeFibonacci(Thread_numberToCompute, "Thread");

                this.Dispatcher.BeginInvoke(DispatcherPriority.Background, new Action(() =>
                {
                    txt_ThreadResult.Text = Thread_Resultnumber.ToString();

                }));

                Thread_processingTime = sw_Task.ElapsedMilliseconds.ToString() + " ms";
            }
            catch (Exception ex)
            {
                MessageBox.Show(ex.ToString());
            }
        }

        public void ttThread()
        {
            while (!systemClose)
            {
                for (int i = 0; i < 123456789; i++)
                {
                    Thread.Sleep(100);
                }
            }
            
        }

        public void taskDump()
        {
            for (int i = 0; i < 123456789; i++)
            {
                Thread.Sleep(1);
            }
        }

        private void btn_bothStart_Click(object sender, RoutedEventArgs e)
        {
            if (!timerCheck)
            {
                dumyThread = new Thread(new ThreadStart(ThreadProc));

                Thread_percentageText = "ThreadInit";

                Thread_numberToCompute = (int)myUpDownControl.Value;

                dumyThread.IsBackground = true;

                Thread_Resultnumber = 0;
                Thread_highestPercentageReached = 0;


                Thread_percentageText = "ThreadStart";

                Task_numberToCompute = (int)myUpDownControl.Value;
                Task_Resultnumber = 0;
                Task_highestPercentageReached = 0;

                if (sw_Task.IsRunning)
                {
                    Log("Task," + Task_processingTime.Replace(" ms","")+",Thread," + Thread_processingTime.Replace(" ms",""));

                    sw_Task.Stop();
                }
                sw_Task.Reset();

                //Start!!!
                sw_Task.Start();

                dumyThread.Start();
                Task.Run(test_Task);
                
                
                timerCheck = true;

                //for (int i = 0; i < 10; i++)
                //{
                //    Thread testThread = new Thread(new ThreadStart(ttThread));

                //    testThread.IsBackground = true;

                //    testThread.Start();
                //}

            }
        }

        private void btn_ThreadStart_Click(object sender, RoutedEventArgs e)
        {
            try
            {
                if (!timerCheck)
                {
                    dumyThread = new Thread(new ThreadStart(ThreadProc));

                    Thread_percentageText = "ThreadInit";

                    Thread_numberToCompute = (int)myUpDownControl.Value;

                    dumyThread.IsBackground = true;

                    Thread_Resultnumber = 0;
                    Thread_highestPercentageReached = 0;


                    Thread_percentageText = "ThreadStart";

                    sw_Thread.Start();

                    dumyThread.Start();

                    timerCheck = true;

                    
                    
                }
            }
            catch (Exception ex)
            {
                MessageBox.Show(ex.ToString());
            }
        }

        private void btn_TaskStart_Click(object sender, RoutedEventArgs e)
        {
            try
            {
                Task_numberToCompute = (int)myUpDownControl.Value;
                Task_Resultnumber = 0;
                Task_highestPercentageReached = 0;

                sw_Task.Start();

                Task.Run(test_Task);
            }
            catch (Exception ex) 
            {
                MessageBox.Show(ex.ToString());
            }
        }

        private void test_Task()
        {
            try
            {
                Task_percentageText = "Task_Start";

                Task_Resultnumber = ComputeFibonacci(Task_numberToCompute, "Task");

                Task_percentageText = "Task_End";

                this.Dispatcher.BeginInvoke(DispatcherPriority.Background, new Action(() =>
                {
                    txt_TaskResult.Text = Task_Resultnumber.ToString();
                }));

                Task_processingTime = sw_Task.ElapsedMilliseconds.ToString() + " ms";

            }
            catch (Exception ex)
            {
                MessageBox.Show(ex.ToString());
            }
            
        }

        long ComputeFibonacci(int n, string type)
        {
            long result = -1;
            try
            {
                // The parameter n must be >= 0 and <= 91.
                // Fib(n), with n > 91, overflows a long.
                if ((n < 0) || (n > 91))
                {
                    throw new ArgumentException(
                        "value must be >= 0 and <= 91", "n");
                }

                if (n < 2)
                {
                    result = 1;
                }
                else
                {
                    result = ComputeFibonacci(n - 1,type) +
                             ComputeFibonacci(n - 2,type);
                }


                if (type.Equals("Thread"))
                {
                    //Thread!!
                    // Report progress as a percentage of the total task.
                    int percentComplete =
                        (int)((float)n / (float)Thread_numberToCompute * 100);

                    if (percentComplete > Thread_highestPercentageReached)
                    {
                        Thread_highestPercentageReached = percentComplete;

                        Thread_percentageText = percentComplete.ToString() + " %";
                    }
                }
                else
                {
                    //TASK!!!
                    // Report progress as a percentage of the total task.
                    int percentComplete =
                        (int)((float)n / (float)Task_numberToCompute * 100);
                    if (percentComplete > Task_highestPercentageReached)
                    {
                        Task_highestPercentageReached = percentComplete;
                        Task_percentageText = percentComplete.ToString() + " %";
                    }
                }
                
                return result;
            }
            catch (Exception)
            {
                throw;
            }
            

            
        }

        private void Log(String msg)
        {
            DateTime today = DateTime.Now;

            string dir_LOG = Directory.GetCurrentDirectory() + @"\Logs";
            string dir_y = dir_LOG + @"\" + today.Year;
            string dir_m = dir_y + @"\" + today.Month;

            string FilePath = dir_m + @"\" + DateTime.Today.ToString("yyyy_MM_dd") + ".log";



            DirectoryInfo di = new DirectoryInfo(dir_LOG);

            DirectoryInfo di_y = new DirectoryInfo(dir_y);

            DirectoryInfo di_m = new DirectoryInfo(dir_m);

            try
            {
                if (di.Exists != true) Directory.CreateDirectory(dir_LOG);
                if (di_y.Exists != true) Directory.CreateDirectory(dir_y);
                if (di_m.Exists != true) Directory.CreateDirectory(dir_m);


                using (StreamWriter sw = File.AppendText(FilePath))
                {
                    sw.WriteLine(msg);
                    sw.Close();
                }
            }
            catch (System.Exception ex)
            {
                MessageBox.Show("Log가 쓰여지지 않았습니다.");
            }

        }

        private void Window_Closing(object sender, CancelEventArgs e)
        {
            systemClose = true;
        }
    }
}

 

 

 

MainWindow.xaml

 

<Window x:Class="TaskTest5.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:xctk="http://schemas.xceed.com/wpf/xaml/toolkit"
        xmlns:local="clr-namespace:TaskTest5"
        mc:Ignorable="d"
        Title="MainWindow" Height="600" Width="1200"
        Loaded="Window_Loaded" Closing="Window_Closing">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="*" />
            <RowDefinition Height="*" />
            <RowDefinition Height="*" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>

        <Grid Grid.Row="0" Grid.Column="0">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="*" />
                <ColumnDefinition Width="2*" />
            </Grid.ColumnDefinitions>
            <Grid.RowDefinitions>
                <RowDefinition Height="*" />
            </Grid.RowDefinitions>
            <Viewbox Grid.Row="0" Grid.Column="0" Margin="10">
                <TextBlock Text="항목" TextWrapping="Wrap" />
            </Viewbox>
            <Viewbox Grid.Row="0" Grid.Column="1" Margin="10">
                <xctk:IntegerUpDown x:Name="myUpDownControl" Value="0" />
            </Viewbox>
        </Grid>

        
        <Grid Grid.Row="1" Grid.Column="0">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="*" />
                <ColumnDefinition Width="*" />
            </Grid.ColumnDefinitions>
            
            <Viewbox Grid.Column="0" Margin="10">
                <TextBlock Text="실행" TextWrapping="Wrap" />
            </Viewbox>
            
            <Button Click="btn_bothStart_Click" x:Name="btn_bothStart" Grid.Column="1">
                <Viewbox Margin="10">
                    <TextBlock Text="동시 실행" Foreground="Black" TextWrapping="Wrap" />
                </Viewbox>
            </Button>
        </Grid>
       
        <Viewbox Grid.Row="2" Grid.Column="0" Margin="10">
            <TextBlock Text="상태" TextWrapping="Wrap" />
        </Viewbox>
        <Viewbox Grid.Row="3" Grid.Column="0" Margin="10">
            <TextBlock Text="Result" TextWrapping="Wrap" />
        </Viewbox>
        
        
        
        <Viewbox Grid.Row="0" Grid.Column="1" Margin="10">
            <TextBlock Text="Thread" TextWrapping="Wrap" />
        </Viewbox>
        <Button Click="btn_ThreadStart_Click"  x:Name="btn_ThreadStart" Grid.Column="1" Grid.Row="1" Margin="5" >
            <Viewbox Margin="10">
                <TextBlock x:Name="txt_ThreadStart" Text="스레드 시작" Foreground="Black" TextWrapping="Wrap" />
            </Viewbox>
        </Button>

        <Grid Grid.Row="2" Grid.Column="1">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="*" />
                <ColumnDefinition Width="*" />
            </Grid.ColumnDefinitions>
            <Viewbox Grid.Column="0"  Margin="10">
                <TextBlock x:Name="txt_ThreadState" Text="{Binding Path=Thread_percentageText, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"  TextWrapping="Wrap" />
            </Viewbox>
            <Viewbox Grid.Column="1"  Margin="10">
                <TextBlock Text="{Binding Path=Thread_processingTime, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"  TextWrapping="Wrap" />
            </Viewbox>
        </Grid >
        
        <Viewbox Grid.Row="3" Grid.Column="1" Margin="10">
            <TextBlock x:Name="txt_ThreadResult" Text="Thread_Result" TextWrapping="Wrap" />
        </Viewbox>
        
        

        <Viewbox Grid.Row="0" Grid.Column="2" Margin="10">
            <TextBlock Text="Task" TextWrapping="Wrap" />
        </Viewbox>
        <Button Click="btn_TaskStart_Click"  x:Name="btn_TaskStart" Grid.Column="2" Grid.Row="1" Margin="5" >
            <Viewbox Margin="10">
                <TextBlock x:Name="txt_TaskStart" Text="태스크 시작" Foreground="Black" TextWrapping="Wrap" />
            </Viewbox>
        </Button>

        <Grid Grid.Row="2" Grid.Column="2" >
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="*" />
                <ColumnDefinition Width="*" />
            </Grid.ColumnDefinitions>
            <Viewbox Grid.Column="0" Margin="10">
                <TextBlock x:Name="txt_TaskState" Text="{Binding Path=Task_percentageText, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" TextWrapping="Wrap" />
            </Viewbox>
            <Viewbox Grid.Column="1" Margin="10">
                <TextBlock Text="{Binding Path=Task_processingTime, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" TextWrapping="Wrap" />
            </Viewbox>
        </Grid>
        
        <Viewbox Grid.Row="3" Grid.Column="2" Margin="10">
            <TextBlock x:Name="txt_TaskResult" Text="Task_Result" TextWrapping="Wrap" />
        </Viewbox>

    </Grid>
</Window>

'C#' 카테고리의 다른 글

폴더관리 프로그램  (0) 2022.10.20
메모리 스트림 사용에 관해  (0) 2021.10.19
Task와 Thread와 BackgroundWorker의 관해서  (2) 2021.09.02
C# - BackgroundWorker에 관하여  (0) 2021.06.28
C#의 역사  (0) 2021.06.20
댓글