보편적 인 구성을보다 효율적으로 만들려면 어떻게해야합니까?


16

"유니버설 구성"은 선형화 할 수있는 순차 오브젝트의 랩퍼 클래스입니다 (동시 오브젝트의 강력한 일관성 조건). 예를 들어, 다음은 [1]의 자바에서 조정 된 대기없는 구성입니다.이 인터페이스는 인터페이스 WFQ(스레드 간 일회성 합의 만 필요) 를 만족하는 대기없는 큐가 존재하고 인터페이스 를 가정합니다 Sequential.

public interface WFQ<T> // "FIFO" iteration
{
    int enqueue(T t); // returns the sequence number of t
    Iterable<T> iterateUntil(int max); // iterates until sequence max
}
public interface Sequential
{
    // Apply an invocation (method + arguments)
    // and get a response (return value + state)
    Response apply(Invocation i); 
}
public interface Factory<T> { T generate(); } // generate new default object
public interface Universal extends Sequential {}

public class SlowUniversal implements Universal
{
    Factory<? extends Sequential> generator;
    WFQ<Invocation> wfq = new WFQ<Invocation>();
    Universal(Factory<? extends Sequential> g) { generator = g; } 
    public Response apply(Invocation i)
    {
        int max = wfq.enqueue(i);
        Sequential s = generator.generate();
        for(Invocation invoc : wfq.iterateUntil(max))
            s.apply(invoc);
        return s.apply(i);
    }
}

이 구현은 실제로 느리기 때문에 만족스럽지 않습니다 (모든 호출을 기억하고 매번 적용 할 때마다 재생해야합니다-히스토리 크기에 선형 런타임이 있습니다). 새로운 호출을 적용 할 때 몇 단계를 줄일 수 있도록 WFQand Sequential인터페이스를 (합리적인 방법으로) 확장 할 수있는 방법이 있습니까?

우리는 대기없는 속성을 잃지 않고 이것을 더 효율적으로 만들 수 있습니까 (히스토리 크기에서 선형 런타임이 아니라 메모리 사용도 줄어 듭니다)?

설명

"유니버설 구조"는 Sequential인터페이스에 의해 일반화되는 스레드 안전하지 않은 스레드 호환 객체를 허용하는 [1]에 의해 만들어진 용어 입니다. 대기없는 대기열을 사용하여 첫 번째 구성은 스레드가없는 선형화 가능한 객체 버전을 제공하여 대기하지 않는 개체를 결정합니다 (이는 결정 성 및 중지 apply작업을 가정 함 ).

이 방법은 각 로컬 스레드가 클린 슬레이트에서 시작하여 기록 된 모든 작업을 효과적으로 적용하기 때문에 비효율적 입니다. 어쨌든 WFQ이것은 모든 작업이 적용되는 순서를 결정하기 위해를 사용하여 효과적으로 동기화를 달성하기 때문에 작동합니다. 모든 스레드 호출 applySequential동일한 시퀀스가 Invocation적용된 동일한 로컬 객체를 볼 수 있습니다.

제 질문은 "시작 상태"를 업데이트하는 백그라운드 정리 프로세스를 도입 할 수 있는지 여부입니다 (예 : 처음부터 다시 시작할 필요가 없습니다). 시작 포인터가있는 원자 포인터를 갖는 것만 큼 간단하지 않습니다. 이러한 접근 방식은 대기없는 보증을 쉽게 잃습니다. 내 생각에 다른 대기열 기반 접근 방식이 여기에서 작동 할 수 있습니다.

특수 용어:

  1. 대기 없음-스레드 수 또는 스케줄러의 의사 결정에 관계없이 apply해당 스레드에 대해 실행 가능한 제한된 수의 명령으로 종료됩니다.
  2. lock-free-위와 동일하지만 apply다른 스레드에서 무한한 수의 작업이 수행되는 경우에만 무한한 실행 시간을 허용 합니다. 일반적으로 낙관적 동기화 체계가이 범주에 속합니다.
  3. 차단-스케줄러의 자비로 효율성.

요청 된 실제 예제 (현재 만료되지 않는 페이지)

[1] Herlihy와 Shavit, 멀티 프로세서 프로그래밍 기술 .


질문 1은 "작동"의 의미를 알고있는 경우에만 대답 할 수 있습니다.
Robert Harvey

@RobertHarvey 나는 그것을 수정했다. "작동"하는 데 필요한 것은 래퍼가 대기하지 않고 모든 작업 CopyableSequential이 유효한 선형화가되어야한다는 사실에서 나온 것이다 Sequential.
VF1

이 질문에는 의미있는 단어가 많이 있지만, 당신이 성취하려고하는 것을 정확하게 이해하기 위해 그것들을 모으는 데 어려움을 겪고 있습니다. 해결하려는 문제에 대한 설명을 제공하고 전문 용어를 약간 얇게 할 수 있습니까?
JimmyJames

@JimmyJames 나는 질문 내에서 "확장 된 의견"으로 설명했다. 정리할 다른 전문 용어가 있으면 알려주세요.
VF1

주석의 첫 번째 단락에서 "스레드 안전하지 않지만 스레드 호환 객체"및 "선형화 가능한 객체 버전"이라고 말합니다. 스레드 안전선형화 는 실행 가능한 명령에만 관련되어 있지만 데이터 인 객체를 설명하는 데 사용하기 때문에 의미하는 바가 확실하지 않습니다 . 나는 것을 가정 호출 (정의되지 않음) 효과적으로하는 방법 포인터이며 스레드 안전하지 않습니다 그 방법입니다. 스레드 호환 이 무엇을 의미 하는지 모르겠습니다 .
JimmyJames

답변:


1

다음은이 작업을 수행하는 방법에 대한 설명과 예입니다. 명확하지 않은 부분이 있으면 알려주십시오.

소스와 요점

만능인

초기화 :

스레드 인덱스는 원자 단위로 적용됩니다. 이것은 AtomicIntegernamed을 사용하여 관리됩니다 nextIndex. 이 인덱스는 ThreadLocal다음 인덱스를 가져 와서 nextIndex증가 시켜 자체적으로 초기화 되는 인스턴스를 통해 스레드에 할당됩니다 . 이것은 각 스레드의 인덱스를 처음 검색 할 때 발생합니다. ThreadLocal이 스레드가 생성 한 마지막 시퀀스를 추적하기 위해 A 가 생성됩니다. 0으로 초기화됩니다. 순차 팩토리 오브젝트 참조가 전달되어 저장됩니다. AtomicReferenceArraysize의 두 인스턴스가 작성 n됩니다. 꼬리 객체는 각 참조에 할당되며 Sequential공장에서 제공 한 초기 상태로 초기화됩니다 . n허용되는 최대 스레드 수입니다. 이 배열의 각 요소는 해당 스레드 인덱스에 속합니다.

방법을 적용하십시오 :

이것이 흥미로운 작업을 수행하는 방법입니다. 다음을 수행합니다.

  • 이 호출에 대한 새 노드를 작성하십시오. mine
  • 현재 스레드의 인덱스에있는 Announce 배열에서이 새 노드를 설정하십시오.

그런 다음 시퀀싱 루프가 시작됩니다. 현재 호출이 순서화 될 때까지 계속됩니다.

  1. 이 스레드에 의해 작성된 마지막 노드의 순서를 사용하여 announce 배열에서 노드를 찾으십시오. 이것에 대해서는 나중에 더 설명하겠습니다.
  2. 2 단계에서 노드가 발견되면 아직 시퀀싱되지 않았으며 계속 진행하십시오. 그렇지 않으면 현재 호출에 집중하십시오. 이것은 호출 당 하나의 다른 노드 만 도와 주려고합니다.
  3. 3 단계에서 선택한 노드가 무엇이든 마지막 시퀀싱 된 노드 이후에 계속 시퀀싱하려고 시도합니다 (다른 스레드가 간섭 할 수 있음). 성공 여부에 관계없이 현재 스레드 헤드 참조가 반환 한 시퀀스를 설정합니다. decideNext()

위에서 설명한 중첩 루프의 핵심은 decideNext()방법입니다. 이를 이해하려면 Node 클래스를 살펴 봐야합니다.

노드 클래스

이 클래스는 이중 연결 목록에서 노드를 지정합니다. 이 수업에는 별다른 조치가 없습니다. 대부분의 방법은 설명이 간단한 간단한 검색 방법입니다.

꼬리 방법

이것은 순서가 0 인 특수 노드 인스턴스를 리턴합니다. 호출이이를 대체 할 때까지 플레이스 홀더 역할을합니다.

특성 및 초기화

  • seq: 시퀀스 번호, -1로 초기화 됨 (시퀀싱되지 않음)
  • invocation:의 호출 값 apply(). 시공을 시작합니다.
  • next: AtomicReference정방향 링크 용. 일단 할당되면 변경되지 않습니다
  • previous: AtomicReference시퀀싱시 할당되고 다음에 의해 지워진 역방향 링크truncate()

다음 결정

이 방법은 사소한 논리를 가진 노드에서만 사용됩니다. 간단히 말해서, 노드는 링크 된 목록에서 다음 노드가 될 후보로 제공됩니다. 이 compareAndSet()메소드는 참조가 널인지 확인하고, 참조 인 경우 참조를 후보로 설정합니다. 참조가 이미 설정되어 있으면 아무 것도 수행하지 않습니다. 이 작업은 원자 적이므로 두 명의 후보자가 동시에 제공되면 하나만 선택됩니다. 이렇게하면 하나의 노드 만 다음 노드로 선택됩니다. 후보 노드를 선택하면 순서가 다음 값으로 설정되고 이전 링크가이 노드로 설정됩니다.

범용 클래스 적용 메소드로 돌아 가기 ...

배열 decideNext()에서 노드 또는 노드로 마지막 시퀀싱 된 노드 (확인 된 경우) 를 호출 한 경우 announce두 가지 가능성이 있습니다. 1. 노드가 성공적으로 시퀀싱되었습니다. 2. 다른 스레드가이 스레드를 선점했습니다.

다음 단계는이 호출을 위해 노드가 작성되었는지 확인하는 것입니다. 이 스레드가 성공적으로 시퀀싱했거나 일부 다른 스레드가 announce어레이 에서 스레드를 선택하여 시퀀싱했기 때문에 발생할 수 있습니다. 시퀀싱되지 않은 경우 프로세스가 반복됩니다. 그렇지 않으면이 스레드의 인덱스에서 announce 배열을 지우고 호출 결과 값을 반환하여 호출이 완료됩니다. Announce 배열은 노드가 가비지 수집되지 않도록하는 노드에 대한 참조가 없음을 보장하기 위해 지워 지므로 연결된 목록의 모든 노드가 힙의 활성 지점에서 유지됩니다.

방법 평가

호출 노드가 성공적으로 순서화되었으므로 호출을 평가해야합니다. 이를 수행하기위한 첫 번째 단계는이 단계 이전의 호출이 평가되었는지 확인하는 것입니다. 그들이하지 않은 경우이 스레드는 기다리지 않지만 즉시 작동합니다.

VerifyPrior 방법

ensurePrior()방법은 연결된 목록에서 이전 노드를 확인하여이 작업을 수행합니다. 상태가 설정되지 않으면 이전 노드가 평가됩니다. 이것이 재귀적인 노드입니다. 이전 노드 이전의 노드가 평가되지 않은 경우 해당 노드에 대한 평가 등을 호출합니다.

이전 노드에 상태가있는 것으로 알려져 있으므로이 노드를 평가할 수 있습니다. 마지막 노드가 검색되어 로컬 변수에 할당됩니다. 이 참조가 null의 경우, 다른 thread가이 thread를 선취 해, 이미이 노드를 평가 한 것을 나타냅니다. 상태를 설정합니다. 그렇지 않으면 이전 노드의 상태 Sequential가이 노드의 호출과 함께 객체의 apply 메소드로 전달 됩니다. 리턴 된 상태는 노드에서 설정되고 truncate()메소드가 호출되어 더 이상 필요하지 않으므로 노드에서 역방향 링크를 지 웁니다.

MoveForward 방법

앞으로 이동 방법은 모든 헤드 참조가 아직 더 이상 무언가를 가리 키지 않으면이 노드로 이동하려고 시도합니다. 이것은 쓰레드가 호출을 멈 추면 더 이상 필요하지 않은 노드에 대한 참조를 유지하지 않도록하기위한 것입니다. 이 compareAndSet()메소드는 다른 스레드가 검색된 후 노드를 변경하지 않은 경우에만 노드를 업데이트하도록합니다.

배열 및 도움 발표

단순히 잠금없는 방식과 달리이 방법을 대기없이 설정하는 핵심은 스레드 스케줄러가 필요할 때 각 스레드에 우선 순위를 부여한다고 가정 할 수 없다는 것입니다. 각 스레드가 단순히 자체 노드를 시퀀싱하려고 시도한 경우로드시 스레드가 계속 선점 될 수 있습니다. 이러한 가능성을 설명하기 위해 각 스레드는 먼저 시퀀싱 할 수없는 다른 스레드를 '도움'하려고합니다.

기본 아이디어는 각 스레드가 성공적으로 노드를 만들면 할당 된 시퀀스가 ​​단조 증가한다는 것입니다. 스레드가 계속해서 다른 스레드를 선점하면 announce배열 에서 순서가 지정되지 않은 노드를 찾는 데 사용되는 인덱스가 앞으로 이동합니다. 현재 특정 노드를 시퀀싱하려고하는 모든 스레드가 다른 스레드에 의해 지속적으로 선점 되더라도 결국 모든 스레드가 해당 노드를 시퀀싱하려고합니다. 설명하기 위해 3 개의 스레드로 예제를 구성합니다.

시작점에서 세 개의 스레드 헤드 및 알림 요소가 모두 tail노드를 가리 킵니다 . lastSequence각 쓰레드는 0입니다.

이 시점에서 스레드 1 이 호출로 실행됩니다. Announce 배열에서 현재 색인을 생성 할 예정인 노드 인 마지막 시퀀스 (0)를 확인합니다. 노드를 시퀀싱하고 lastSequence1로 설정합니다.

스레드 2 는 이제 호출로 실행되며 마지막 배열 (제로)에서 Announce 배열을 확인하고 도움이 필요하지 않은지 확인하여 호출 순서를 지정합니다. 성공하면 이제 lastSequence2로 설정됩니다.

스레드 3 이 이제 실행되고 at 노드 announce[0]가 이미 시퀀싱되고 자체 호출 시퀀싱되는 것을 볼 수 있습니다. 그것은 것 lastSequence해주기로 설정됩니다.

이제 스레드 1 이 다시 호출됩니다. 인덱스 1에서 Announce 배열을 확인하고 이미 시퀀싱 된 것을 찾습니다. 동시에 스레드 2 가 호출됩니다. 인덱스 2에서 Announce 배열을 확인하고 이미 시퀀싱 된 것을 찾습니다. 두 스레드 1스레드 2는 이제 자신의 노드 염기 서열을 시도합니다. 스레드 2가 이기고 호출 순서입니다. 그것은 것 lastSequence4. 한편으로 설정, 스레드 세 가지가 호출되었습니다. 인덱스를 점검하고 lastSequence(mod 3) at 노드 announce[0]가 시퀀싱되지 않았 음을 발견합니다. 스레드 1 이 두 번째 시도 와 동시에 스레드 2 가 다시 호출됩니다 . 실 1Thread 2에announce[1] 의해 방금 생성 된 노드 인 순서없는 호출을 찾습니다 . 스레드 2의 호출 을 시퀀싱하려고 시도 하고 성공합니다. 스레드 2 는 자신의 노드를 찾고 시퀀싱되었습니다. 5로 설정 합니다. 그런 다음 스레드 3 이 호출되고 스레드 1이있는 노드 가 여전히 시퀀싱되지 않았으며이를 시도합니다. 한편 스레드 2 도 호출되어 스레드 3을 선점합니다. 노드 2를 시퀀싱하고 6으로 설정합니다 .announce[1]lastSequenceannounce[0]lastSequence

불쌍한 실 1 . 스레드 3 이이를 시퀀싱하려고 하더라도 스케줄러에 의해 두 스레드가 지속적으로 차단되었습니다. 그러나이 시점에서. 스레드 2 도 이제 announce[0](6 mod 3)을 가리 킵니다 . 세 개의 스레드 모두 동일한 호출을 시퀀싱하도록 설정되어 있습니다. 어떤 스레드가 성공하든 상관없이 시퀀싱 될 다음 노드는 스레드 1 의 대기 호출, 즉에 의해 참조되는 노드 announce[0]입니다.

불가피합니다. 스레드를 미리 비우려면 다른 스레드가 시퀀싱 노드 여야하며 그렇게하면 계속해서 lastSequence앞으로 나아갑니다. 주어진 스레드의 노드가 연속적으로 시퀀싱되지 않으면 결국 모든 스레드는 Announce 배열의 인덱스를 가리 킵니다. 도움을 줄 노드가 시퀀싱 될 때까지 어떤 스레드도 다른 작업을 수행하지 않습니다. 최악의 시나리오는 모든 스레드가 동일한 순서가없는 노드를 가리키는 것입니다. 따라서 호출 순서를 정하는 데 필요한 시간은 입력 크기가 아닌 스레드 수의 함수입니다.


코드 발췌 부분 중 일부를 pastebin에 넣어 주시겠습니까? lockfree 링크 목록과 같은 많은 것들을 간단하게 언급 할 수 있습니까? 세부 사항이 너무 많으면 답변을 전체적으로 이해하기가 약간 어렵습니다. 어쨌든, 이것은 유망 해 보입니다. 확실히 제공하는 것을 파고 싶습니다.
VF1 2016 년

이것은 확실히 유효한 잠금없는 구현처럼 보이지만 걱정되는 근본적인 문제가 빠져 있습니다. 원 자성의 요구 사항은 "올바른 역사"연결된리스트의 구현의 경우, 필요하는 존재가 될 필요로 previous하고 next포인터가 유효합니다. 기다림없이 유효한 기록을 유지하고 생성하는 것은 어려운 것 같습니다.
VF1

@ VF1 어떤 문제가 해결되지 않았는지 모르겠습니다. 나머지 의견에서 언급 한 모든 것은 내가 말할 수있는 것에서 내가 준 예에서 다루어졌습니다.
JimmyJames

당신은 대기없는 재산을 포기 했습니다.
VF1

@ VF1 어떻게 생각하십니까?
JimmyJames

0

내 이전 답변이 실제로 질문에 올바르게 대답하지는 않지만 OP가 유용하다고 생각하면 그대로 둡니다. 질문에있는 링크의 코드를 기반으로 내 시도는 다음과 같습니다. 나는 이것에 대해 실제로 기본적인 테스트 만했지만 평균을 올바르게 계산하는 것처럼 보입니다. 대기 시간이 적절하지 않은지에 대한 피드백을 환영합니다.

참고 : 범용 인터페이스를 제거하고 클래스로 만들었습니다. Universal을 Sequentials로 구성하고 하나는 불필요한 합병증처럼 보이지만 뭔가 빠진 것일 수 있습니다. 평균 클래스에서 상태 변수를로 표시했습니다 volatile. 코드가 작동하는 데 필요하지 않습니다. 보수적이며 (스레딩을 사용하는 것이 좋습니다) 각 스레드가 모든 계산을 수행하지 못하게합니다 (한 번).

순차 및 공장

public interface Sequential<E, S, R>
{ 
  R apply(S priorState);

  S state();

  default boolean isApplied()
  {
    return state() != null;
  }
}

public interface Factory<E, S, R>
{
   S initial();

   Sequential<E, S, R> generate(E input);
}

만능인

import java.util.concurrent.ConcurrentLinkedQueue;

public class Universal<I, S, R> 
{
  private final Factory<I, S, R> generator;
  private final ConcurrentLinkedQueue<Sequential<I, S, R>> wfq = new ConcurrentLinkedQueue<>();
  private final ThreadLocal<Sequential<I, S, R>> last = new ThreadLocal<>();

  public Universal(Factory<I, S, R> g)
  { 
    generator = g;
  }

  public R apply(I invocation)
  {
    Sequential<I, S, R> newSequential = generator.generate(invocation);
    wfq.add(newSequential);

    Sequential<I, S, R> last = null;
    S prior = generator.initial(); 

    for (Sequential<I, S, R> i : wfq) {
      if (!i.isApplied() || newSequential == i) {
        R r = i.apply(prior);

        if (i == newSequential) {
          wfq.remove(last.get());
          last.set(newSequential);

          return r;
        }
      }

      prior = i.state();
    }

    throw new IllegalStateException("Houston, we have a problem");
  }
}

평균

public class Average implements Sequential<Integer, Average.State, Double>
{
  private final Integer invocation;
  private volatile State state;

  private Average(Integer invocation)
  {
    this.invocation = invocation;
  }

  @Override
  public Double apply(State prior)
  {
    System.out.println(Thread.currentThread() + " " + invocation + " prior " + prior);

    state = prior.add(invocation);

    return ((double) state.sum)/ state.count;
  }

  @Override
  public State state()
  {
    return state;
  }

  public static class AverageFactory implements Factory<Integer, State, Double> 
  {
    @Override
    public State initial()
    {
      return new State(0, 0);
    }

    @Override
    public Average generate(Integer i)
    {
      return new Average(i);
    }
  }

  public static class State
  {
    private final int sum;
    private final int count;

    private State(int sum, int count)
    {
      this.sum = sum;
      this.count = count;
    }

    State add(int value)
    {
      return new State(sum + value, count + 1);
    }

    @Override
    public String toString()
    {
      return sum + " / " + count;
    }
  }
}

데모 코드

private static final int THREADS = 10;
private static final int SIZE = 50;

public static void main(String... args)
{
  Average.AverageFactory factory = new Average.AverageFactory();

  Universal<Integer, Average.State, Double> universal = new Universal<>(factory);

  for (int i = 0; i < THREADS; i++)
  {
    new Thread(new Test(i * SIZE, universal)).start();
  }
}

static class Test implements Runnable
{
  final int start;
  final Universal<Integer, Average.State, Double> universal;

  Test(int start, Universal<Integer, Average.State, Double> universal)
  {
    this.start = start;
    this.universal = universal;
  }

  @Override
  public void run()
  {
    for (int i = start; i < start + SIZE; i++)
    {
      System.out.println(Thread.currentThread() + " " + i);

      System.out.println(System.nanoTime() + " " + Thread.currentThread() + " " + i + " result " + universal.apply(i));
    }
  }
}

여기에 게시 할 때 코드를 약간 수정했습니다. 문제는 없지만 문제가 있으면 알려주십시오.


당신은 나를 위해 다른 대답을 유지할 필요가 없습니다 (나는 이전에 내 질문을 업데이트하여 관련 결론을 이끌어 냈습니다). 불행히도,이 답변은 실제로 어떤 메모리도 확보하지 않기 때문에 질문에 대답하지 않으므로 wfq여전히 전체 기록을 통과해야합니다. 상수 요인을 제외하고 런타임이 개선되지 않았습니다.
VF1

@ Vf1 전체 목록을 순회하여 계산되었는지 여부를 확인하는 데 걸리는 시간은 각 계산을 수행하는 것과 비교하여 미미합니다. 이전 상태는 필요하지 않기 때문에 초기 상태를 제거 할 수 있어야합니다. 테스트가 어렵고 맞춤 컬렉션을 사용해야 할 수도 있지만 약간의 변경 사항을 추가했습니다.
JimmyJames

@ VF1 기본 커서 테스트에서 작동하는 것으로 구현으로 업데이트되었습니다. 나는 그것이 안전하다고 확신하지는 않지만 내 머리 꼭대기에서 떨어져 있습니다. 유니버셜이 작업하는 스레드를 알고 있다면 모든 스레드가 안전하게 지나간 후에 각 스레드를 추적하고 요소를 제거 할 수 있습니다.
JimmyJames

@ VF1 ConcurrentLinkedQueue의 코드를 살펴보면 offer 메소드에는 다른 응답을 비대기없는 것으로 만든 것과 유사한 루프가 있습니다. 주석을 찾아 "다른 스레드에 분실 CAS 경주, 다음 재 - 읽기"
JimmyJames

"초기 상태를 제거 할 수 있어야합니다"-정확히. 그것은 해야 하지만, 쉬운 미묘 대기 자유를 잃게 코드를 소개합니다. 스레드 추적 구성표가 작동 할 수 있습니다. 마지막으로 CLQ 소스에 액세스 할 수 없습니다. 연결 하시겠습니까?
VF1
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.