앞서 언급했듯이 각 스레드는 자체 리소스를 가지지만 코드 영역은 공유됩니다. 즉, 각 스레드는 동일한 기능을 실행할 수 있습니다. 이로 인해 발생할 수 있는 문제는 여러 스레드가 동시에 함수를 실행하여 데이터 혼란과 예측할 수 없는 결과를 초래한다는 점이므로 이러한 상황을 피해야 합니다.
C#은 코드 섹션을 상호 배타적 섹션(중요 섹션)으로 정의할 수 있는 키워드 잠금을 제공합니다. 상호 배타적 섹션에서는 한 번에 하나의 스레드만 실행에 들어가고 다른 스레드는 기다려야 합니다. C#에서 lock 키워드는 다음과 같이 정의됩니다.
잠금(표현식) 문_블록
표현식은 추적하려는 개체(일반적으로 개체 참조)를 나타냅니다.
state_block은 상호 배타적인 섹션의 코드입니다. 이 코드는 한 번에 하나의 스레드에서만 실행될 수 있습니다.
다음은 lock 키워드를 사용하는 일반적인 예입니다. lock 키워드의 사용법과 목적은 주석에 설명되어 있습니다.
예는 다음과 같습니다:
암호
시스템 사용;
System.Threading 사용;
네임스페이스 ThreadSimple
{
내부 클래스 계정
{
정수 균형;
무작위 r = 새로운 무작위();
내부 계정(int 초기)
{
잔액 = 초기;
}
내부 int 출금(int 금액)
{
if (잔액 < 0)
{
//잔액이 0보다 작으면 예외 발생
throw new Exception("마이너스 잔액");
}
//다음 코드는 현재 스레드가 Balance 값을 수정하기 전에 완료되도록 보장됩니다.
//다른 스레드는 잔액 값을 수정하기 위해 이 코드를 실행하지 않습니다.
//따라서 잔액의 값은 0보다 작을 수 없습니다.
자물쇠(이것)
{
Console.WriteLine("현재 스레드:"+Thread.CurrentThread.Name);
//lock 키워드의 보호가 없는 경우 if 조건부 판단을 실행한 후일 수 있습니다.
//다른 스레드가 Balance=balance-amount를 실행하고 Balance 값을 수정했습니다.
//이 수정 사항은 이 스레드에 표시되지 않으므로 if 조건이 더 이상 유지되지 않을 수 있습니다.
//그러나 이 스레드는 Balance=balance-amount를 계속 실행하므로 잔액이 0보다 작을 수 있습니다.
if (잔액 >= 금액)
{
Thread.Sleep(5);
잔액 = 잔액 - 금액;
반품 금액;
}
또 다른
{
0을 반환합니다. // 거래가 거부되었습니다.
}
}
}
내부 무효 DoTransactions()
{
for (int i = 0; i < 100; i++)
Withdraw(r.Next(-50, 100));
}
}
내부 클래스 테스트
{
정적 내부 Thread[] 스레드 = 새 Thread[10];
공개 정적 무효 Main()
{
계정 계정 = 새 계정(0);
for (int i = 0; i < 10; i++)
{
스레드 t = new Thread(new ThreadStart(acc.DoTransactions));
스레드[i] = t;
}
for (int i = 0; i < 10; i++)
스레드[i].Name=i.ToString();
for (int i = 0; i < 10; i++)
스레드[i].Start();
Console.ReadLine();
}
}
}모니터 클래스가 객체를 잠급니다.
여러 스레드가 하나의 개체를 공유하는 경우에도 일반적인 코드와 유사한 문제가 발생하므로 이러한 문제의 경우 lock 키워드를 사용하면 안 됩니다. 여기서는 System.Threading의 Monitor 클래스를 사용하면 됩니다. , 모니터는 스레드가 리소스를 공유할 수 있는 솔루션을 제공합니다.
Monitor 클래스는 개체를 잠글 수 있으며 스레드는 잠금을 획득한 경우에만 개체에 대해 작업을 수행할 수 있습니다. 개체 잠금 메커니즘은 혼란을 야기할 수 있는 상황에서 한 번에 하나의 스레드만 이 개체에 액세스할 수 있도록 보장합니다. 모니터는 특정 객체와 연결되어야 하지만 정적 클래스이기 때문에 객체를 정의하는 데 사용할 수 없으며, 해당 메서드도 모두 정적이므로 객체를 사용하여 참조할 수 없습니다. 다음 코드는 Monitor를 사용하여 개체를 잠그는 방법을 보여줍니다.
...
대기열 oQueue=새 대기열();
...
Monitor.Enter(oQueue);
......//이제 oQueue 개체는 현재 스레드에서만 조작할 수 있습니다.
Monitor.Exit(oQueue);//잠금을 해제합니다.
위에 표시된 대로 스레드가 개체를 잠그기 위해 Monitor.Enter() 메서드를 호출하면 개체는 해당 개체의 소유가 됩니다. 다른 스레드가 이 개체에 액세스하려는 경우 해당 개체가 Monitor.Exit()를 사용할 때까지만 기다릴 수 있습니다. 잠금을 해제하는 방법. 스레드가 결국 잠금을 해제하도록 하려면 try-catch-finally 구조의 finally 코드 블록에 Monitor.Exit() 메서드를 작성할 수 있습니다.
Monitor에 의해 잠긴 개체의 경우 이와 관련된 일부 정보가 메모리에 저장됩니다.
하나는 현재 잠금을 보유하고 있는 스레드에 대한 참조입니다.
두 번째는 잠금을 획득할 준비가 된 스레드를 저장하는 준비 대기열입니다.
세 번째는 현재 객체의 상태가 변경되기를 기다리고 있는 큐에 대한 참조를 보유하는 대기 큐입니다.
개체 잠금을 소유한 스레드가 잠금을 해제할 준비가 되면 Monitor.Pulse() 메서드를 사용하여 대기 대기열의 첫 번째 스레드에 알리므로 개체 잠금이 해제되면 스레드가 준비 대기열로 전송됩니다. , 준비 대기열에서 스레드는 즉시 개체 잠금을 획득할 수 있습니다.
다음은 lock 키워드와 Monitor 클래스를 사용하여 스레드 동기화 및 통신을 구현하는 방법을 보여주는 예입니다. 이는 일반적인 생산자 및 소비자 문제이기도 합니다.
이 루틴에서는 생산자 스레드와 소비자 스레드가 교대로 숫자를 쓰고 소비자는 즉시 이를 읽고 표시합니다(프로그램의 본질은 주석에 소개됩니다).
사용되는 시스템 네임스페이스는 다음과 같습니다.
시스템 사용;
System.Threading 사용;
먼저, 작업 중인 개체의 Cell 클래스를 정의합니다. 이 클래스에는 ReadFromCell() 및 WriteToCell이라는 두 가지 메서드가 있습니다. 소비자 스레드는 ReadFromCell()을 호출하여 cellContents의 내용을 읽고 표시하며, 생산자 프로세스는 WriteToCell() 메서드를 호출하여 cellContents에 데이터를 씁니다.
예는 다음과 같습니다:
암호
공개 클래스 셀
{
int cellContents; //Cell 객체의 내용
bool readerFlag = false; // 상태 플래그, true이면 읽을 수 있고, false이면 쓰기 중입니다.
공개 int ReadFromCell()
{
lock(this) // Lock 키워드는 무엇을 보장하나요? 잠금에 대한 이전 소개를 읽어보세요.
{
if (!readerFlag)//지금 읽을 수 없는 경우
{
노력하다
{
//WriteToCell 메서드에서 Monitor.Pulse() 메서드가 호출될 때까지 기다립니다.
모니터.잠깐(this);
}
잡기(SynchronizationLockException e)
{
Console.WriteLine(e);
}
잡기(ThreadInterruptedExceptione)
{
Console.WriteLine(e);
}
}
Console.WriteLine("소비: {0}",cellContents);
readerFlag = 거짓;
//소비 동작이 완료되었음을 나타내기 위해 readerFlag 플래그를 재설정합니다.
모니터.펄스(this);
//WriteToCell() 메서드에 알림(이 메서드는 다른 스레드에서 실행되어 대기 중입니다.)
}
cellContents를 반환합니다.
}
공공 무효 WriteToCell(int n)
{
자물쇠(이것)
{
if(리더플래그)
{
노력하다
{
모니터.잠깐(this);
}
잡기(SynchronizationLockException e)
{
//비동기 코드 영역에서 동기화된 메소드(Enter를 제외한 Monitor 클래스의 메소드 참조)를 호출하는 경우
Console.WriteLine(e);
}
잡기(ThreadInterruptedException e)
{
//스레드가 대기 상태일 때 중단
Console.WriteLine(e);
}
}
셀컨텐츠 = n;
Console.WriteLine("생산: {0}",cellContents);
readerFlag = true;
모니터.펄스(this);
//다른 스레드에서 대기 중인 ReadFromCell() 메서드에 알림
}
}
}
생산자 클래스 CellProd와 소비자 클래스 CellCons는 둘 다 하나의 메서드 ThreadRun()만 갖고 있으므로 Main() 함수에서 스레드에 제공된 ThreadStart 프록시 개체가 스레드에 대한 입구 역할을 합니다.
공개 클래스 CellProd
{
Cell cell; // 연산 중인 셀 객체
int amount = 1; // 생산자가 생산하는 횟수, 1로 초기화됨
public CellProd(셀 상자, int 요청)
{
//건설자
셀 = 상자;
수량 = 요청;
}
공공 무효 ThreadRun( )
{
for(int 루퍼=1; 루퍼<=수량; 루퍼++)
cell.WriteToCell(looper); //생산자는 작업 객체에 정보를 씁니다.
}
}
공개 클래스 CellCons
{
세포세포;
정수 수량 = 1;
public CellCons(셀 상자, int 요청)
{
//건설자
셀 = 상자;
수량 = 요청;
}
공공 무효 ThreadRun( )
{
int valReturned;
for(int 루퍼=1; 루퍼<=수량; 루퍼++)
valReturned=cell.ReadFromCell();//소비자는 작업 개체에서 정보를 읽습니다.
}
}
그런 다음 아래 MonitorSample 클래스의 Main() 함수에서 우리가 해야 할 일은 생산자와 소비자로 두 개의 스레드를 생성하고 CellProd.ThreadRun() 메서드와 CellCons.ThreadRun() 메서드를 사용하여 동일한 작업을 수행하는 것입니다. 셀 개체가 작동합니다.
암호
공개 클래스 MonitorSample
{
공개 정적 무효 Main(String[] args)
{
int result = 0; //플래그 비트입니다. 0이면 프로그램에 오류가 없음을 의미합니다.
셀 셀 = 새로운 셀( );
//다음은 cell을 사용하여 CellProd 및 CellCons 클래스를 초기화합니다. 생산 시간과 소비 시간은 모두 20배입니다.
CellProd prod = new CellProd(cell, 20);
CellCons cons = new CellCons(셀, 20);
스레드 생산자 = new Thread(new ThreadStart(prod.ThreadRun));
스레드 소비자 = new Thread(new ThreadStart(cons.ThreadRun));
//생산자 스레드와 소비자 스레드가 모두 생성되었지만 실행이 시작되지 않았습니다.
노력하다
{
생산자.시작( );
소비자.시작( );
생산자.조인( );
소비자.참여( );
Console.ReadLine();
}
잡기(ThreadStateExceptione)
{
//스레드의 상태로 인해 요청한 작업을 수행할 수 없는 경우
Console.WriteLine(e);
결과 = 1;
}
잡기(ThreadInterruptedException e)
{
//스레드가 대기 상태일 때 중단
Console.WriteLine(e);
결과 = 1;
}
//Main() 함수는 값을 반환하지 않지만, 다음 문은 실행 결과를 상위 프로세스로 반환할 수 있습니다.
Environment.ExitCode = 결과;
}
}
위 루틴에서는 Monitor.Pulse()를 기다리면서 동기화가 이루어집니다. 먼저, 생산자는 가치를 생산하고, 동시에 생산자로부터 생산이 완료되었음을 알리는 "Pulse"를 받을 때까지 대기 상태에 있으며, 그 후 소비자는 소비 상태에 진입합니다. 생산자는 소비자가 완료될 때까지 기다리기 시작합니다. Monitor.Pulese()에서 발생하는 "펄스"는 작업 후에 호출됩니다.
실행 결과는 간단합니다.
생산: 1
소비: 1
생산 : 2
소비: 2
생산 : 3
소비: 3
...
...
생산 : 20
소비: 20
실제로 이 간단한 예제는 멀티 스레드 응용 프로그램에서 발생할 수 있는 큰 문제를 해결하는 데 도움이 되었습니다. 스레드 간의 충돌을 해결하는 기본 방법을 이해하는 한 이를 더 복잡한 프로그램에 쉽게 적용할 수 있습니다.
-