[C#] 멀티스레드

728x90

1. 스레드 사용방법

일반적으로 프로그램은 한번에 하나씩 실행됩니다. 그러나 멀티스레드를 사용하면 병렬로 실행하게 만들 수 있습니다.

 

예제를 들어보겠습니다.

 

먼저 스레드를 사용하지 않는 경우를 보겠습니다.

 

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace WindowsFormsApp2
{
    public partial class Form1 : Form
    {
        Thread thread = null; //스레드 클래스 선언
        Thread thread2 = null;
        public Form1()
        {
            InitializeComponent();
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            work1();
            work2();
                        
        }
        
        /// <summary>
        /// 스레드가 사용할 함수
        /// </summary>
        private void work1() 
        {
            try
            {
                //실행함수
                Thread.Sleep(4000); //4초 동안 중단
                MessageBox.Show("thread 실행"); 
                                              
            }
            catch (Exception ex)
            { 
            
            }
        }
        private void work2()
        {
            try
            {
                //실행함수
                Thread.Sleep(5000); //5초 동안 중단
                MessageBox.Show("thread2 실행"); 
            }
            catch(Exception ex) 
            {
            
            }
        }
    }
}

 

위 코드를 실행해보면 처음 4초 후에 work1함수가 실행되며 그 후 5초 후에 work2함수가 실행됩니다. 또한 두개의 함수가 모두 실행된 후 form1이 load됩니다.

 

그러면 두번째는 멀티스레드를 이용하여 함수를 실행해 보겠습니다.

 

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace WindowsFormsApp2
{
    public partial class Form1 : Form
    {
        Thread thread = null; //스레드 클래스 선언
        Thread thread2 = null;
        public Form1()
        {
            InitializeComponent();
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            thread = new Thread(new ThreadStart(work1));
            thread.IsBackground = true; // UIThread가 종료될 때 work 스레드도 종료된다. //UIThread란 main함수에서 실행되는 스레드를 의미한다.
            thread.Priority = ThreadPriority.Normal; //OS(운영체제) 자원을 얼마나 자주 사용할 것인지
            thread.Start();

            thread2 = new Thread(new ThreadStart(work2));
            thread2.IsBackground = true; 
            thread2.Priority = ThreadPriority.Normal; 
            thread2.Start();

            
        }
        
        /// <summary>
        /// 스레드가 사용할 함수
        /// </summary>
        private void work1() 
        {
            try
            {
                //실행함수
                Thread.Sleep(4000); //4초 동안 중단
                MessageBox.Show("thread 실행"); //개별적으로 동작함
                                              //오래 걸리는 작업들을 스레드로 실행 ex)네트워크 전송,수신, DB작업
            }
            catch (Exception ex)
            { 
            
            }
        }
        private void work2()
        {
            try
            {
                //실행함수
                Thread.Sleep(5000); //6초 동안 중단
                MessageBox.Show("thread2 실행"); 
            }
            catch(Exception ex) 
            {
            
            }
        }
    }
}

 

위 코드를 실행해보면 가장 먼저 Form1 클래스가 load 되며 4초 후에 work1함수가 실행되고 1초후에 work2함수가 실행되는 것을 볼 수 있습니다. 

 

즉 오래걸리는 작업에 하나의 스레드를 부여하여 개별적으로 동작하게 만든다면 불필요한 시간낭비를 줄일 수 있고

 

코드를 다양하게 활용할 수 있습니다.

 

2. 스레드 문제해결

스레드를 제대로 종료하지 않을 경우 백그라운드에서 계속해서 실행되면서 자원을 낭비할 수 있습니다. 이를 방지하기 위해 메인스레드(UI스레드)가 종료될때 모든 스레드가 종료되어야 합니다. 이러한 방법에는 두가지가 있습니다.

 

1)스레드 자원이 남는 문제 해결

1] IsBackground 이용

첫번째 방법은 위에서 사용한 IsBackground를 사용하는 방법입니다. 위 코드에 주석으로 작성해놨듯이 IsBackground를 true로 준다면 UI스레드가 종료될 때 모든 스레드가 같이 종료됩니다.

 

2] 폼이 닫힐 때 스레드 자원을 해제

Form1.cs

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace WindowsFormsApp4
{
    public partial class Form1 : Form
    {
        Thread thread = null;

        public Form1()
        {
            InitializeComponent();
        }

        private void Form1_Load(object sender, EventArgs e)
        {
           
            Form2 form2 = new Form2();
            form2.Show();
        }

                
    }
}

 

Form2.cs

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace WindowsFormsApp4
{
    public partial class Form2 : Form
    {
        Thread thread = null;

        public Form2()
        {
            InitializeComponent();
        }

        private void Form2_Load(object sender, EventArgs e)
        {
            thread = new Thread(new ThreadStart(methodStart));
            thread.IsBackground = true;
            thread.Start();
        }

        private void methodStart()
        {
            while (true)
            {
                Thread.Sleep(1000);
                MessageBox.Show("실행중");
            }


        }
    }
}

 

위 코드를 실행해보시면 Form2에 IsBackground를 true로 했음에도 불구하고 실제로 실행시켜보면 form2를 종료시켜도 메시지박스는 계속해서 나타나는 것을 볼 수 있습니다.  이런 경우 폼이 클로징하는 이벤트에서 스레드 자원을 해제해 줘야 합니다.

 

수정된 Form2.cs

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace WindowsFormsApp4
{
    public partial class Form2 : Form
    {
        Thread thread = null;
        bool threadStop = false;
        
        public Form2()
        {
            InitializeComponent();
        }

        private void Form2_Load(object sender, EventArgs e)
        {
            thread = new Thread(new ThreadStart(methodStart));
            thread.IsBackground = true;
            thread.Start();
        }

        private void methodStart()
        {
            while (!threadStop)
            {
                Thread.Sleep(1000);
                MessageBox.Show("실행중");
            }


        }
                

        private void Form2_FormClosing(object sender, FormClosingEventArgs e)
        {
            
            threadStop = true;

            //폼이 닫힐때 스레드 자원을 해제
            if (thread != null)
            {                
                thread.Abort();
            }
            thread = null;
        }
    }
}

2)크로스 스레드

크로스 스레드는 컨트롤이 자신이 만들어진 스레드가 아닌 스레드에서 액세스되었을 경우에 나타납니다.
다시 말해 컨트롤을 스레드(이벤트 포함)로 사용하고 있는데, 다른 스레드에서 접근할 경우 나타납니다.

하나의 컨트롤을 동시에 접근할 경우 데이터의 접근 순서에 따라 전혀 다른 결과가 나올수 있기 때문에
에러를 발생한 것입니다.

 

위 코드에서 Form2에 컨트롤을 하나 생성하고 디버그모드로 실행해보겠습니다.

 

위 화면처럼 크로스 스레드 에러가 발생하는 것을 확인할 수 있습니다.

 

수정된 Form2.cs

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace WindowsFormsApp4
{
    public partial class Form2 : Form
    {
        Thread thread = null;
        bool threadStop = false;
        public Form2()
        {
            InitializeComponent();
        }

        private void Form2_Load(object sender, EventArgs e)
        {
            thread = new Thread(new ThreadStart(methodStart));
            thread.IsBackground = true;
            thread.Start();
        }

        private void methodStart()
        {

            ////MethodInvoker 사용
            //this.Invoke((MethodInvoker)delegate
            //{
            //    label1.Text = "form2 라벨";
            //});

            //new Action 사용
            this.Invoke(new Action(() =>
            {
                label1.Text = "form2 라벨";
            }));
            //비동기로 사용하고 싶다면 Invoke대신 BeginInvoke 사용

            while (!threadStop)
            {
                Thread.Sleep(1000);
                
            }


        }
                
        private void Form2_FormClosing(object sender, FormClosingEventArgs e)
        {
            
            threadStop = true;

            //폼이 닫힐때 스레드 자원을 해제
            if (thread != null)
            {                
                thread.Abort();
            }
            thread = null;
        }
    }
}

 

코드를 수정한 후 디버그모드를 실행하면 에러가 나지 않습니다.

3) 스레드 동기화

한 스레드가 진행 중인 작업을 다른 스레드가 간섭하지 못하도록 막는 것을 '스레드의 동기화(synchronization)'라고 합니다.

 

이러한 스레드 동기화가 필요한 이유는 둘 이상의 스레드가 자원을 공유하게 될 때 문제가 발생할 수 있기 때문입니다.

 

예컨대 메인스레드에서 a를 선언한 후 thread1에서는 a=3으로 변경하고 thread2에서는 a=5로 변경했을 때 thread2에서 a를 출력하려고 하면 thread1때문에 얻으려고 했던 값인 5가 아니라 3을 얻게될 수 있습니다.

 

이러한 문제를 방지하기 위해 동기화를 합니다.

 

스레드 동기화를 이해하기 위해서는 임계영역에 대해 알아야 합니다.

 

임계영역에 대한 내용은 아래 블로그를 참조하시면 됩니다.

[운영체제]임계영역(Critical Section) (velog.io)

 

[운영체제]임계영역(Critical Section)

임계영역이란? > 둘 이상의 스레드가 공유 자원에 접근할 때, 오직 한 스레드만 접근을 허용해야 하는 경우에 사용한다. 일반 동기화 객체와 달리 개별 프로세스의 유저(user) 메모리 영역에 존재

velog.io

 

 

쉽게 말해서 공유자원을 한 스레드가 실행될 때 독점한다고 생각하시면 됩니다.

 

문제가 되는 코드 예제를 확인해보겠습니다.

 

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace WindowsFormsApp2
{
    public partial class Form1 : Form
    {
        //공유자원 변수 a
        int a;

        Thread thread = null; //스레드 클래스 선언
        Thread thread2 = null;
        public Form1()
        {
            InitializeComponent();
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            thread = new Thread(new ThreadStart(work1));
            thread.IsBackground = true; 
            thread.Priority = ThreadPriority.Normal; 
            thread.Start();

            thread2 = new Thread(new ThreadStart(work2));
            thread2.IsBackground = true; 
            thread2.Priority = ThreadPriority.Normal; 
            thread2.Start();

            
        }
        
        /// <summary>
        /// 스레드가 사용할 함수
        /// </summary>
        private void work1() 
        {
            Thread.Sleep(2000);
            a = 3;
        }
        private void work2()
        {
            
            a = 5;
            Thread.Sleep(3000);

            MessageBox.Show("a값은:" + a);
        }
    }
}

 

위 코드를 실행해보면 MessageBox에서 5가 출력되어야 할 것 같지만 thread1때문에 3이 출력되는 것을 확인할 수 있습니다. 이러한 문제를 방지하기 위해 여러가지 방법들이 있습니다.

 

1] lock 적용

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace WindowsFormsApp2
{
    public partial class Form1 : Form
    {
        //공유자원 변수 a
        int a;
        object lockobj = new object();

        Thread thread = null; //스레드 클래스 선언
        Thread thread2 = null;
        public Form1()
        {
            InitializeComponent();
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            thread = new Thread(new ThreadStart(work1));
            thread.IsBackground = true; 
            thread.Priority = ThreadPriority.Normal; 
            thread.Start();

            thread2 = new Thread(new ThreadStart(work2));
            thread2.IsBackground = true; 
            thread2.Priority = ThreadPriority.Normal; 
            thread2.Start();

            
        }
        
        /// <summary>
        /// 스레드가 사용할 함수
        /// </summary>
        private void work1() 
        {
            while (true) //정확한 값이 지속적으로 나오는지 확인하기 위해 반복문 적용
            {
                //lockobj 상호배제 잠금 (1번)
                lock (lockobj)
                {
                	//실행 (2번)
                    Thread.Sleep(2000);
                    a = 3;
                }
                //상호배제 잠금 해제 (4번)
            }
        }
        private void work2()
        {
            while (true) //정확한 값이 지속적으로 나오는지 확인하기 위해 반복문 적용
            {
                //lockobj 상호배제 잠금 요청 ->thread1이 잠금이 되어 있으므로 대기(3번) -> thread1 상호배제 잠금 해제 후 thread2 잠금(5번)
                lock (lockobj)
                {
                	//실행(6번)
                    a = 5;
                    Thread.Sleep(3000);

                    MessageBox.Show("a값은:" + a);
                }
                //상호배제 잠금 해제(7번)
            }
        }
    }
}

 

위 코드를 실행해보면 a값은 계속 5가 나오는 것을 확인할 수 있습니다.

 

주의할 점은 아래와 같 이중 lock을 사용할 경우 DeadLock 이 발생할 수 있습니다. 

 

lock(obj)
{
	lock(obj)
    {
        //실행
    } //교착상태
}

 

 

2]AutoResetEvent

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace WindowsFormsApp2
{
    public partial class Form1 : Form
    {
        // 공유자원 변수 a
        int a;
        

        AutoResetEvent resetEvent = new AutoResetEvent(false);


        Thread thread = null; //스레드 클래스 선언
        Thread thread2 = null;
        public Form1()
        {
            InitializeComponent();
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            thread = new Thread(new ThreadStart(work1));
            thread.IsBackground = true; 
            thread.Priority = ThreadPriority.Normal; 
            thread.Start();

            thread2 = new Thread(new ThreadStart(work2));
            thread2.IsBackground = true; 
            thread2.Priority = ThreadPriority.Normal; 
            thread2.Start();

            
        }
        
        /// <summary>
        /// 스레드가 사용할 함수
        /// </summary>
        private void work1() 
        {
            while (true)
            {
                //신호를 받기 위해 기다리는 상태 (1번)
                resetEvent.WaitOne(); //신호를 받을때까지 대기 //AutoResetEvent가 true라면 첫번째 신호는 받은상태로 변경
                //메시지박스 출력(3번)
                MessageBox.Show(""+a);
            }
        }
        private void work2()
        {
           while(true)
            {

                a = 1;
                Thread.Sleep(5000);
                //신호를 보낸 상태(2번)
                resetEvent.Set(); 
                
            }
        }
    }
}

 

AutoResetEvent에서는 Set을 보내면 waitOne 아래의 코드가 실행됩니다.

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

[C#] 이벤트 게시 및 수신  (0) 2024.05.15
[C#] 비동기 프로그래밍  (0) 2024.05.15
[C#] 데이터 베이스 연결 및 기본 CRUD  (1) 2024.05.14
[C#] 객체지향 프로그래밍  (0) 2024.05.14
[C#] 배열과 foreach문(반복)  (0) 2024.05.14