재진입 기능은 정확히 무엇입니까?


198

대부분 시간은 , 재입국의 정의에서 인용 위키 백과 :

컴퓨터 프로그램 또는 루틴은 이전 호출이 완료되기 전에 다시 안전하게 호출 될 수있는 경우 (즉, 동시에 안전하게 실행될 수있는 경우) 재진입 자로 설명됩니다 . 재진입, 컴퓨터 프로그램 또는 루틴 :

  1. 정적 (또는 전역) 비 일관 데이터를 보유하지 않아야합니다.
  2. 고정 된 (또는 전역적인) 일정하지 않은 데이터로 주소를 반환해서는 안됩니다.
  3. 발신자가 제공 한 데이터에 대해서만 작업해야합니다.
  4. 싱글 톤 리소스에 대한 잠금에 의존해서는 안됩니다.
  5. 고유 한 스레드 저장소에서 실행하지 않는 한 자체 코드를 수정해서는 안됩니다.
  6. 재진입 할 ​​수없는 컴퓨터 프로그램이나 루틴을 호출해서는 안됩니다.

안전하게 정의되는 방법은 무엇입니까?

프로그램을 동시에 안전하게 실행할 수 있다면 항상 재진입한다는 의미입니까?

재진입 기능에 대한 코드를 확인하면서 명심해야 할 6 가지 요점 사이의 공통 스레드는 정확히 무엇입니까?

또한,

  1. 모든 재귀 함수는 재진입합니까?
  2. 모든 스레드 안전 기능이 재진입합니까?
  3. 재귀 및 스레드 안전 기능이 모두 재진입합니까?

이 질문을 쓰는 동안 한 가지 명심해야합니다. 재진입나사산 안전 과 같은 용어는 절대적입니까, 즉 고정 된 구체적인 정의가 있습니까? 그렇지 않은 경우이 질문은 의미가 없습니다.


6
사실, 나는 첫 번째 목록에서 # 2에 동의하지 않습니다. 재진입 기능에서 원하는 주소로 주소를 반환 할 수 있습니다. 제한은 호출 코드에서 해당 주소로 수행하는 작업에 있습니다.

2
@Neil 그러나 재진입 함수의 작성자는 발신자가 확실하게 재진입 할 ​​수 있도록 정적 (또는 전역) 비 일관적인 데이터에 주소를 반환해서는 안되는 것을 확실하게 제어 할 수 없습니까?
Robben_Ford_Fan_boy

2
@drelihan 호출자가 반환 값으로 수행하는 작업을 제어하는 ​​것은 ANY 함수 (재진입 여부)의 작성자의 책임이 아닙니다. 그들은 발신자가 할 수있는 일을 분명히 말해야하지만, 발신자가 다른 일을하기로 선택하면 발신자에게는 운이 좋지 않습니다.

"스레드 안전"은 스레드가 수행하는 작업과 해당 작업의 예상 효과를 지정하지 않으면 의미가 없습니다. 그러나 아마도 그것은 별도의 질문이어야합니다.

나는 행동이 일정에 관계없이 잘 정의되고 결정적이라는 것을 의미합니다.
AturSams

답변:


191

1. 어떻게 안전하게 정의됩니까?

의미 적으로. 이 경우 이것은 명확한 용어가 아닙니다. 그것은 단지 "위험없이 그렇게 할 수 있습니다"를 의미합니다.

2. 프로그램을 동시에 안전하게 실행할 수 있다면 항상 재진입한다는 의미입니까?

아니.

예를 들어, 잠금과 콜백을 매개 변수로 사용하는 C ++ 함수를 만들어 봅시다.

#include <mutex>

typedef void (*callback)();
std::mutex m;

void foo(callback f)
{
    m.lock();
    // use the resource protected by the mutex

    if (f) {
        f();
    }

    // use the resource protected by the mutex
    m.unlock();
}

다른 함수는 동일한 뮤텍스를 잠글 필요가 있습니다.

void bar()
{
    foo(nullptr);
}

첫눈에 모든 것이 정상인 것 같습니다… 그러나 기다리십시오 :

int main()
{
    foo(bar);
    return 0;
}

뮤텍스의 잠금이 재귀 적이 지 않으면 메인 스레드에서 다음과 같은 일이 발생합니다.

  1. main전화 foo합니다.
  2. foo 자물쇠를 얻을 것입니다.
  3. foo호출 bar호출하는 foo.
  4. 두 번째 foo는 잠금을 획득하려고 시도하고 실패하고 잠금이 해제 될 때까지 기다립니다.
  5. 이중 자물쇠.
  6. 죄송합니다…

좋아, 나는 콜백을 사용하여 바람을 피웠다. 그러나 비슷한 효과를 갖는 더 복잡한 코드를 상상하기 쉽습니다.

3. 코드에 재진입 기능이 있는지 확인하면서 명심해야 할 6 가지 요점 사이의 공통 스레드는 무엇입니까?

당신은 할 수 있습니다 냄새 함수가 / 수정 가능한 지속적인 리소스에 대한 액세스를 제공, 또는이 / 함수에 액세스 할 경우 문제가 냄새 .

( 좋아요, 우리 코드의 99 %가 냄새를 맡아야합니다. 그러면 마지막 섹션을 참조하십시오… )

따라서 코드를 연구 할 때 그 중 하나가 경고해야합니다.

  1. 함수에는 상태가 있습니다 (예 : 전역 변수 또는 클래스 멤버 변수에 액세스)
  2. 이 함수는 여러 스레드에 의해 호출되거나 프로세스가 실행되는 동안 스택에 두 번 나타날 수 있습니다 (즉, 함수가 직접 또는 간접적으로 호출 될 수 있음). 매개 변수로 콜백을 취하는 함수는 냄새 가 많이납니다.

비재 통화는 바이러스 성입니다. 재진입 가능하지 않은 함수를 호출 할 수있는 함수는 재진입 자로 간주 될 수 없습니다.

또한 C ++ 메소드 는에 액세스 할 수 있기 때문에 냄새가납니다this . 따라서 코드를 연구하여 재미있는 상호 작용이 없는지 확인해야합니다.

4.1. 모든 재귀 함수는 재진입합니까?

아니.

다중 스레드의 경우 공유 리소스에 액세스하는 재귀 함수가 동시에 여러 스레드에 의해 호출되어 데이터가 잘못 / 손상 될 수 있습니다.

단일 스레드의 경우 재귀 함수는 재 진행이 아닌 함수 (예 : infamous strtok)를 사용하거나 데이터가 이미 사용중인 사실을 처리하지 않고 전역 데이터를 사용할 수 있습니다. 따라서 함수는 직접 또는 간접적으로 호출되기 때문에 재귀 적이지만 여전히 재귀 적 안전하지 않을 수 있습니다.

4.2. 모든 스레드 안전 기능이 재진입합니까?

위의 예에서 분명히 스레드 안전 기능이 재진입되지 않은 방법을 보여주었습니다. 좋아, 나는 콜백 매개 변수 때문에 바람을 피웠다. 그러나 스레드가 비 재귀 잠금을 두 번 획득하여 스레드를 교착 상태로 만드는 방법에는 여러 가지가 있습니다.

4.3. 재귀 및 스레드 안전 기능이 모두 재진입합니까?

"재귀"가 "재귀 안전"을 의미하는 경우 "예"라고 대답합니다.

여러 스레드에서 동시에 함수를 호출 할 수 있고 문제없이 직접 또는 간접적으로 호출 할 수있는 경우 재진입됩니다.

문제가이 보증을 평가하는 중입니다… ^ _ ^

5. 재진입 및 나사산 안전과 같은 용어는 절대적입니까, 즉 고정 된 구체적인 정의가 있습니까?

나는 그렇게 생각하지만 함수를 평가하는 것이 스레드로부터 안전하거나 재진입이 어려울 수 있습니다. 이것이 위에서 냄새 라는 용어를 사용한 이유입니다 . 함수가 재진입되지는 않지만 복잡한 코드가 재진입되는 것을 확인하기는 어려울 수 있습니다

6. 예

리소스를 사용해야하는 한 가지 방법으로 개체가 있다고 가정 해 보겠습니다.

struct MyStruct
{
    P * p;

    void foo()
    {
        if (this->p == nullptr)
        {
            this->p = new P();
        }

        // lots of code, some using this->p

        if (this->p != nullptr)
        {
            delete this->p;
            this->p = nullptr;
        }
    }
};

첫 번째 문제는 어떻게 든이 함수가 재귀 적으로 호출되는 경우 (즉,이 함수가 직접 또는 간접적 this->p으로 호출되는 경우) 마지막 호출이 끝날 때 삭제되고 여전히 종료 전에 사용되기 때문에 코드가 중단 될 수 있다는 것입니다 첫 번째 통화의.

따라서이 코드는 재귀 적으로 안전 하지 않습니다 .

이 문제를 해결하기 위해 참조 카운터를 사용할 수 있습니다.

struct MyStruct
{
    size_t c;
    P * p;

    void foo()
    {
        if (c == 0)
        {
            this->p = new P();
        }

        ++c;
        // lots of code, some using this->p
        --c;

        if (c == 0)
        {
            delete this->p;
            this->p = nullptr;
        }
    }
};

이 방법은 코드는 재귀 안전이됩니다 ...하지만 여전히 문제 때문에 멀티 스레딩의 재진입되지 않습니다 : 우리가해야 확인의 수정 c과의 p사용하여, 원자 수행됩니다 재귀 뮤텍스를 (모든 뮤텍스는 재귀) :

#include <mutex>

struct MyStruct
{
    std::recursive_mutex m;
    size_t c;
    P * p;

    void foo()
    {
        m.lock();

        if (c == 0)
        {
            this->p = new P();
        }

        ++c;
        m.unlock();
        // lots of code, some using this->p
        m.lock();
        --c;

        if (c == 0)
        {
            delete this->p;
            this->p = nullptr;
        }

        m.unlock();
    }
};

그리고 물론 이것은 모두 lots of code의 사용을 포함하여 그 자체가 재진입 이라고 가정합니다 p.

그리고 위의 코드는 원격으로 예외적으로 안전 하지는 않지만 이것은 또 다른 이야기입니다 ... ^ _ ^

7. 우리 코드의 99 %는 재진입이 아닙니다!

스파게티 코드는 매우 사실입니다. 그러나 코드를 올바르게 분할하면 재진입 문제를 피할 수 있습니다.

7.1. 모든 기능이 NO 상태인지 확인하십시오

매개 변수, 자체 로컬 변수, 상태가없는 기타 함수 만 사용해야하며 데이터가 전혀 리턴되지 않으면 데이터 사본을 리턴해야합니다.

7.2. 객체가 "재귀 적 안전"인지 확인하십시오

객체 메소드는에 액세스 할 수 this있으므로 동일한 객체 인스턴스의 모든 메소드와 상태를 공유합니다.

따라서 전체 객체를 손상시키지 않고 스택의 한 지점 (예 : 호출 방법 A)에서 다른 지점 (예 : 호출 방법 B)에서 객체를 사용할 수 있어야합니다. 메소드를 종료 할 때 오브젝트가 안정적이고 올바른지 확인하도록 오브젝트를 설계하십시오 (매달려있는 포인터, 모순되는 멤버 변수 등 없음).

7.3. 모든 물체가 올바르게 캡슐화되어 있는지 확인하십시오

다른 사람은 내부 데이터에 액세스 할 수 없습니다.

    // bad
    int & MyObject::getCounter()
    {
        return this->counter;
    }

    // good
    int MyObject::getCounter()
    {
        return this->counter;
    }

    // good, too
    void MyObject::getCounter(int & p_counter)
    {
        p_counter = this->counter;
    }

const 참조를 반환하는 코드가 const 참조를 알려주지 않고 코드의 다른 부분이 데이터를 수정할 수 있으므로 사용자가 데이터의 주소를 검색하면 const 참조를 반환하는 것조차 위험 할 수 있습니다.

7.4. 객체가 스레드로부터 안전하지 않다는 것을 사용자에게 알리십시오

따라서 사용자는 뮤텍스를 사용하여 스레드간에 공유되는 객체를 사용해야합니다.

STL의 개체는 성능 문제로 인해 스레드로부터 안전하지 않도록 설계되었으므로 사용자 std::string가 두 스레드간에 공유 하려면 동시성 기본 형식으로 액세스를 보호해야합니다.

7.5. 스레드 안전 코드가 재귀 적으로 안전해야합니다

이는 동일한 리소스를 동일한 스레드에서 두 번 사용할 수 있다고 생각되면 재귀 뮤텍스를 사용한다는 의미입니다.


1
약간의 문제를 해결하기 위해 실제로이 경우 "안전성"이 정의되어 있다고 생각합니다. 즉, 함수가 제공된 변수에 대해서만 작동한다는 것을 의미합니다. 그리고 이것은 다른 안전 아이디어를 암시하지 않을 수도 있다는 것입니다.
조 소울 브링 거

첫 번째 예에서 뮤텍스를 지나치지 않았습니까?
detly

@ paercebal : 귀하의 예가 잘못되었습니다. 실제로 콜백을 신경 쓸 필요가 없으며 간단한 재귀에는 하나의 문제가있을 경우 동일한 문제가 발생하지만 유일한 문제는 잠금이 할당 된 위치를 정확히 말하는 것을 잊어 버린 것입니다.
Yttrill

3
@Yttrill : 첫 번째 예에 대해 이야기하고 있다고 가정합니다. 본질적으로 콜백 냄새가 나기 때문에 "콜백"을 사용했습니다. 물론 재귀 함수는 동일한 문제가 있지만 일반적으로 함수와 재귀 특성을 쉽게 분석 할 수 있으므로 재진입인지 재귀에 적합한 지 여부를 감지 할 수 있습니다. 반면에 콜백은 콜백을 호출하는 함수의 작성자가 콜백이 무엇을하고 있는지에 대한 정보가 없기 때문에이 함수가 자신의 함수가 재진입되는지 확인하기 어렵다는 것을 의미합니다. 이것이 내가 보여주고 싶었던 어려움입니다.
paercebal

1
@Gab 是 好人 : 첫 번째 예를 수정했습니다. 감사! 신호 처리기는 일반적으로 신호가 발생하면 특별히 선언 된 전역 변수를 변경하는 것 외에는 아무것도 할 수 없기 때문에 재진입과는 별도로 자체 문제가 발생합니다.
paercebal

21

"안전하게"는 상식이 지시하는대로 정확하게 정의됩니다. "다른 것을 방해하지 않고 올바르게하는 것"을 의미합니다. 당신이 인용 한 여섯 가지 요점은 그것을 달성하기위한 요구 사항을 아주 명확하게 표현합니다.

3 가지 질문에 대한 답은 3 배입니다.


모든 재귀 함수는 재진입합니까?

아니!

예를 들어 동일한 전역 / 정적 데이터에 액세스하는 경우 재귀 함수를 동시에 두 번 호출하면 서로 쉽게 망칠 수 있습니다.


모든 스레드 안전 기능이 재진입합니까?

아니!

함수가 동시에 호출되면 오작동하지 않으면 스레드로부터 안전합니다. 그러나 이것은 예를 들어 첫 번째가 끝날 때까지 두 번째 호출의 실행을 차단하기 위해 뮤텍스를 사용하여 달성 할 수 있으므로 한 번에 하나의 호출 만 작동합니다. 재진입은 다른 호출을 방해하지 않고 동시에 실행하는 것을 의미 합니다 .


재귀 및 스레드 안전 기능이 모두 재진입합니까?

아니!

위 참조.


10

일반적인 실 :

루틴이 중단 된 동안 루틴이 호출되면 동작이 올바르게 정의되어 있습니까?

다음과 같은 기능이 있다면 :

int add( int a , int b ) {
  return a + b;
}

그런 다음 외부 상태에 의존하지 않습니다. 동작이 잘 정의되어 있습니다.

다음과 같은 기능이 있다면 :

int add_to_global( int a ) {
  return gValue += a;
}

결과는 여러 스레드에서 제대로 정의되지 않았습니다. 타이밍이 잘못되면 정보가 손실 될 수 있습니다.

재진입 함수의 가장 간단한 형태는 전달 된 인수와 상수 값에서만 독점적으로 작동하는 것입니다. 다른 처리에는 특별한 처리가 필요하거나 종종 재진입되지 않습니다. 물론 인수는 변경 가능한 전역을 참조해서는 안됩니다.


7

이제 이전 의견에 대해 자세히 설명해야합니다. @paercebal 답변이 잘못되었습니다. 예제 코드에서 매개 변수로 간주 된 뮤텍스가 실제로 전달되지 않았다는 것을 아무도 알지 못했습니다.

나는 결론에 이의를 제기한다. 동시성이 존재할 때 함수가 안전하기 위해서는 재진입해야한다. 따라서 동시 안전 (일반적으로 쓰레드 안전)은 재진입을 의미합니다.

스레드 안전이나 재진입 모두 인수에 대해 할 말이 없습니다. 우리는 함수의 동시 실행에 대해 이야기하고 있습니다. 부적절한 매개 변수가 사용되는 경우 여전히 안전하지 않을 수 있습니다.

예를 들어 memcpy ()는 스레드로부터 안전하고 재진입 가능합니다 (일반적으로). 분명히 두 개의 다른 스레드에서 동일한 대상에 대한 포인터로 호출하면 예상대로 작동하지 않습니다. 이것이 SGI 정의의 요점이며, 동일한 데이터 구조에 대한 액세스가 클라이언트에 의해 동기화되도록 클라이언트에 onus를 배치합니다.

일반적으로 스레드 안전 작동에 매개 변수를 포함시키는 것은 말도 안된다는 것을 이해하는 것이 중요합니다 . 데이터베이스 프로그래밍을 완료했다면 이해할 것입니다. "원자"라는 개념과 뮤텍스 또는 다른 기술로 보호 될 수있는 개념은 반드시 사용자 개념입니다. 데이터베이스에서 트랜잭션을 처리하려면 중단없이 여러 번 수정해야합니다. 누가 클라이언트 프로그래머와 동기화를 유지해야하는지 누가 말할 수 있습니까?

요점은 "손상"이 직렬화되지 않은 쓰기로 컴퓨터의 메모리를 망칠 필요가 없다는 것입니다. 모든 개별 작업이 직렬화되어 있어도 손상이 여전히 발생할 수 있습니다. 함수가 스레드로부터 안전하거나 재진입 할 ​​것인지를 물을 때, 질문은 적절하게 분리 된 모든 인수를 의미합니다. 결합 된 인수를 사용하는 것이 반례가되지는 않습니다.

많은 프로그래밍 시스템이 있습니다. Ocaml은 하나이며, 파이썬도 마찬가지입니다. 많은 비 재진입 코드가 있지만 전역 잠금을 사용하여 스레드 acess를 인터리브합니다. 이러한 시스템은 재진입이 아니며 스레드 안전 또는 동시 안전이 아니며, 전 세계적으로 동시성을 방지하기 때문에 단순히 안전하게 작동합니다.

좋은 예는 malloc입니다. 재진입이 아니며 스레드로부터 안전하지 않습니다. 전역 리소스 (힙)에 액세스해야하기 때문입니다. 자물쇠를 사용해도 안전하지는 않습니다. 확실히 재진입 할 ​​수는 없습니다. malloc에 ​​대한 인터페이스가 올바르게 설계 되었다면 재진입 및 스레드 안전성을 확보 할 수 있습니다.

malloc(heap*, size_t);

이제 단일 힙에 대한 공유 액세스 직렬화에 대한 책임을 클라이언트에게 이전하므로 안전합니다. 별도의 힙 객체가있는 경우 특히 작업이 필요하지 않습니다. 공통 힙이 사용되는 경우 클라이언트는 액세스를 직렬화해야합니다. 함수 에서 잠금을 사용하는 것만으로는 충분하지 않습니다. 힙을 잠그는 malloc을 고려하면 * 신호가 따라오고 같은 포인터에서 malloc을 호출합니다. 중단되었습니다.

일반적으로 잠금은 스레드로부터 안전하지 않습니다. 실제로 클라이언트가 소유 한 자원을 부적절하게 관리하여 안전을 파괴합니다. 잠금은 객체 제조업체가 수행해야합니다.이 객체는 생성되는 객체 수와 사용 방법을 알고있는 유일한 코드입니다.


"따라서 동시 안전 (일반적으로 쓰레드 안전)은 재진입을 의미합니다." 이것은 "쓰레드 안전하지만 재진입 할 ​​수 없음"위키피디아 예제 와 모순 됩니다.
Maggyero

3

나열된 포인트 중 "공통 스레드"(pun 의도 된!?)는 함수가 동일한 함수에 대한 재귀 적 또는 동시 호출의 동작에 영향을 미치는 어떤 것도 수행해서는 안된다는 것입니다.

예를 들어 정적 데이터는 모든 스레드가 소유하기 때문에 문제가됩니다. 한 번의 호출로 정적 변수를 수정하면 모든 스레드가 수정 된 데이터를 사용하여 동작에 영향을줍니다. 스레드가 여러 개 있지만 코드 사본이 하나만 있기 때문에 자체 수정 코드 (드물게 발생하지만 경우에 따라 방지 됨)에 문제가 있습니다. 코드는 필수 정적 데이터이기도합니다.

본질적으로 재진입 할 ​​수 있도록 각 스레드는이 기능을 유일한 사용자 인 것처럼 사용할 수 있어야하며, 한 스레드가 다른 스레드의 동작에 비 결정적 방식으로 영향을 줄 수있는 경우에는 해당되지 않습니다. 주로 여기에는 함수가 작동하는 별도 또는 상수 데이터가있는 각 스레드가 포함됩니다.

모든 말에서, 포인트 (1)이 반드시 사실은 아닙니다. 예를 들어 합법적으로 그리고 설계 상 정적 변수를 사용하여 재귀 횟수를 유지하여 과도한 재귀를 방지하거나 알고리즘을 프로파일 링 할 수 있습니다.

스레드 안전 기능은 재진입 할 ​​필요가 없습니다. 잠금으로 재진입을 구체적으로 방지하여 스레드 안전성을 달성 할 수 있으며 포인트 (6)은 그러한 기능이 재진입되지 않는다고 말합니다. 포인트 (6)과 관련하여 잠금하는 스레드 안전 함수를 호출하는 함수는 재귀에 사용하기에 안전하지 않으므로 (데드락이 될 수 있음) 동시성에 대해 안전하지만 재진입이라고는하지 않습니다. 여러 스레드가 (잠긴 영역이 아닌) 그러한 함수에서 동시에 프로그램 카운터를 가질 수 있다는 점에서 여전히 재진입됩니다. 이것은 스레드 안전성과 재 진행을 구별하는 데 도움이 될 수 있습니다 (또는 혼란을 더할 수 있습니다!).


1

"또한"질문에 대한 대답은 "아니오", "아니오"및 "아니오"입니다. 함수가 재귀 적이거나 스레드로부터 안전하기 때문에 재진입하지 않습니다.

이러한 각 유형의 함수는 인용하는 모든 포인트에서 실패 할 수 있습니다. (5 점은 100 % 확실하지 않습니다).


1

"Thread-safe"및 "re-entrant"라는 용어는 그 정의가 말하는 것만을 의미합니다. 이 문맥에서 "안전"은 아래에 인용 한 정의 의미 합니다 .

여기서 "안전"은 특정 맥락에서 주어진 함수를 호출한다고해서 애플리케이션이 완전히 보호되지는 않는다는 의미에서 더 안전하다는 의미는 아닙니다. 전체적으로 함수는 다중 스레드 응용 프로그램에서 원하는 효과를 안정적으로 생성 할 수 있지만 정의에 따라 재진입 또는 스레드 안전 자격을 갖추지 못할 수 있습니다. 반대로 다중 스레드 응용 프로그램에서 다양한 바람직하지 않은, 예기치 않은 및 / 또는 예측할 수없는 효과를 생성하는 방식으로 재진입 기능을 호출 할 수 있습니다.

재귀 함수는 무엇이든 될 수 있으며 재진입자는 스레드 안전보다 더 강력한 정의를 가지므로 번호가 매겨진 질문에 대한 대답은 모두 아니오입니다.

재진입의 정의를 읽으면, 그것을 수정하기 위해 호출하는 것 이상으로 아무것도 수정하지 않는 함수를 의미하는 것으로 요약 할 수 있습니다. 그러나 요약에만 의존해서는 안됩니다.

멀티 스레드 프로그래밍은 일반적인 경우에 매우 어렵습니다 . 코드 재진입의 어느 부분을 아는 것은이 과제의 일부일뿐입니다. 나사산 안전은 부가적인 것이 아닙니다. 재진입 기능을 함께 모으는 대신 전체 스레드 안전 설계 패턴 을 사용하고이 패턴을 사용 하여 프로그램 의 모든 스레드 및 공유 리소스 사용을 안내하는 것이 좋습니다 .

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.