반응형

LiveCharts2를 설치하고 간단한 차트를 그려보자.

 

Windows Forms App을 선택한다.

 

Windows Forms App (.NET Framework)를 선택하면 빌드 후 실행 시 에러가 발생한다.

 

 

 

LiveChartsCore.SkiaSharpView.WinForms를 설치한다.

 

Form에 버튼을 하나 배치한다.

 

using LiveChartsCore;
using LiveChartsCore.SkiaSharpView;
using LiveChartsCore.SkiaSharpView.Drawing.Geometries;
using LiveChartsCore.SkiaSharpView.Painting;
using LiveChartsCore.SkiaSharpView.VisualElements;
using LiveChartsCore.SkiaSharpView.WinForms;
using SkiaSharp;

namespace WinFormsApp1
{
    public partial class Form1 : Form
    {
        private bool _isChartDrawn = false;

        public Form1()
        {
            InitializeComponent();

            button1.Click += Button1_Click;
        }

        private void Button1_Click(object? sender, EventArgs e)
        {
            if (_isChartDrawn) return;
            _isChartDrawn = true;

            double[] values1 = new double[] { 2, 1, 3, 5, 3, 4, 6 };
            int[] values2 = new int[] { 4, 2, 5, 2, 4, 5, 3 };

            ISeries[] series = new ISeries[]
            {
                new LineSeries<double>
                {
                    Values = values1,
                    Fill = null,
                    GeometrySize = 20
                },
                new LineSeries<int, StarGeometry>
                {
                    Values = values2,
                    Fill = null,
                    GeometrySize = 20
                }
            };

            DrawnLabelVisual title = new DrawnLabelVisual(
                new LabelGeometry
                {
                    Text = "My chart title",
                    Paint = new SolidColorPaint(SKColor.Parse("#303030")),
                    TextSize = 25,
                    Padding = new LiveChartsCore.Drawing.Padding(15),
                    // padding은 텍스트와 라벨의 경계 사이의 간격을 지정하는 속성이다.
                    // 위 코드에서는 15로 설정되어 있어 텍스트와 라벨의 경계 사이에 15픽셀의 간격이 생긴다.
                    VerticalAlign = LiveChartsCore.Drawing.Align.Start,
                    // vertical align은 Start, Middle, End가 있다. Start는 위쪽, Middle은 가운데, End는 아래쪽에 위치한다.
                    HorizontalAlign = LiveChartsCore.Drawing.Align.Start
                    // horizontal align은 Start, Middle, End가 있다. Start는 왼쪽, Middle은 가운데, End는 오른쪽에 위치한다.
                });

            CartesianChart cartesianChart = new CartesianChart
            {
                Series = series,
                Title = title,
                Location = new System.Drawing.Point(0, 0),
                Size = new System.Drawing.Size(800, 300),
                //Anchor = AnchorStyles.Left | AnchorStyles.Right | AnchorStyles.Top | AnchorStyles.Bottom
                // 위 코드와 같이 Anchor를 지정하면 폼의 크기가 변경될 때 차트의 크기가 자동으로 조정되는데 그려진 비율이 유지되는게 아니라
                // 그려지지 않은 부분의 사이즈가 그대로 유지되는 방식으로 조정된다.
            };

            Controls.Add(cartesianChart);
        }
    }
}

 

코드를 입력하고 빌드한다.

 

 

버튼을 클릭하면 차트가 표시된다.

 

이번엔 차트에 Zoom & Pan 모드를 설정해 보자.

using LiveChartsCore;
using LiveChartsCore.SkiaSharpView;
using LiveChartsCore.SkiaSharpView.WinForms;

namespace WinFormsApp1
{
    public partial class Form1 : Form
    {
        private bool _isChartAdded = false;

        public Form1()
        {
            InitializeComponent();

            button1.Click += Button1_Click;
        }

        private void Button1_Click(object? sender, EventArgs e)
        {
            if (_isChartAdded) return;
            _isChartAdded = true;

            int[] values = Fetch();
            ISeries[] series = new ISeries[]
            {
                new LineSeries<int>
                {
                    Values = values
                }
            };

            CartesianChart cartesianChart = new CartesianChart
            {
                Series = series,
                ZoomMode = LiveChartsCore.Measure.ZoomAndPanMode.X, // X: Enables zooming and panning on the X axis.
                // 줌 및 팬 모드를 설정하여 X축에서만 작동하도록 지정한다.
                Location = new System.Drawing.Point(0, 0),
                Size = new System.Drawing.Size(800, 300),
                //Anchor = AnchorStyles.Left | AnchorStyles.Right | AnchorStyles.Top | AnchorStyles.Bottom
            };

            Controls.Add(cartesianChart);
        }

        private static int[] Fetch()
        {
            int[] values = new int[100];
            Random r = new Random();
            int t = 0;

            for (int i = 0; i < 100; i++)
            {
                t += r.Next(-90, 100);
                values[i] = t;
            }

            return values;
        }
    }
}

 

코드를 빌드하고 실행한다.

 

왼쪽 버튼: Pan

오른쪽 버튼: Select

휠: Zoom

 

실시간 업데이트 차트를 만들어 보자.

using LiveChartsCore;
using LiveChartsCore.Defaults;
using LiveChartsCore.SkiaSharpView;
using LiveChartsCore.SkiaSharpView.Painting;
using LiveChartsCore.SkiaSharpView.WinForms;
using SkiaSharp;
using System.Collections.ObjectModel;

namespace WinFormsApp1
{
    public partial class Form1 : Form
    {
        private readonly ObservableCollection<DateTimePoint> _values = new ObservableCollection<DateTimePoint>();
        private readonly object _sync = new object();
        private bool _isReading = true;
        private double[] _separators = Array.Empty<double>();
        private readonly CartesianChart _cartesianChart = new CartesianChart();
        private bool _isChartAdded = false;

        public Form1()
        {
            InitializeComponent();

            button1.Click += Button1_Click;
        }

        private void Button1_Click(object? sender, EventArgs e)
        {
            if (_isChartAdded) return; // 차트가 이미 추가된 경우, 중복 추가 방지
            _isChartAdded = true;

            ISeries[] seriesColection = new ISeries[]
            {
                new LineSeries<DateTimePoint>
                {
                    Values = _values,
                    Fill = null,
                    GeometryFill = null,
                    GeometryStroke = null
                }
            };

            Axis xAxis = new Axis
            {
                Labeler = value => Formatter(new DateTime((long)value)),
                AnimationsSpeed = TimeSpan.FromMilliseconds(0),
                SeparatorsPaint = new SolidColorPaint(SKColors.Gray),
                CustomSeparators = _separators
            };

            _cartesianChart.Series = seriesColection;
            _cartesianChart.XAxes = [xAxis];
            _cartesianChart.Location = new System.Drawing.Point(0, 0);
            _cartesianChart.Size = new System.Drawing.Size(800, 300);
            //_cartesianChart.Anchor = AnchorStyles.Left | AnchorStyles.Right | AnchorStyles.Top | AnchorStyles.Bottom;

            Controls.Add(_cartesianChart);
            // 중복 추가를 방지하기 위해 폼 컨트롤에 있는지 아래와 같이 확인할 수도 있다.
            //if (!Controls.Contains(_cartesianChart))
            //    Controls.Add(_cartesianChart);

            _ = ReadData(xAxis);
            // _ = 는 비동기 메서드의 반환값(Task)을 무시하기 위한 구문이다. 반환값은 주로 비동기 작업의 완료를 나타내는
            // Task 객체이지만, 여기서는 반환값을 사용하지 않으므로 _ = 구문을 사용하여 명시적으로 무시한다.
            // 이게 없으면 컴파일러가 반환값이 사용되지 않았다고 경고한다.
        }
        private async Task ReadData(Axis xAxis) // 데이터를 비동기적으로 읽어와 차트에 업데이트하는 메서드
        {
            Random random = new Random();
            while (_isReading)
            {
                await Task.Delay(100);
                lock (_sync)
                {
                    _values.Add(new DateTimePoint(DateTime.Now, random.Next(0, 10)));
                    // DateTimePoint: 날짜와 값을 함께 저장하는 클래스
                    if (_values.Count > 250) _values.RemoveAt(0);
                    _separators = GetSeparators();
                    xAxis.CustomSeparators = _separators;
                }
            }
        }

        private static double[] GetSeparators() // 차트의 X축에 표시할 구분자(시간 간격)를 생성하는 메서드
        {
            DateTime now = DateTime.Now;
            return new double[]
            {
                now.AddSeconds(-25).Ticks,
                now.AddSeconds(-20).Ticks,
                now.AddSeconds(-15).Ticks,
                now.AddSeconds(-10).Ticks,
                now.AddSeconds(-5).Ticks,
                now.Ticks
            };
        }

        private static string Formatter(DateTime date) // X축 레이블을 포맷팅하는 메서드
        {
            double secsAgo = (DateTime.Now - date).TotalSeconds;
            return secsAgo < 1 ? "now" : $"{secsAgo:N0}s ago";
            // "N0"는 소수점 없는 숫자 형식 지정자.
        }
    }
}

 

빌드하고 실행한다.

 

 

※ 참고

Toolbox - LiveChartsCore.SkiaSharpView.WinForms 툴은 사용하지 말자. 그리는 건 되지만 지울 수가 없다.

 

LiveCharts는 코드로만 작성하거나 코드로 UserControl을 만들어 Toolbox에서 사용해야 한다.

 

※ 참고

LiveCharts2 (화면 상단의 Framework를 WinForms 등 원하는 대로 선택해야 한다)

 

반응형
Posted by J-sean
:
반응형

우선 DateTime 클래스와 TimeSpan 클래스를 사용해 보자.

 

using System;

namespace ConsoleApp1
{
    internal class Program
    {
        static void Main(string[] args)
        {
            DateTime now = DateTime.Now;
            Console.WriteLine(now);

            TimeSpan hours = TimeSpan.FromHours(3);
            Console.WriteLine(now - hours);

            TimeSpan timeSpan = new TimeSpan(3, 30, 30); // 3 hours, 30 minutes, 30 seconds
            Console.WriteLine(now - timeSpan);

            DateTime past1 = new DateTime(2026, 1, 1);
            Console.WriteLine(now - past1); // TimeSpan

            string str = "20260101";
            DateTime past2 = DateTime.ParseExact(str, "yyyyMMdd", null);
            Console.WriteLine(now - past2); // TimeSpan

            string str2 = "20260101-153030";
            DateTime past3 = DateTime.ParseExact(str2, "yyyyMMdd-HHmmss", null);
            Console.WriteLine(now - past3); // TimeSpan            

            TimeSpan diff = now - past3;
            Console.WriteLine(new TimeSpan(diff.Days, diff.Hours, diff.Minutes, diff.Seconds));
            Console.WriteLine($"{diff.Days}일 {diff.Hours}시간 {diff.Minutes}분 {diff.Seconds}초");
        }
    }
}

 

 

이번엔 DateTimePicker로 날짜와 시간을 선택하고 DateTime 클래스를 사용해 보자.

 

Form에 DateTimePicker, TextBox, Button을 적당히 배치한다.

 

using System;
using System.IO;
using System.Windows.Forms;

namespace WindowsFormsApp1
{
    public partial class Form1 : Form
    {
        string date;
        string time;
        string dateTime;
        string[] mkvFiles;
        DateTime[] mkvFilesInDateTime;

        public Form1()
        {
            InitializeComponent();

            // 현재 날짜와 시간 초기화
            date = DateTime.Now.ToString("yyyyMMdd");
            time = DateTime.Now.ToString("HHmmss");
            dateTime = date + "-" + time;

            // 이벤트 핸들러 등록
            dateTimePicker1.ValueChanged += dateTimePicker1_ValueChanged;
            dateTimePicker2.ValueChanged += dateTimePicker2_ValueChanged;

            // DateTimePicker1 설정, 이렇게 하면 날짜와 시간 모두 선택할 수 있다.
            //dateTimePicker1.Format = DateTimePickerFormat.Custom;
            //dateTimePicker1.CustomFormat = "yyyy-MM-dd HH:mm:ss";

            // DateTimePicker2 설정, 시간과 분만 선택할 수 있도록 설정한다.
            dateTimePicker2.Format = DateTimePickerFormat.Custom;
            dateTimePicker2.CustomFormat = "HH:mm"; // 시간과 분만 표시하여 초 선택을 없앰
            dateTimePicker2.ShowUpDown = true; // 시간 선택을 위한 UpDown 컨트롤 표시

            // TextBox 설정
            textBox1.Multiline = true;
            textBox1.Height = textBox1.Font.Height * 6;

            // Button 이벤트 핸들러 등록
            button1.Click += button1_Click;

            // MKV 파일 검색 및 TextBox에 출력
            GetMkvFiles();
        }

        private void dateTimePicker1_ValueChanged(object sender, EventArgs e)
        {
            DateTime selectedDate = dateTimePicker1.Value;

            date = selectedDate.ToString("yyyyMMdd");
            dateTime = date + "-" + time;
        }

        private void dateTimePicker2_ValueChanged(object sender, EventArgs e)
        {
            DateTime selectedTime = dateTimePicker2.Value;
            // 초 선택이 없으므로 "HHmm00"으로 초를 항상 00으로 고정
            time = selectedTime.ToString("HHmm00");
            dateTime = date + "-" + time;
        }

        private void button1_Click(object sender, EventArgs e)
        {
            // 버튼 클릭 시 dateTime이 비어있지 않으면 작업 수행
            if (!string.IsNullOrEmpty(dateTime))
            {
                DateTime selectedDateTime = DateTime.ParseExact(dateTime, "yyyyMMdd-HHmmss", null);
                DateTime closestDateTime = DateTime.MinValue;
                string closestFile = string.Empty;

                // mkvFilesInDateTime 배열의 각 요소를 순회하면서 selectedDateTime과 비교하여 이전 시점의 가장 가까운 파일을 찾는다.
                foreach (DateTime fileDateTime in mkvFilesInDateTime)
                {
                    if (fileDateTime <= selectedDateTime)
                    {
                        if (fileDateTime > closestDateTime)
                        {
                            closestDateTime = fileDateTime;
                            closestFile = fileDateTime.ToString("yyyyMMdd-HHmmss") + ".mkv";
                        }
                    }
                }

                if (!string.IsNullOrEmpty(closestFile))
                    MessageBox.Show("가장 가까운 파일: " + closestFile);
                else
                    MessageBox.Show("선택한 날짜와 시간보다 이전에 생성된 파일이 없습니다.");
            }
            else
            {
                MessageBox.Show("날짜와 시간을 선택해주세요.");
            }
        }

        private void GetMkvFiles()
        {
            // 검색할 대상 폴더 경로 지정
            string directoryPath = @".\Video";
            // 문자열 앞에 @를 붙이면 이스케이프 시퀀스를 무시하고 문자열을 그대로 사용할 수 있다.
            // 1. 이스케이프 시퀀스(\) 무시 (파일 경로에 유용)
            //  보통 C# 문자열 안에서 백슬래시(\)는 이스케이프 문자로 쓰이기 때문에(\n, \t 등), 파일 경로를 적을 때 백슬래시를
            //  두 번씩 적어줘야 한다. 하지만 @를 붙이면 백슬래시를 있는 그대로 인식한다.
            // 2. 여러 줄(Multi-line) 문자열 작성
            //  @를 사용하면 \n이나 Environment.NewLine을 쓰지 않고도 엔터를 쳐서 여러 줄의 문자열을 그대로 작성할 수 있다.

            //  경로가 존재하는지 확인
            if (Directory.Exists(directoryPath))
            {
                // 지정된 폴더에서 .mkv 확장자를 가진 모든 파일의 전체 경로 배열을 가져온다.
                mkvFiles = Directory.GetFiles(directoryPath, "*.mkv");

                // 파일 목록을 TextBox에 출력
                textBox1.Clear();
                mkvFilesInDateTime = new DateTime[mkvFiles.Length];

                for (int i = 0; i < mkvFiles.Length; i++)
                {
                    // Path.GetFileName(file)를 사용하면 파일 이름(확장자 포함)을 가져올 수 있다.
                    mkvFiles[i] = Path.GetFileNameWithoutExtension(mkvFiles[i]);

                    // 파일 이름에서 날짜와 시간 부분을 추출하여 mkvFilesInDateTime 배열에 저장
                    mkvFilesInDateTime[i] = DateTime.ParseExact(mkvFiles[i], "yyyyMMdd-HHmmss", null);

                    textBox1.AppendText(mkvFiles[i] + Environment.NewLine);
                }
            }
            else
            {
                MessageBox.Show("지정된 폴더를 찾을 수 없습니다.");
            }
        }
    }
}

 

위 코드를 빌드하고 실행한다.

 

Video 폴더에는 위와 같은 파일이 있다.

 

적당한 날짜(26/03/27)와 시간(09:36)을 선택하고 버튼을 클릭한다.

 

26/03/27 09:36보다 작으면서 가장 큰 시간의 파일은 20260327-091500.mkv이다.

 

반응형
Posted by J-sean
:

보호되어 있는 글입니다.
내용을 보시려면 비밀번호를 입력하세요.

보호되어 있는 글입니다.
내용을 보시려면 비밀번호를 입력하세요.

반응형

여러 가지 비동기 스레딩을 구현해 보자.

 

우선 인수와 리턴값이 없는 스레드다.

아래 3가지 코드는 모두 같은 내용이지만 다른 방식으로 작성되었다.

 

using System;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleApp1
{
    internal class Program
    {
        static void Counter()
        {
            int subCounter = 0;
            for (int i = 0; i < 5; i++)
            {
                Thread.Sleep(1000);
                Console.WriteLine($"Sub Thread counting... {subCounter++}");
            }
        }

        static void Main(string[] args)
        {
            Task task = new Task(Counter);
            task.Start();
                        
            int mainCounter = 0;
            for (int i = 0; i < 5; i++)
            {
                Thread.Sleep(1000);
                Console.WriteLine($"Main Thread counting... {mainCounter++}");                
            }

            task.Wait();
        }
    }
}

 

using System;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleApp1
{
    internal class Program
    {
        static void Main(string[] args)
        {
            Action counter = () =>
            {
                int subCounter = 0;
                for (int i = 0; i < 5; i++)
                {
                    Thread.Sleep(1000);
                    Console.WriteLine($"Sub Thread counting... {subCounter++}");
                }
            };

            Task task = new Task(counter);
            task.Start();
                        
            int mainCounter = 0;
            for (int i = 0; i < 5; i++)
            {
                Thread.Sleep(1000);
                Console.WriteLine($"Main Thread counting... {mainCounter++}");                
            }

            task.Wait();
        }
    }
}

 

using System;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleApp1
{
    internal class Program
    {
        static void Main(string[] args)
        {
            Task task = Task.Run(() =>
            {
                int subCounter = 0;
                for (int i = 0; i < 5; i++)
                {
                    Thread.Sleep(1000);
                    Console.WriteLine($"Task counting... {subCounter++}");
                }
            });

            int mainCounter = 0;
            for (int i = 0; i < 5; i++)
            {
                Thread.Sleep(1000);
                Console.WriteLine($"Main Thread counting... {mainCounter++}");
            }

            task.Wait();
        }
    }
}

 

순서는 약간 달라질 수 있다.

 

 

이번엔 인수를 하나 받는 스레드를 만들어 보자. 아래 2가지 코드는 모두 같은 내용이지만 다른 방식으로 작성되었다.

 

using System;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleApp1
{
    internal class Program
    {
        static void Counter(object to)
        {
            int subCounter = 0;
            for (int i = 0; i < (int)to; i++)
            {
                Thread.Sleep(1000);
                Console.WriteLine($"Sub Thread counting... {subCounter++}");
            }
        }

        static void Main(string[] args)
        {
            Task task = new Task(Counter, 5);
            task.Start();

            int mainCounter = 0;
            for (int i = 0; i < 5; i++)
            {
                Thread.Sleep(1000);
                Console.WriteLine($"Main Thread counting... {mainCounter++}");
            }

            task.Wait();
        }
    }
}

 

using System;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleApp1
{
    internal class Program
    {
        static void Main(string[] args)
        {
            Action<object> Counter = (object to) =>
            {
                int subCounter = 0;
                for (int i = 0; i < (int)to; i++)
                {
                    Thread.Sleep(1000);
                    Console.WriteLine($"Sub Thread counting... {subCounter++}");
                }
            };

            Task task = new Task(Counter, 5);
            task.Start();

            int mainCounter = 0;
            for (int i = 0; i < 5; i++)
            {
                Thread.Sleep(1000);
                Console.WriteLine($"Main Thread counting... {mainCounter++}");
            }

            task.Wait();
        }
    }
}

 

인수가 있는 경우, Task.Run() 메서드에서 람다식으로 코드를 만드는 건 의미가 없는 것 같다.

결과는 위와 같다.

 

 

이번엔 리턴값이 있는 스레드를 만들어 보자. 아래 2가지 코드는 모두 같은 내용이지만 다른 방식으로 작성되었다.

 

using System;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleApp1
{
    internal class Program
    {
        static int Counter()
        {
            int subCounter = 0;
            for (int i = 0; i < 5; i++)
            {
                Thread.Sleep(1000);
                Console.WriteLine($"Sub Thread counting... {subCounter++}");
            }

            return subCounter;
        }

        static void Main(string[] args)
        {
            Task<int> task = new Task<int>(Counter);
            task.Start();

            int mainCounter = 0;
            for (int i = 0; i < 5; i++)
            {
                Thread.Sleep(1000);
                Console.WriteLine($"Main Thread counting... {mainCounter++}");
            }

            task.Wait();
            Console.WriteLine($"Sub Thread finished with count: {task.Result}");
        }
    }
}

 

using System;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleApp1
{
    internal class Program
    {
        static void Main(string[] args)
        {
            Task<int> task = Task<int>.Run(() =>
            {
                int subCounter = 0;
                for (int i = 0; i < 5; i++)
                {
                    Thread.Sleep(1000);
                    Console.WriteLine($"Sub Thread counting... {subCounter++}");
                }

                return subCounter;
            });

            int mainCounter = 0;
            for (int i = 0; i < 5; i++)
            {
                Thread.Sleep(1000);
                Console.WriteLine($"Main Thread counting... {mainCounter++}");
            }

            task.Wait();
            Console.WriteLine($"Sub Thread finished with count: {task.Result}");
        }
    }
}

 

Action은 리턴값이 없는 메서드 전용이기 때문에 이 경우 Action을 사용할 수 없다.

 

순서는 약간 달라질 수 있다.

 

 

이번엔 인수도 있고 리턴값도 있는 스레드를 만들어 보자.

아래 2가지 코드는 모두 같은 내용이지만 다른 방식으로 작성되었다.

 

using System;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleApp1
{
    internal class Program
    {
        static int Counter(object to)
        {
            int subCounter = 0;
            for (int i = 0; i < (int)to; i++)
            {
                Thread.Sleep(1000);
                Console.WriteLine($"Sub Thread counting... {subCounter++}");
            }

            return subCounter;
        }

        static void Main(string[] args)
        {
            Task<int> task = new Task<int>(Counter, 5);
            task.Start();

            int mainCounter = 0;
            for (int i = 0; i < 5; i++)
            {
                Thread.Sleep(1000);
                Console.WriteLine($"Main Thread counting... {mainCounter++}");
            }

            task.Wait();
            Console.WriteLine($"Sub Thread finished with count: {task.Result}");
        }
    }
}

 

using System;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleApp1
{
    internal class Program
    {
        static void Main(string[] args)
        {
            Func<object, int> Counter = (object to) =>
            {
                int subCounter = 0;
                for (int i = 0; i < (int)to; i++)
                {
                    Thread.Sleep(1000);
                    Console.WriteLine($"Sub Thread counting... {subCounter++}");
                }

                return subCounter;
            };

            Task<int> task = new Task<int>(Counter, 5);
            task.Start();

            int mainCounter = 0;
            for (int i = 0; i < 5; i++)
            {
                Thread.Sleep(1000);
                Console.WriteLine($"Main Thread counting... {mainCounter++}");
            }

            task.Wait();
            Console.WriteLine($"Sub Thread finished with count: {task.Result}");
        }
    }
}

 

 

 

async, await를 사용해 보자.

 

아래 코드는 컴파일 에러가 발생한다. Main 함수에는 async가 단순하게 그냥 붙을 수 없다.

 

using System;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleApp1
{
    internal class Program
    {
    	// 컴파일 에러 발생
        static async void Main(string[] args)
        {
            await Task.Run(() => Count());

            int mainCounter = 0;
            for (int i = 0; i < 5; i++)
            {
                Thread.Sleep(1000);
                Console.WriteLine($"Main Thread counting... {mainCounter++}");
            }
        }

        static void Count()
        {
            int subCounter = 0;
            for (int i = 0; i < 5; i++)
            {
                Thread.Sleep(1000);
                Console.WriteLine($"Sub Thread counting... {subCounter++}");
            }

        }
    }
}

 

Main 함수에 단순하게 async가 붙으면 다음과 같은 에러가 발생한다.

Program does not contain a static 'Main' method suitable for an entry point

 

또, 아래와 같이 바꾸면 단일 스레드처럼 동작한다.

 

using System;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleApp1
{
    internal class Program
    {
        static async Task Main(string[] args)
        {
            await Task.Run(() => Count());

            int mainCounter = 0;
            for (int i = 0; i < 5; i++)
            {
                Thread.Sleep(1000);
                Console.WriteLine($"Main Thread counting... {mainCounter++}");
            }
        }

        static void Count()
        {
            int subCounter = 0;
            for (int i = 0; i < 5; i++)
            {
                Thread.Sleep(1000);
                Console.WriteLine($"Sub Thread counting... {subCounter++}");
            }

        }
    }
}

 

 

await 키워드는 이 비동기 작업(서브 스레드)이 완전히 끝날 때까지 여기서 기다리겠다는 의미이기 때문이다.

 

아래와 같이 바꿔보자.

 

using System;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleApp1
{
    internal class Program
    {
        static async Task Main(string[] args)
        {
            // await 없이 Task를 실행(시작)만 하고 변수에 담아둔다.
            // 이 시점부터 서브 스레드가 백그라운드에서 동작한다.
            Task countTask = Task.Run(() => Count());

            // 메인 스레드는 멈추지 않고 자신의 루프 작업을 동시에 수행한다.
            int mainCounter = 0;
            for (int i = 0; i < 5; i++)
            {
                // 참고: 비동기 메서드(async) 내에서는 Thread.Sleep 대신 await Task.Delay를 사용하는 것을 권장.
                await Task.Delay(1000);
                Console.WriteLine($"Main Thread counting... {mainCounter++}");
            }

            // 메인 스레드의 작업이 다 끝나면, 메인 프로세스가 종료되기 전에
            // 서브 스레드의 작업이 모두 완료되었는지 마지막으로 확인(대기).
            await countTask;
        }

        static void Count()
        {
            int subCounter = 0;
            for (int i = 0; i < 5; i++)
            {
                Thread.Sleep(1000);
                Console.WriteLine($"Sub Thread counting... {subCounter++}");
            }
        }
    }
}

 

아니면 아래와 같이 다른 함수에서 async, await를 사용하도록 코드를 바꾼다.

 

using System;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleApp1
{
    internal class Program
    {
        static void Main(string[] args)
        {
            Count();

            int mainCounter = 0;
            for (int i = 0; i < 5; i++)
            {
                Thread.Sleep(1000);
                Console.WriteLine($"Main Thread counting... {mainCounter++}");
            }
        }

        static async void Count()
        {
            await Task.Run(async () =>
            {
                int subCounter = 0;
                for (int i = 0; i < 5; i++)
                {
                    // Use Task.Delay instead of Thread.Sleep to avoid blocking the thread
                    await Task.Delay(1000);
                    Console.WriteLine($"Sub Thread counting... {subCounter++}");
                }
            });
        }
    }
}

 

 

 

이번엔 반환값이 있는 함수를 살펴보자.

 

using System;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleApp1
{
    internal class Program
    {
        static void Main(string[] args)
        {
            Task<int> taskResult = Count();

            int mainCounter = 0;
            for (int i = 0; i < 5; i++)
            {
                Thread.Sleep(1000);
                Console.WriteLine($"Main Thread counting... {mainCounter++}");
            }

            int result = taskResult.Result;
            Console.WriteLine($"Result from Sub Thread: {result}");
        }

        static async Task<int> Count()
        {
            return await Task.Run(() =>
            {
                int subCounter = 0;
                for (int i = 0; i < 5; i++)
                {
                    Thread.Sleep(1000);
                    Console.WriteLine($"Sub Thread counting... {subCounter++}");
                }

                return subCounter;
            });
        }
    }
}

 

 

위 코드를 아래와 같이 바꾸면 단일 스레드처럼 작동하게 된다. 조심하자.

 

using System;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleApp1
{
    internal class Program
    {
        static void Main(string[] args)
        {
            int taskResult = Count().Result;
            // Task가 시작되자마자 바로 Result를 요구하고 있다.
            // 메인 스레드가 진행하지 못하게 된다.
            Console.WriteLine($"Result from Sub Thread: {taskResult}");

            int mainCounter = 0;
            for (int i = 0; i < 5; i++)
            {
                Thread.Sleep(1000);
                Console.WriteLine($"Main Thread counting... {mainCounter++}");
            }
        }

        static async Task<int> Count()
        {
            return await Task.Run(() =>
            {
                int subCounter = 0;
                for (int i = 0; i < 5; i++)
                {
                    Thread.Sleep(1000);
                    Console.WriteLine($"Sub Thread counting... {subCounter++}");
                }

                return subCounter;
            });
        }
    }
}

 

 

 

Parallel 클래스를 사용해서 간편하게 스레드를 사용해 보자.

 

using System;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleApp1
{
    internal class Program
    {
        static void Main(string[] args)
        {
            Parallel.For(0, 10, DoSomething);

            // Alternatively, you can use a lambda expression:
            //Parallel.For(0, 10, i =>
            //{
            //    DoSomething(i);
            //});
        }

        static void DoSomething(int i)
        {
            Thread.Sleep(1000);
            Console.WriteLine($"Job {i} completed.");
        }
    }
}

 

 

하나씩 처리하면 10초가 걸리는 작업이 10개의 스레드가 생성되어 1초 만에 끝난다. (스레드는 CPU 코어의 갯수에 따라 다르게 생성된다)

 

반응형
Posted by J-sean
:

보호되어 있는 글입니다.
내용을 보시려면 비밀번호를 입력하세요.

보호되어 있는 글입니다.
내용을 보시려면 비밀번호를 입력하세요.

반응형

다른 콘솔 프로그램을 실행하고 결과를 확인해 보자.

 

using System;
using System.Diagnostics;
using System.IO;

namespace ConsoleApp1
{
    internal class Program
    {
        static void Main(string[] args)
        {
            // ProcessStartInfo 설정
            ProcessStartInfo startInfo = new ProcessStartInfo();
            //startInfo.FileName = "cmd.exe"; // 실행할 프로그램 (예: cmd)
            //startInfo.Arguments = "/c dir"; // 프로그램 인자 (예: 현재 디렉토리 파일 목록)
            startInfo.FileName = "cmd.exe";
            startInfo.Arguments = "/c dir";
            startInfo.UseShellExecute = false; // 쉘 사용 안 함 (리다이렉션 필수 설정)
            startInfo.RedirectStandardOutput = true; // 표준 출력 리다이렉션
            startInfo.CreateNoWindow = true; // 콘솔 창 띄우지 않음

            // 프로세스 실행
            using (Process process = Process.Start(startInfo))
            {
                // 3. 출력 내용 읽기
                using (StreamReader reader = process.StandardOutput)
                {
                    // 전체 결과 가져오기. 너무 길면 메모리 문제 발생 가능
                    //string result = reader.ReadToEnd();
                    //Console.WriteLine("--- 외부 프로그램 결과 ---");
                    //Console.WriteLine(result);
                    //Console.WriteLine("--------------------------");

                    // 한 줄씩 읽기
                    string line;
                    Console.WriteLine("--- 외부 프로그램 결과 ---");
                    while ((line = reader.ReadLine()) != null)
                    {
                        Console.WriteLine(line);
                        //Console.Write(line + Environment.NewLine);
                    }
                    Console.WriteLine("--------------------------");
                }
                process.WaitForExit(); // 프로그램 종료 대기
            }
        }
    }
}

 

 

2025.04.14 - [C, C++] - 명령창(cmd)을 열지 않고 명령 실행하고 결과 받아오기 popen(pipe open), pclose(pipe close)

 

반응형
Posted by J-sean
: