[C#] CPU Usage 사용량

C# 2026. 4. 21. 12:36 |
반응형

Performance Counter를 사용해 여러 가지 정보를 가져올 수 있다.

 

using System;
using System.Diagnostics;
using System.Linq;

namespace ConsoleApp1
{
    internal class Program
    {
        static void Main(string[] args)
        {
            // 퍼포먼스 카운터의 카테고리를 가져와서 각 카테고리의 이름, 유형, 도움말을 출력하고,
            // 각 카테고리에 속한 인스턴스와 카운터를 출력하는 코드
            // 리스트가 너무 길어질 수 있으므로, "Processor Information" 카테고리에 속한 카운터만 출력하도록 필터링
            PerformanceCounterCategory[] categories = PerformanceCounterCategory.GetCategories();
            foreach (PerformanceCounterCategory category in categories)
            {
                Console.WriteLine("Category name: {0}", category.CategoryName);
                Console.WriteLine("Category type: {0}", category.CategoryType);
                Console.WriteLine("Category help: {0}", category.CategoryHelp);
                string[] instances = category.GetInstanceNames();

                if (instances.Any())
                {
                    foreach (string instance in instances)
                    {
                        if (category.InstanceExists(instance))
                        {
                            PerformanceCounter[] countersOfCategory = category.GetCounters(instance);
                            foreach (PerformanceCounter pc in countersOfCategory)
                            {
                                if (pc.CategoryName == "Processor Information")
                                {
                                    Console.WriteLine("■ Category: {0}, ■ Counter: {1}, ■ Instance: {2}", pc.CategoryName, pc.CounterName, instance);
                                }
                            }
                        }
                    }
                }
                else
                {
                    PerformanceCounter[] countersOfCategory = category.GetCounters();
                    foreach (PerformanceCounter pc in countersOfCategory)
                    {
                        if (pc.CategoryName == "Processor Information")
                        {
                            Console.WriteLine("Category: {0}, counter: {1}", pc.CategoryName, pc.CounterName);
                        }
                    }
                }
            }
        }
    }
}

 

 

위 결과를 이용해 CPU 사용량을 측정해 보자.

 

using System;
using System.Diagnostics;
using System.Threading;

namespace ConsoleApp1
{
    internal class Program
    {
        static void Main(string[] args)
        {
            string categoryName = "Processor Information";
            string counterName = "% Processor Utility";
            string instanceName = "_Total";
            float cpuPercent = 0.0f;

            PerformanceCounter cpuCounter = new PerformanceCounter(categoryName, counterName, instanceName);
            // categoryName: The category of the performance counter, in this case "Processor Information".
            // counterName: The specific counter to monitor, in this case "% Processor Utility".
            // instanceName: The instance of the counter, in this case "_Total" for overall CPU usage.

            // The first call to NextValue() returns 0, so we call it once before entering the loop            
            cpuCounter.NextValue();

            while (true)
            {
                Thread.Sleep(1000);
                cpuPercent = cpuCounter.NextValue();
                Console.WriteLine("CPU Usage: {0}%", cpuPercent);
            }
        }
    }
}

 

 

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

LiveCharts를 이용해 NVR Playback Timeline UI를 만들어 보자.

 

using LiveChartsCore;
using LiveChartsCore.SkiaSharpView;
using LiveChartsCore.SkiaSharpView.WinForms;
using LiveChartsCore.SkiaSharpView.Painting;
using SkiaSharp;
using LiveChartsCore.Defaults;

namespace WinFormsApp1
{
    public partial class Form1 : Form
    {
        private bool _isChartAdded = false;
        private System.Windows.Forms.Timer? panTimer = null;

        public Form1()
        {
            InitializeComponent();

            button1.Click += Button1_Click;
            button2.Click += Button2_Click;
        }

        private void Button2_Click(object? sender, EventArgs e)
        {
            // 타이머는 Button1_Click에서 차트를 생성할 때 초기화 된다.
            if (panTimer == null)
                return;

            if (panTimer.Enabled)
            {
                panTimer.Stop();
                button2.Text = "Play";
            }
            else
            {
                panTimer.Start();
                button2.Text = "Pause";
            }
        }

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

            DateTime now = DateTime.Now;

            // 비디오 존재 여부를 나타내는 타임라인 데이터 생성 (1: 비디오 있음, 0: 비디오 없음)
            System.Collections.Generic.List<DateTimePoint> valuesList = new System.Collections.Generic.List<DateTimePoint>();
            DateTime currentTime = now.AddDays(-2); // 2일 전부터 시작
            Random r = new Random();

            for (int i = 0; i < 50; i++) // 50회의 녹화/끊김 반복 (총 100개의 데이터셋)
            {
                // 비디오 시작
                valuesList.Add(new DateTimePoint(currentTime, 1));

                // 비디오 녹화 지속 시간 (10분 ~ 120분)
                currentTime = currentTime.AddMinutes(r.Next(10, 121)); // AddMinutes는 DateTime 객체에 지정된 분을 더하는 메서드입니다. r.Next(10, 121)은 10부터 120까지의 랜덤한 정수를 생성하여 녹화 지속 시간을 결정합니다.
                valuesList.Add(new DateTimePoint(currentTime, 0));

                // 다음 녹화까지 비어있는(녹화 안 됨) 시간 (5분 ~ 60분)
                currentTime = currentTime.AddMinutes(r.Next(5, 61));
            }

            // 진짜 데이터의 첫 시간과 마지막 시간 보관 (초기 화면 영역 설정 시 활용)
            long realMinTicks = valuesList[0].DateTime.Ticks;
            long realMaxTicks = valuesList[valuesList.Count - 1].DateTime.Ticks;

            DateTimePoint[] values = valuesList.ToArray();

            // 화면을 드래그할 때 마지막 데이터가 중앙에 올 수 있으려면,
            // 화면 절반 이상의 "보이지 않는 여백 데이터"가 필요합니다.
            // 앞뒤로 전체 데이터 길이/2만큼 X축의 패닝 한계를 늘려줍니다.
            TimeSpan paddingSpan = TimeSpan.FromTicks(realMaxTicks - realMinTicks);
            DateTime dummyStart = new DateTime(realMinTicks).Subtract(paddingSpan / 2);
            DateTime dummyEnd = new DateTime(realMaxTicks).Add(paddingSpan / 2);

            ISeries[] series = new ISeries[]
            {
                // StepLineSeries: 실제 데이터를 그립니다.
                new StepLineSeries<DateTimePoint>
                {
                    Values = values,
                    GeometrySize = 0, // 데이터 포인트 마커 숨김
                    GeometryFill = null, // 마우스 오버 시 나타나는 투명/반투명 마커 내부 숨김
                    GeometryStroke = null, // 마우스 오버 시 나타나는 투명/반투명 마커 외곽선 숨김
                    MiniatureShapeSize = 0, // 툴팁 안의 작은 마커를 0으로 만들어 숨김                    

                    Fill = new SolidColorPaint(SKColors.LightBlue.WithAlpha(120)), // 블록 내부 색상
                    Stroke = new SolidColorPaint(SKColors.DodgerBlue) { StrokeThickness = 1 }, // 블록 외곽선

                    // YToolTipLabelFormatter를 빈 문자열로 설정하여 0, 1 값이 툴팁에 표시되지 않도록 합니다.
                    YToolTipLabelFormatter = chartPoint => string.Empty,
                    // XToolTipLabelFormatter를 통해 원하는 텍스트만 표시합니다.
                    XToolTipLabelFormatter = chartPoint => string.Empty
                    // 툴팁에 날짜와 비디오 존재 여부를 표시하려면 아래와 같이 XToolTipLabelFormatter를 설정할 수 있습니다.
                    // 이 내용이 표시되게 하려면 아래 CartesianChart 생성 부분에서 TooltipPosition을 Hidden이 아닌 다른 값으로 설정해야 합니다. (예: TooltipPosition.Top)
                    //XToolTipLabelFormatter = chartPoint =>
                    //{
                    //    if (chartPoint?.Model == null) return string.Empty;
                    //    // SkiaSharp 기본 툴팁 렌더링에서 \n 같은 개행 문자를 인식하지 못하고 깨진 문자로 표시할 수 있습니다. 
                    //    // 따라서 한 줄로 풀어서 표시하거나 Environment.NewLine을 사용하는 것이 좋습니다.
                    //    return $"{chartPoint.Model.DateTime:yyyy-MM-dd HH:mm}" + Environment.NewLine +
                    //           (chartPoint.Model.Value == 1 ? "Video Start" : "Video End");
                    //}
                },

                // 투명한 더미 시리즈를 추가하여 X축의 "패닝 가능한 절대 영역(DataBounds)"을 강제로 늘려줍니다.
                // null 값은 계산에서 무시되므로 실제 값을 넣되 투명하게 만듭니다.
                new LineSeries<DateTimePoint>
                {
                    Values = new[]
                    {
                        new DateTimePoint(dummyStart, 0),
                        new DateTimePoint(dummyEnd, 0)
                    },
                    Fill = null,
                    Stroke = null,
                    GeometrySize = 0,
                    GeometryFill = null,
                    GeometryStroke = null,
                    YToolTipLabelFormatter = chartPoint => string.Empty,
                    XToolTipLabelFormatter = chartPoint => string.Empty
                }
            };

            // X축의 초기 최소/최대 기준값을 실제 데이터 범위로 설정
            long minTicks = realMinTicks;
            long maxTicks = realMaxTicks;

            Axis xAxis = new Axis
            {
                UnitWidth = TimeSpan.FromMinutes(1).Ticks, // 데이터 간의 주요 단위를 1분으로 설정
                MinStep = TimeSpan.FromMinutes(1).Ticks,

                // MinLimit과 MaxLimit을 실제 데이터의 최소/최대 시간으로 설정하여 초기 화면에서 전체 데이터 범위를 보여줍니다.
                MinLimit = realMinTicks,
                MaxLimit = realMaxTicks,
            };

            xAxis.Labeler = value =>
            {
                long ticks = (long)value;
                if (ticks < DateTime.MinValue.Ticks || ticks > DateTime.MaxValue.Ticks)
                    return string.Empty;

                DateTime date = new DateTime(ticks);
                // 현재 화면에 보이는 범위 (Zoom 상태)
                double min = xAxis.MinLimit ?? minTicks; // ?? 의미: xAxis.MinLimit이 null이 아니면 그 값을 사용하고, null이면 minTicks를 사용한다는 의미입니다. 즉, MinLimit이 설정되어 있으면 그 값을 사용하고, 그렇지 않으면 데이터의 최소 시간으로 간주합니다.
                double max = xAxis.MaxLimit ?? maxTicks;
                TimeSpan visibleSpan = TimeSpan.FromTicks((long)Math.Max(0, max - min));

                if (visibleSpan.TotalDays >= 1) // 1일 이상 보이면 날짜 표시
                    return date.ToString("MM-dd");
                else if (visibleSpan.TotalHours >= 2) // 2시간 이상 보이면 시간 표시
                    return date.ToString("MM-dd HH:00");
                else // 그 이하로 확대하면 분 단위까지 표시
                    return date.ToString("MM-dd HH:mm");
            };

            // LiveChartsCore는 다중 Y축을 지원하므로 YAxes 속성은 Axis 객체의 배열을 받습니다.
            // 이 차트에서는 단일 Y축만 필요하지만 속성 타입이 배열이기 때문에 Axis 배열로 선언합니다.
            Axis[] yAxes = new Axis[]
            {
                new Axis
                {
                    IsVisible = false, // Y축은 비디오 존재 유무(0과 1)만 나타내므로 숨깁니다.
                    MinLimit = 0,      // 바닥을 0으로 맞춤
                    MaxLimit = 1.2     // 비디오 블록(1) 상단에 여백을 조금 둠
                }
            };

            CartesianChart cartesianChart = new CartesianChart
            {
                Series = series,
                XAxes = new Axis[] { xAxis },
                YAxes = yAxes,
                TooltipPosition = LiveChartsCore.Measure.TooltipPosition.Hidden, // 툴팁 위치를 Hidden으로 설정하여 빈 툴팁 박스와 화살표를 완전히 숨깁니다.                
                Sections = new[]
                {
                    // X축과 일치하는 수평선 섹션 추가 (Y=0 위치에 수평선)
                    new RectangularSection
                    {
                        Yi = 0, // Y축의 시작 위치
                        Yj = 0, // Y축의 끝 위치 (0으로 설정하여 X축과 일치)
                        Stroke = new SolidColorPaint { Color = SKColors.DodgerBlue, StrokeThickness = 1 }
                    }
                },
                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
            };

            // 차트 중앙에 고정된 빨간 수직선(현재 재생 위치) 추가
            Panel centerLine = new Panel
            {
                BackColor = System.Drawing.Color.Red,
                Width = 2,
                Height = cartesianChart.Height,
                Location = new System.Drawing.Point(cartesianChart.Left + cartesianChart.Width / 2 - 1, cartesianChart.Top),
                //Anchor = AnchorStyles.Top | AnchorStyles.Bottom,
                Enabled = false // 마우스 이벤트가 차트로 전달되도록 비활성화
            };

            // 폼의 크기가 변경될 때 차트와 중앙선을 함께 조정하여 항상 중앙에 위치하도록 합니다.
            // 그런데 이 프로그램에서 차트의 사이즈는 고정되어 있기 때문에 폼의 크기가 변경되어도 차트의 크기는 변하지 않습니다.
            // 그러므로 차트의 Resize 이벤트는 트리거되지 않을 것입니다. 사이즈가 변경될 때 무엇인가를 조정하려면 폼의 Resize 이벤트를 사용하는 것이 더 적절할 수 있습니다.
            cartesianChart.Resize += (s, ev) =>
            {
                centerLine.Location = new System.Drawing.Point(cartesianChart.Left + cartesianChart.Width / 2 - 1, cartesianChart.Top); // 중앙선의 위치를 차트의 중앙으로 조정
                centerLine.Height = cartesianChart.Height;
            };

            // 차트가 업데이트(Zoom, Pan 등) 될 때 중앙 좌표(시간)를 계산하여 텍스트박스에 표시합니다.
            cartesianChart.UpdateStarted += (chart) =>
            {
                double min = xAxis.MinLimit ?? minTicks;
                double max = xAxis.MaxLimit ?? maxTicks;
                long center = (long)(min + (max - min) / 2);

                if (center >= DateTime.MinValue.Ticks && center <= DateTime.MaxValue.Ticks)
                {
                    DateTime centerTime = new DateTime(center);

                    // 빨간 선(중앙) 시간에 해당하는 Y값(비디오 존재 유무) 찾기
                    // 계단형(StepLine) 그래프이므로 현재 시간보다 작거나 같은 직전 데이터의 값을 사용합니다.
                    double centerValue = 0;
                    for (int i = values.Length - 1; i >= 0; i--)
                    {
                        if (values[i].DateTime.Ticks <= center)
                        {
                            centerValue = values[i].Value ?? 0;
                            break;
                        }
                    }

                    // 차트 업데이트 이벤트는 백그라운드 스레드에서 트리거될 수 있으므로 UI 스레드로 마샬링합니다.
                    if (textBox1.IsHandleCreated && !textBox1.IsDisposed)
                    {
                        textBox1.BeginInvoke(new Action(() =>
                        {
                            string status = centerValue == 1 ? "Video ON" : "Video OFF";
                            textBox1.Text = $"{centerTime:yyyy-MM-dd HH:mm:ss} [{status}]";
                        }));
                    }
                }
            };

            // 1초마다 차트를 1초 분량만큼 이동시키는 타이머 설정
            panTimer = new System.Windows.Forms.Timer
            {
                Interval = 1000 // 1000 밀리초 = 1초
            };
            panTimer.Tick += (s, ev) =>
            {
                if (xAxis.MinLimit.HasValue && xAxis.MaxLimit.HasValue)
                {
                    long oneSecondTicks = TimeSpan.FromSeconds(1).Ticks;
                    // X축의 최소/최대값에 1초를 더하여 화면(카메라)을 오른쪽으로 이동시킵니다.
                    // 이로 인해 시각적으로 차트의 데이터가 왼쪽으로 이동(스팬)하는 효과가 납니다.
                    // 반대 방향 이동을 원하실 경우 += 대신 -= 를 사용하시면 됩니다.
                    xAxis.MinLimit += oneSecondTicks;
                    xAxis.MaxLimit += oneSecondTicks;
                }
            };

            // cartesianChart 내부에 컨트롤을 추가하면 형변환 에러가 발생하므로 센터의 빨간 선은 폼의 Controls에 추가하여 차트 위에 표시되도록 합니다.            
            Controls.Add(centerLine);
            Controls.Add(cartesianChart);
            centerLine.BringToFront(); // 이 코드가 없어도 차트보다 빨간 선이 위에 표시되지만, 명시적으로 BringToFront()를 호출하여 확실히 한다.            
        }
    }
}

 

 

 

※ 참고

2026.04.07 - [C#] - [C#] DateTime Class & DateTimePicker

 

 

 

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

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"는 소수점 없는 숫자 형식 지정자.
        }
    }
}

 

빌드하고 실행한다.

 

 

Angular Gauge를 만들어 보자.

using LiveChartsCore.Defaults;
using LiveChartsCore.SkiaSharpView;
using LiveChartsCore.SkiaSharpView.Extensions;
using LiveChartsCore.SkiaSharpView.Painting;
using LiveChartsCore.SkiaSharpView.VisualElements;
using LiveChartsCore.SkiaSharpView.WinForms;
using SkiaSharp;

namespace WinFormsApp1
{
    public partial class Form1 : Form
    {
        private readonly PieChart pieChart = new PieChart();
        private readonly Random random = new Random();
        private bool _isChartAdded = false;

        public Form1()
        {
            InitializeComponent();

            button1.Click += Button1_Click;
        }

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

            double sectionsOuter = 130; // 섹션의 외부 반경 오프셋
            double sectionsWidth = 20; // 섹션의 최대 반경 너비

            NeedleVisual needle = new NeedleVisual
            {
                Value = 45 // 바늘이 가리키는 값 (pieChart MinValue와 MaxValue 사이)
            };

            pieChart.Series = GaugeGenerator.BuildAngularGaugeSections(
                    new GaugeItem(60, s => SetStyle(sectionsOuter, sectionsWidth, SKColors.LimeGreen, s)), // 첫 번째 섹션: 60% 비율, 녹색
                    new GaugeItem(30, s => SetStyle(sectionsOuter, sectionsWidth, SKColors.Gold, s)),      // 두 번째 섹션: 30% 비율, 노란색
                    new GaugeItem(10, s => SetStyle(sectionsOuter, sectionsWidth, SKColors.Crimson, s)));  // 세 번째 섹션: 10% 비율, 빨간색
            pieChart.VisualElements = [
                new AngularTicksVisual
                {
                    Labeler = value => value.ToString("N1"), // 눈금 레이블 포맷, N1은 소수점 한 자리까지 표시
                    LabelsSize = 16,
                    LabelsOuterOffset = 15,
                    OuterOffset = 65,
                    TicksLength = 20
                },
                needle
                ];
            pieChart.InitialRotation = -225; // 시작 각도 설정
            pieChart.MaxAngle = 270; // 게이지가 차지하는 각도 설정
            pieChart.MinValue = 0; // 게이지의 최소값 설정
            pieChart.MaxValue = 100; // 게이지의 최대값 설정
            pieChart.Location = new System.Drawing.Point(0, 0);
            pieChart.Size = new System.Drawing.Size(400, 400);
            //Anchor = AnchorStyles.Left | AnchorStyles.Right | AnchorStyles.Top | AnchorStyles.Bottom;

            Controls.Add(pieChart);

            Button b1 = new Button
            {
                Text = "Update",
                Location = new System.Drawing.Point(400, 300),
                Size = new System.Drawing.Size(100, 50)
            };
            b1.Click += (sender, e) => needle.Value = random.Next(0, 100);
            Controls.Add(b1);
            b1.BringToFront();
        }

        private static void SetStyle(double sectionsOuter, double sectionsWidth, SKColor color, PieSeries<ObservableValue> series)
        {
            series.OuterRadiusOffset = sectionsOuter; // 섹션의 외부 반경 오프셋 설정
            series.MaxRadialColumnWidth = sectionsWidth; // 섹션의 최대 반경 너비 설정
            series.CornerRadius = 0; // 섹션의 모서리 반경 설정
            series.Fill = new SolidColorPaint(color); // 섹션의 칠하기 색상 설정
        }
    }
}

 

 

※ 참고

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
:
반응형

문화에 따라 다르게 표현되는 데이터를 일관되게 표현해 보자.

 

namespace ConsoleApp1
{
    using System.Globalization;

    internal class Program
    {
        static void Main(string[] args)
        {
            var allCultures = CultureInfo.GetCultures(CultureTypes.AllCultures);
            foreach (var ci in allCultures)
            {
                // Display the name of each culture.
                Console.Write($"{ci.EnglishName} ({ci.Name}): ");
                // Indicate the culture type.
                if (ci.CultureTypes.HasFlag(CultureTypes.NeutralCultures))
                    Console.Write(" NeutralCulture");
                if (ci.CultureTypes.HasFlag(CultureTypes.SpecificCultures))
                    Console.Write(" SpecificCulture");
                Console.WriteLine();
            }

            Console.WriteLine("Current culture is {0}", CultureInfo.CurrentCulture.Name);

            double value = 1234.56;
            
            Console.WriteLine(value);
            Console.WriteLine(value.ToString());
            // The current culture is a property of the executing thread.
            Console.WriteLine(value.ToString(CultureInfo.CurrentCulture));
            // The invariant culture is culture-insensitive; it's associated with the English
            // language but not with any country/region.
            Console.WriteLine(value.ToString(CultureInfo.InvariantCulture));

            CultureInfo.CurrentCulture = new CultureInfo("de-DE");

            Console.WriteLine("Current culture is {0}", CultureInfo.CurrentCulture.Name);
            Console.WriteLine(value);
            Console.WriteLine(value.ToString());
            Console.WriteLine(value.ToString(CultureInfo.CurrentCulture));
            Console.WriteLine(value.ToString(CultureInfo.InvariantCulture));

            CultureInfo.CurrentCulture = new CultureInfo("en-US");

            Console.WriteLine("Current culture is {0}", CultureInfo.CurrentCulture.Name);
            Console.WriteLine(value);
            Console.WriteLine(value.ToString());
            Console.WriteLine(value.ToString(CultureInfo.CurrentCulture));
            Console.WriteLine(value.ToString(CultureInfo.InvariantCulture));
        }
    }
}

 

 

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

난수를 만들고 섞어보자.

 

namespace ConsoleApp1
{
    internal class Program
    {
        static void Main(string[] args)
        {
            Random random = new Random(); // 난수 생성기

            // byte 배열 난수 생성
            byte[] bytes = new byte[10];            
            random.NextBytes(bytes);
            foreach (byte b in bytes)
            {
                Console.Write($"{b}, ");
            }
            Console.WriteLine();

            // 난수 섞기
            random.Shuffle<byte>(bytes);
            foreach (byte b in bytes)
            {
                Console.Write($"{b}, ");
            }
            Console.WriteLine();

            // 문자열 리스트 섞기
            List<string> fruits = new List<string> { "사과", "바나나", "딸기", "오렌지", "수박", "멜론", "키위" };
            string[] fruitsArray = fruits.ToArray();
            random.Shuffle<string>(fruitsArray);
            
            foreach (string fruit in fruitsArray)
            {
                Console.Write(fruit + ", ");
            }
            Console.WriteLine();

            // 정수 리스트 생성
            List<int> numbers = Enumerable.Range(1, 10).ToList();
            foreach (int i in numbers)
            {
                Console.Write(i + ", ");
            }
            Console.WriteLine();

            // 정수 배열 섞기
            int[] numbersArray = numbers.ToArray();
            random.Shuffle<int>(numbersArray);
            foreach (int i in numbersArray)
            {
                Console.Write(i + ", ");
            }
            Console.WriteLine();

            // 섞인 정수 배열 일부 선택
            foreach (int i in numbersArray[2..^5]) // '^'는 뒤에서 부터 인덱싱. ^5는 파이썬의 -5와 동일.
            {
                Console.Write(i + ", ");
            }
            Console.WriteLine();

            // 리스트 컴프리헨션 기능
            List<int> squared = (from x in numbers
                                 where x % 2 == 0 // 짝수라는 조건이 필요 없다면 삭제 가능.
                                 select x * x).ToList();
            foreach (int i in squared)
            {
                Console.Write(i + ", ");
            }
            Console.WriteLine();
        }
    }
}

 

 

정답(2)을 포함하는 선택지를 만들어 보자.

 

namespace ConsoleApp1
{
    internal class Program
    {
        static void Main(string[] args)
        {
            Random random = new Random(); // 난수 생성기

            // 1~10 정수 리스트 생성
            List<int> numbers = Enumerable.Range(1, 10).ToList();

            // 특정 숫자(2)를 포함하는 선택지 4개 만들기
            List<int> choices = new List<int>();
            choices.Add(2); // 2는 무조건 포함
            while (choices.Count < 4)
            {
                int i = random.Next(0, numbers.Count);
                if (!choices.Contains(numbers[i]))
                    choices.Add(numbers[i]);
            }

            // 선택지 섞기
            choices = choices.Shuffle<int>().ToList();
            foreach (int i in choices)
            {
                Console.Write(i + ", ");
            }
            Console.WriteLine();
        }
    }
}

 

 

난수 생성기 없이 만들 수도 있다.

 

namespace ConsoleApp1
{
    internal class Program
    {
        static void Main(string[] args)
        {
            IEnumerable<int> numbers = Enumerable.Range(1, 10);
            numbers = numbers.Shuffle();
            // 1~10 정수를 생성하고 섞기

            IEnumerable<int> choices = [2]; // 선택지에 2는 무조건 포함

            foreach (int n in numbers)
            {
                if (!choices.Contains(n))
                    choices = choices.Append(n);

                if (choices.Count() >= 4)
                    break;
            }

            choices = choices.Shuffle();

            foreach (int i in choices)
                Console.Write(i + ", ");
        }
    }
}

 

 

반응형
Posted by J-sean
: