멀티 스레드 응용 프로그램을 작성할 때 발생하는 가장 일반적인 문제 중 하나는 경쟁 조건입니다.
커뮤니티에 대한 나의 질문은 :
경쟁 조건은 무엇입니까?
그것들을 어떻게 감지합니까?
어떻게 처리합니까?
마지막으로, 당신은 어떻게 발생하지 않도록합니까?
멀티 스레드 응용 프로그램을 작성할 때 발생하는 가장 일반적인 문제 중 하나는 경쟁 조건입니다.
커뮤니티에 대한 나의 질문은 :
경쟁 조건은 무엇입니까?
그것들을 어떻게 감지합니까?
어떻게 처리합니까?
마지막으로, 당신은 어떻게 발생하지 않도록합니까?
답변:
경쟁 조건은 둘 이상의 스레드가 공유 데이터에 액세스 할 수 있고 동시에 변경하려고 할 때 발생합니다. 스레드 예약 알고리즘은 언제든지 스레드간에 교환 할 수 있으므로 스레드가 공유 데이터에 액세스하려는 순서를 모릅니다. 따라서, 데이터 변경의 결과는 스레드 스케줄링 알고리즘에 의존한다. 즉, 두 스레드 모두 데이터를 액세스 / 변경하기 위해 "레이싱"하고있다.
하나의 스레드가 "check-then-act"(예 : 값이 X 인 경우 "check", 값은 X 인 값에 의존하는 무언가를 수행하려면 "act")를 수행하고 다른 스레드는 "확인"과 "행위"사이. 예 :
if (x == 5) // The "Check"
{
y = x * 2; // The "Act"
// If another thread changed x in between "if (x == 5)" and "y = x * 2" above,
// y will not be equal to 10.
}
요점은 y가 10이 될 수도 있고 다른 스레드가 검사와 작동 사이에서 x를 변경했는지 여부에 따라 무엇이든 될 수 있습니다. 당신은 알 방법이 없습니다.
경쟁 조건이 발생하지 않도록하려면 일반적으로 한 번에 하나의 스레드 만 데이터에 액세스 할 수 있도록 공유 데이터를 잠그십시오. 이것은 다음과 같은 것을 의미합니다.
// Obtain lock for x
if (x == 5)
{
y = x * 2; // Now, nothing can change x until the lock is released.
// Therefore y = 10
}
// release lock for x
공유 자원에 액세스하는 멀티 스레드 (또는 병렬) 코드가 예기치 않은 결과를 초래할 수있는 경우 "경합 상태"가 존재합니다.
이 예제를 보자 :
for ( int i = 0; i < 10000000; i++ )
{
x = x + 1;
}
이 코드를 한 번에 5 개의 스레드로 실행하면 x WOULD 값은 50,000,000이되지 않습니다. 실제로는 실행마다 다릅니다.
각 스레드가 x 값을 늘리려면 다음을 수행해야하기 때문입니다.
x의 값을 구합니다 이 값에 1을 더하십시오 이 값을 x에 저장
모든 스레드는 언제든지이 프로세스의 모든 단계에있을 수 있으며 공유 리소스가 관련 될 때 서로 밟을 수 있습니다. x의 상태는 x를 읽는 시간과 다시 쓰는 시간 동안 다른 스레드에 의해 변경 될 수 있습니다.
스레드가 x 값을 검색하지만 아직 저장하지 않았다고 가정 해 봅시다. 다른 스레드도 동일한 값의 x를 검색 할 수 있으며 (스레드가 아직 변경되지 않았기 때문에) 동일한 값 (x + 1)을 다시 x에 저장합니다 !
예:
스레드 1 : x를 읽고 값은 7입니다. 스레드 1 : x에 1을 더하면 값은 8입니다. 스레드 2 : x를 읽고 값은 7입니다. 스레드 1 : x를 8로 저장 스레드 2 : 1을 x에 더하고 값은 8입니다. 스레드 2 : x에 8을 저장
공유 리소스에 액세스하는 코드 앞에 일종의 잠금 메커니즘 을 사용하여 경쟁 조건을 피할 수 있습니다 .
for ( int i = 0; i < 10000000; i++ )
{
//lock x
x = x + 1;
//unlock x
}
여기서 대답은 매번 50,000,000으로 나타납니다.
잠금에 대한 자세한 내용은 뮤텍스, 세마포어, 중요 섹션, 공유 리소스를 검색하십시오.
경쟁 조건이란 무엇입니까?
오후 5시에 영화를 보러 갈 계획입니다. 오후 4시에 티켓의 가용성에 대해 문의하십시오. 담당자는 이용 가능하다고 말합니다. 공연 5 분 전에 휴식을 취하고 티켓 창에 도착하십시오. 나는 당신이 무슨 일이 일어 났는지 추측 할 수 있다고 확신합니다 : 그것은 풀 하우스입니다. 여기서 문제는 점검과 조치 사이의 기간에있었습니다. 당신은 4시에 물었고 5시에 행동했습니다. 그동안 누군가 다른 사람이 티켓을 bb습니다. 이는 경쟁 조건입니다. 특히 경쟁 조건에 대한 "확인 후 행동"시나리오입니다.
당신은 그들을 어떻게 감지합니까?
종교적 코드 검토, 다중 스레드 단위 테스트. 바로 가기가 없습니다. 여기에는 Eclipse 플러그인이 거의 없지만 아직 안정된 것은 없습니다.
어떻게 처리하고 방지합니까?
가장 좋은 방법은 부작용이없고 상태 비 저장 기능을 만들고 가능한 한 불변을 사용하는 것입니다. 그러나 이것이 항상 가능한 것은 아닙니다. 따라서 java.util.concurrent.atomic을 사용하면 동시 데이터 구조, 적절한 동기화 및 액터 기반 동시성이 도움이됩니다.
동시성에 가장 적합한 리소스는 JCIP입니다. 위의 설명에 대한 자세한 내용은 여기를 참조하십시오 .
경쟁 조건과 데이터 경쟁간에 중요한 기술적 차이가 있습니다. 대부분의 답변은 이러한 용어가 동일하다고 가정하지만 그렇지는 않습니다.
데이터 레이스는 2 개의 명령어가 동일한 메모리 위치에 액세스 할 때 발생하며, 이러한 액세스 중 적어도 하나는 쓰기이며 이러한 액세스 중 순서 전에 발생 하지 않습니다 . 이제 주문하기 전에 발생하는 것은 많은 논쟁의 대상이지만 일반적으로 동일한 잠금 변수의 ulock-lock 쌍과 동일한 조건 변수의 대기 신호 쌍은 사전 순서를 유발합니다.
경쟁 조건은 의미 상 오류입니다. 잘못된 프로그램 동작으로 이어지는 이벤트의 타이밍 또는 순서에 발생하는 결함입니다 .
많은 경쟁 조건이 데이터 경쟁으로 인해 발생할 수 있지만 실제로는 그렇지 않습니다. 사실, 데이터 경쟁과 경쟁 조건은 서로에게 필요한 조건도 충분하지도 않습니다. 이 블로그 게시물은 간단한 은행 거래 예제를 통해 차이점을 잘 설명합니다. 차이점을 설명하는 또 다른 간단한 예가 있습니다.
이제 용어를 정리 했으므로 원래 질문에 대답하려고합니다.
경쟁 조건이 의미 상 버그라는 점을 감안할 때 일반적인 버그 감지 방법은 없습니다. 일반적인 경우에 올바른 프로그램 동작과 잘못된 프로그램 동작을 구별 할 수있는 자동화 된 Oracle을 보유 할 방법이 없기 때문입니다. 레이스 감지는 결정 불가능한 문제입니다.
반면에 데이터 레이스는 정확성과 반드시 관련이없는 정확한 정의를 가지므로이를 감지 할 수 있습니다. 다양한 데이터 레이스 탐지기 (정적 / 동적 데이터 레이스 감지, 잠금 기반 데이터 레이스 감지, 이전 데이터 레이스 감지, 하이브리드 데이터 레이스 감지)가 있습니다. 최첨단 동적 데이터 레이스 검출기는 ThreadSanitizer 로, 실제로 매우 잘 작동합니다.
일반적으로 데이터 경쟁을 처리하려면 공유 데이터에 대한 액세스 사이 (개발 중 또는 위에서 언급 한 도구를 사용하여 감지 된 경우) 사이의 에지를 유발하는 프로그래밍 규칙이 필요합니다. 이것은 잠금, 조건 변수, 세마포어 등을 통해 수행 될 수 있습니다. 그러나 구성에 의한 데이터 경쟁을 피하는 메시지 공유 (공유 메모리 대신)와 같은 다른 프로그래밍 패러다임을 사용할 수도 있습니다.
표준 정렬 정의는 " 두 개의 스레드가 동시에 메모리의 동일한 위치에 액세스하고 액세스 중 하나 이상이 쓰기 "인 경우입니다. 상황에서 "리더"스레드는 "레이스를이기는"스레드에 따라 이전 값 또는 새 값을 얻을 수 있습니다. 이것이 항상 버그는 아니지만 실제로는 털이 많은 저수준 알고리즘이 의도적으로이를 수행하지만 일반적으로 피해야합니다. @Steve Gury는 언제 문제가 될 수 있는지에 대한 좋은 예입니다.
경쟁 조건은 일종의 버그이며 특정 시간적 조건에서만 발생합니다.
예 : 두 개의 스레드 A와 B가 있다고 가정하십시오.
스레드 A에서 :
if( object.a != 0 )
object.avg = total / object.a
스레드 B에서 :
object.a = 0
객체 A를 검사 한 직후에 스레드 A가 선점되면 a는 null이 아니고 B는 수행 a = 0
하고 스레드 A가 프로세서를 확보하면 "0으로 나누기"를 수행합니다.
이 버그는 스레드 A가 if 문 바로 뒤에 선점 된 경우에만 발생하지만 매우 드물지만 발생할 수 있습니다.
경쟁 조건은 소프트웨어와 관련이있을뿐만 아니라 하드웨어와도 관련이 있습니다. 실제로이 용어는 처음에 하드웨어 산업에서 만들어졌습니다.
Wikipedia 에 따르면 :
이 용어 는 출력 에 영향 을 미치기 위해 서로 경주 하는 두 신호 의 개념에서 비롯됩니다 .
논리 회로의 경쟁 조건 :
소프트웨어 산업은이 용어를 수정하지 않고 이해하기가 조금 어려웠습니다.
소프트웨어 세계에 매핑하려면 대체를 수행해야합니다.
따라서 소프트웨어 산업의 경쟁 조건은 "일부 공유 상태에 영향을 미치기"위해 서로 경쟁하는 "두 스레드"/ "두 프로세스"를 의미하며, 공유 상태의 최종 결과는 일부 미묘한 타이밍 차이에 따라 달라집니다. 스레드 / 프로세스 실행 순서, 스레드 / 프로세스 스케줄링 등
경쟁 조건은 동시 프로그래밍에서 두 개의 동시 스레드 또는 프로세스가 리소스를 놓고 경쟁하며 최종 최종 상태는 누가 리소스를 먼저 얻는 지에 따라 달라집니다.
경쟁 조건은 다중 스레드 응용 프로그램 또는 다중 프로세스 시스템에서 발생합니다. 경쟁 조건은 가장 기본적으로 동일한 스레드 또는 프로세스에없는 두 가지 작업이 특정 순서로 수행되며이를 수행하기위한 조치를 취하지 않는다고 가정하는 것입니다. 이것은 일반적으로 두 스레드가 클래스의 멤버 변수를 설정하고 확인하여 메시지를 전달할 때 발생합니다. 하나의 스레드가 작업을 완료하기 위해 다른 스레드에 시간을주기 위해 휴면을 호출 할 때는 거의 항상 경쟁 조건이 있습니다 (일부 점검 메커니즘을 사용하여 휴면 상태가 반복되지 않는 한).
경쟁 조건을 방지하기위한 도구는 언어 및 OS에 따라 다르지만 일부 comon 도구는 뮤텍스, 중요 섹션 및 신호입니다. 뮤텍스는 자신이 무언가를하는 유일한 사람인지 확인하고 싶을 때 좋습니다. 다른 사람이 무언가를 마쳤 으면 신호가 좋습니다. 공유 리소스를 최소화하면 예기치 않은 동작을 방지 할 수 있습니다
경쟁 조건을 감지하는 것은 어려울 수 있지만 몇 가지 징후가 있습니다. 수면에 크게 의존하는 코드는 경쟁 조건에 취약하기 때문에 먼저 영향을받는 코드에서 대기중인 통화를 확인하십시오. 특히 긴 대기 시간을 추가하면 특정 순서의 이벤트를 시도하고 강제로 디버깅하는 데 사용할 수 있습니다. 이는 동작을 재현하고, 타이밍을 변경하여 동작을 사라지게 할 수 있는지 확인하고, 솔루션을 테스트하는 데 유용합니다. 디버깅 후에는 절전 모드를 제거해야합니다.
경쟁 조건이있는 서명 부호는 일부 시스템에서 간헐적으로 만 발생하는 문제가있는 경우입니다. 일반적인 버그는 충돌 및 교착 상태입니다. 로깅을 사용하면 영향을받는 영역을 찾아서 다시 작업 할 수 있어야합니다.
Microsoft는 실제로이 경쟁 조건 및 교착 상태에 대한 자세한 기사 를 게시했습니다 . 그것에서 가장 요약 된 초록은 제목 단락입니다.
경쟁 조건은 두 스레드가 동시에 공유 변수에 액세스 할 때 발생합니다. 첫 번째 스레드는 변수를 읽고 두 번째 스레드는 변수에서 동일한 값을 읽습니다. 그런 다음 첫 번째 스레드와 두 번째 스레드는 값에 대한 작업을 수행하고 공유 변수에 마지막으로 값을 쓸 수있는 스레드를 찾기 위해 경쟁합니다. 스레드가 이전 스레드가 쓴 값을 덮어 쓰므로 마지막으로 값을 쓰는 스레드의 값이 유지됩니다.
경쟁 조건은 무엇입니까?
프로세스가 다른 이벤트의 순서 나 타이밍에 결정적으로 의존하는 상황.
예를 들어, 프로세서 A와 프로세서 B 모두 실행에 동일한 리소스가 필요 합니다.
당신은 그들을 어떻게 감지합니까?
경쟁 조건을 자동으로 감지하는 도구가 있습니다.
어떻게 처리합니까?
경쟁 조건은 Mutex 또는 Semaphores에 의해 처리 될 수 있습니다 . 잠금 역할을하여 프로세스가 경쟁 조건을 방지하기 위해 특정 요구 사항에 따라 리소스를 획득 할 수 있도록합니다.
그것들이 발생하는 것을 어떻게 방지합니까?
치명적 섹션 회피 와 같은 경쟁 조건을 방지하는 다양한 방법이 있습니다 .
경쟁 조건은 장치 또는 시스템이 동시에 둘 이상의 작업을 수행하려고 할 때 발생하는 바람직하지 않은 상황이지만 장치 또는 시스템의 특성으로 인해 작업을 수행하려면 적절한 순서로 수행해야합니다. 올바르게 완료되었습니다.
컴퓨터 메모리 또는 스토리지에서 대량의 데이터를 읽고 쓰는 명령이 거의 같은 순간에 수신되고 머신이 이전 데이터가 여전히있는 동안 이전 데이터의 일부 또는 전부를 덮어 쓰려고하면 경쟁 조건이 발생할 수 있습니다. 읽다. 결과는 컴퓨터 충돌, "잘못된 작업", 프로그램 알림 및 종료, 이전 데이터 읽기 오류 또는 새 데이터 쓰기 오류 중 하나 이상일 수 있습니다.
다음은 초보자가 Java의 스레드를 쉽게 이해하여 경쟁 조건을 쉽게 이해하는 데 도움이되는 고전적인 은행 계좌 잔액 예제입니다.
public class BankAccount {
/**
* @param args
*/
int accountNumber;
double accountBalance;
public synchronized boolean Deposit(double amount){
double newAccountBalance=0;
if(amount<=0){
return false;
}
else {
newAccountBalance = accountBalance+amount;
accountBalance=newAccountBalance;
return true;
}
}
public synchronized boolean Withdraw(double amount){
double newAccountBalance=0;
if(amount>accountBalance){
return false;
}
else{
newAccountBalance = accountBalance-amount;
accountBalance=newAccountBalance;
return true;
}
}
public static void main(String[] args) {
// TODO Auto-generated method stub
BankAccount b = new BankAccount();
b.accountBalance=2000;
System.out.println(b.Withdraw(3000));
}
"원자"클래스를 사용하면 경쟁 조건을 방지 할 수 있습니다 . 그 이유는 스레드가 get 및 set 작업을 분리하지 않기 때문입니다.
AtomicInteger ai = new AtomicInteger(2);
ai.getAndAdd(5);
결과적으로 링크 "ai"에 7이 있습니다. 두 가지 작업을 수행했지만 두 작업 모두 동일한 스레드를 확인하고 다른 스레드가이를 방해하지 않으므로 경쟁 조건이 없습니다!
경쟁 조건에 대한 이해를 돕기 위해이 기본 예제를 사용해보십시오.
public class ThreadRaceCondition {
/**
* @param args
* @throws InterruptedException
*/
public static void main(String[] args) throws InterruptedException {
Account myAccount = new Account(22222222);
// Expected deposit: 250
for (int i = 0; i < 50; i++) {
Transaction t = new Transaction(myAccount,
Transaction.TransactionType.DEPOSIT, 5.00);
t.start();
}
// Expected withdrawal: 50
for (int i = 0; i < 50; i++) {
Transaction t = new Transaction(myAccount,
Transaction.TransactionType.WITHDRAW, 1.00);
t.start();
}
// Temporary sleep to ensure all threads are completed. Don't use in
// realworld :-)
Thread.sleep(1000);
// Expected account balance is 200
System.out.println("Final Account Balance: "
+ myAccount.getAccountBalance());
}
}
class Transaction extends Thread {
public static enum TransactionType {
DEPOSIT(1), WITHDRAW(2);
private int value;
private TransactionType(int value) {
this.value = value;
}
public int getValue() {
return value;
}
};
private TransactionType transactionType;
private Account account;
private double amount;
/*
* If transactionType == 1, deposit else if transactionType == 2 withdraw
*/
public Transaction(Account account, TransactionType transactionType,
double amount) {
this.transactionType = transactionType;
this.account = account;
this.amount = amount;
}
public void run() {
switch (this.transactionType) {
case DEPOSIT:
deposit();
printBalance();
break;
case WITHDRAW:
withdraw();
printBalance();
break;
default:
System.out.println("NOT A VALID TRANSACTION");
}
;
}
public void deposit() {
this.account.deposit(this.amount);
}
public void withdraw() {
this.account.withdraw(amount);
}
public void printBalance() {
System.out.println(Thread.currentThread().getName()
+ " : TransactionType: " + this.transactionType + ", Amount: "
+ this.amount);
System.out.println("Account Balance: "
+ this.account.getAccountBalance());
}
}
class Account {
private int accountNumber;
private double accountBalance;
public int getAccountNumber() {
return accountNumber;
}
public double getAccountBalance() {
return accountBalance;
}
public Account(int accountNumber) {
this.accountNumber = accountNumber;
}
// If this method is not synchronized, you will see race condition on
// Remove syncronized keyword to see race condition
public synchronized boolean deposit(double amount) {
if (amount < 0) {
return false;
} else {
accountBalance = accountBalance + amount;
return true;
}
}
// If this method is not synchronized, you will see race condition on
// Remove syncronized keyword to see race condition
public synchronized boolean withdraw(double amount) {
if (amount > accountBalance) {
return false;
} else {
accountBalance = accountBalance - amount;
return true;
}
}
}
경쟁 조건을 항상 버리고 싶지는 않습니다. 여러 스레드에서 읽고 쓸 수있는 플래그가 있고이 플래그가 한 스레드에 의해 '완료'로 설정되어 플래그가 '완료'로 설정된 경우 다른 스레드가 처리를 중지하도록하려면 "레이스"를 원하지 않습니다. 조건 "을 제거해야합니다. 실제로 이것은 양성 경쟁 조건이라고 할 수 있습니다.
그러나 경쟁 조건 감지 도구를 사용하면 유해한 경쟁 조건으로 간주됩니다.
경쟁 조건에 대한 자세한 내용은 여기 ( http://msdn.microsoft.com/en-us/magazine/cc546569.aspx)를 참조하십시오 .
카운트가 증가하자마자 카운트를 표시해야하는 작업을 고려하십시오. 즉, CounterThread가 증가하는 즉시 DisplayThread 는 최근 업데이트 된 값을 표시해야합니다.
int i = 0;
산출
CounterThread -> i = 1
DisplayThread -> i = 1
CounterThread -> i = 2
CounterThread -> i = 3
CounterThread -> i = 4
DisplayThread -> i = 4
여기서 CounterThread 는 잠금을 자주 받고 DisplayThread 가 표시 하기 전에 값을 업데이트 합니다. 경쟁 조건이 있습니다. 동기화를 사용하여 경쟁 조건을 해결할 수 있습니다