TN058: MFC 모듈 상태 구현

참고 항목

다음 기술 노트는 온라인 설명서에 먼저 포함되어 있었으므로 업데이트되지 않았습니다. 따라서 일부 절차 및 항목은 만료되거나 올바르지 않을 수 있습니다. 최신 정보를 보려면 온라인 설명서 색인에서 관심 있는 항목을 검색하는 것이 좋습니다.

이 기술 참고에서는 MFC "모듈 상태" 구문의 구현에 대해 설명합니다. 모듈 상태 구현에 대한 이해는 DLL(또는 OLE In Process 서버)에서 MFC 공유 DLL을 사용하는 데 중요합니다.

이 메모를 읽기 전에 새 문서, Windows 및 뷰 만들기에서 "MFC 모듈의 상태 데이터 관리"를 참조하세요. 이 문서에는 이 주제에 대한 중요한 사용 정보 및 개요 정보가 포함되어 있습니다.

개요

MFC 상태 정보에는 모듈 상태, 프로세스 상태 및 스레드 상태의 세 가지 종류가 있습니다. 경우에 따라 이러한 상태 유형을 결합할 수 있습니다. 예를 들어 MFC의 핸들 맵은 모듈 로컬 및 스레드 로컬입니다. 이렇게 하면 두 개의 서로 다른 모듈이 각 스레드에 서로 다른 맵을 가질 수 있습니다.

프로세스 상태와 스레드 상태는 비슷합니다. 이러한 데이터 항목은 일반적으로 전역 변수였지만 적절한 Win32s 지원 또는 적절한 다중 스레딩 지원을 위해 지정된 프로세스 또는 스레드와 관련이 있어야 하는 항목입니다. 지정된 데이터 항목이 적합한 범주는 해당 항목과 프로세스 및 스레드 경계와 관련하여 원하는 의미 체계에 따라 달라집니다.

모듈 상태는 로컬 또는 스레드 로컬을 처리하는 진정한 전역 상태 또는 상태를 포함할 수 있다는 측면에서 고유합니다. 또한 신속하게 전환할 수 있습니다.

모듈 상태 전환

각 스레드는 "현재" 또는 "활성" 모듈 상태에 대한 포인터를 포함합니다(당연히 포인터는 MFC의 스레드 로컬 상태의 일부임). 이 포인터는 실행 스레드가 OLE 컨트롤 또는 DLL로 호출하는 애플리케이션 또는 애플리케이션으로 다시 호출하는 OLE 컨트롤과 같은 모듈 경계를 통과할 때 변경됩니다.

현재 모듈 상태는 호출 AfxSetModuleState하여 전환됩니다. 대부분의 경우 API를 직접 처리하지 않습니다. MFC는 대부분의 경우 이를 호출합니다(WinMain, OLE 진입점 AfxWndProc등). 이 작업은 특수 WndProc한 모듈에 정적으로 연결하여 작성하는 모든 구성 요소와 현재 상태여야 하는 모듈 상태를 알고 있는 특수 WinMain (또는 DllMain)에서 수행됩니다. DLLMODUL을 보면 이 코드를 볼 수 있습니다. CPP 또는 APPMODUL. MFC\SRC 디렉터리의 CPP입니다.

모듈 상태를 설정한 다음 다시 설정하지 않으려는 경우는 드뭅니다. 대부분의 경우 자신의 모듈 상태를 현재 모듈 상태로 "푸시"한 다음 완료 후 원래 컨텍스트를 다시 "팝"합니다. 이 작업은 매크로 AFX_MANAGE_STATE 특수 클래스 AFX_MAINTAIN_STATE에 의해 수행됩니다.

CCmdTarget 에는 모듈 상태 전환을 지원하는 특수 기능이 있습니다. 특히 CCmdTarget OLE 자동화 및 OLE COM 진입점에 사용되는 루트 클래스입니다. 시스템에 노출된 다른 진입점과 마찬가지로 이러한 진입점은 올바른 모듈 상태를 설정해야 합니다. 지정된 CCmdTarget 모듈 상태가 "올바른" 모듈 상태를 어떻게 알 수 있나요? 대답은 생성될 때 "현재" 모듈 상태가 무엇인지 "기억"하여 나중에 호출될 때 현재 모듈 상태를 "기억됨" 값으로 설정할 수 있다는 것입니다. 결과적으로 지정된 CCmdTarget 개체가 연결된 모듈 상태는 개체가 생성될 때 현재 상태였던 모듈 상태입니다. INPROC 서버를 로드하고, 개체를 만들고, 메서드를 호출하는 간단한 예제를 살펴보세요.

  1. DLL은 OLE에서 .를 사용하여 LoadLibrary로드됩니다.

  2. RawDllMain 가 먼저 호출됩니다. 모듈 상태를 DLL의 알려진 정적 모듈 상태로 설정합니다. 이러한 이유로 RawDllMain DLL에 정적으로 연결됩니다.

  3. 개체와 연결된 클래스 팩터리의 생성자가 호출됩니다. COleObjectFactory 는 파생되어 CCmdTarget 결과적으로 인스턴스화된 모듈 상태를 기억합니다. 이는 중요합니다. 클래스 팩터리에서 개체를 만들라는 메시지가 표시되면 현재로 만들 모듈 상태를 알 수 있습니다.

  4. DllGetClassObject 클래스 팩터리를 가져오기 위해 호출됩니다. MFC는 이 모듈과 연결된 클래스 팩터리 목록을 검색하여 반환합니다.

  5. COleObjectFactory::XClassFactory2::CreateInstance을 호출합니다. 개체를 만들고 반환하기 전에 이 함수는 모듈 상태를 3단계의 현재 모듈 상태(인스턴스화할 때 COleObjectFactory 현재 상태)로 설정합니다. 이 작업은 METHOD_PROLOGUE 내부에서 수행됩니다.

  6. 개체를 만들 때도 파생 개체이며 CCmdTarget 어떤 모듈 상태가 활성 상태인지 기억한 것과 같은 방식으로 COleObjectFactory 이 새 개체도 마찬가지입니다. 이제 개체는 호출할 때마다 전환할 모듈 상태를 알 수 있습니다.

  7. 클라이언트는 호출에서 받은 OLE COM 개체에서 함수를 호출합니다 CoCreateInstance . 개체를 호출할 때와 마찬가지로 COleObjectFactory 모듈 상태를 전환하는 데 사용합니다METHOD_PROLOGUE.

여기에서 볼 수 있듯이 모듈 상태는 생성될 때 개체에서 개체로 전파됩니다. 모듈 상태를 적절하게 설정하는 것이 중요합니다. 설정되지 않은 경우 DLL 또는 COM 개체가 호출하는 MFC 애플리케이션과 제대로 상호 작용하지 않거나 자체 리소스를 찾을 수 없거나 다른 비참한 방법으로 실패할 수 있습니다.

특정 종류의 DLL, 특히 "MFC 확장" DLL은 모듈 상태를 RawDllMain 전환하지 않습니다(실제로는 일반적으로 모듈이 없음 RawDllMain). 이는 실제로 사용하는 애플리케이션에 있는 것처럼 "마치" 동작하기 위한 것입니다. 이는 실행 중인 애플리케이션의 일부이며 해당 애플리케이션의 전역 상태를 수정하려는 의도입니다.

OLE 컨트롤 및 기타 DLL은 매우 다릅니다. 호출 애플리케이션의 상태를 수정하지 않으려는 경우 호출하는 애플리케이션은 MFC 애플리케이션이 아닐 수도 있으므로 수정할 상태가 없을 수 있습니다. 이것이 모듈 상태 전환이 발명된 이유입니다.

DLL에서 대화 상자를 시작하는 함수와 같이 DLL에서 내보낸 함수의 경우 함수의 시작 부분에 다음 코드를 추가해야 합니다.

AFX_MANAGE_STATE(AfxGetStaticModuleState())

이렇게 하면 현재 모듈 상태를 AfxGetStaticModuleState에서 반환된 상태로 현재 범위가 끝날 때까지 바꿉니다.

AFX_MODULE_STATE 매크로를 사용하지 않으면 DLL의 리소스에 문제가 발생합니다. 기본적으로 MFC는 기본 애플리케이션의 리소스 핸들을 사용하여 리소스 템플릿을 로드합니다. 이 템플릿은 실제로 DLL에 저장됩니다. 근본 원인은 MFC의 모듈 상태 정보가 AFX_MODULE_STATE 매크로에 의해 전환되지 않았기 때문에 발생합니다. 리소스 핸들은 MFC의 모듈 상태에서 복구됩니다. 모듈 상태를 전환하지 않으면 잘못된 리소스 핸들이 사용됩니다.

AFX_MODULE_STATE DLL의 모든 함수에 넣을 필요는 없습니다. 예를 들어 InitInstance MFC는 모듈 상태를 자동으로 이전 InitInstance 으로 이동한 다음 반환 후 InitInstance 다시 전환하므로 AFX_MODULE_STATE 없이 애플리케이션의 MFC 코드에서 호출할 수 있습니다. 모든 메시지 맵 처리기에서도 마찬가지입니다. 일반 MFC DLL에는 메시지를 라우팅하기 전에 모듈 상태를 자동으로 전환하는 특수 마스터 창 프로시저가 있습니다.

로컬 데이터 처리

Win32s DLL 모델의 어려움이 아니었다면 로컬 데이터를 처리하는 것은 큰 문제가 되지 않을 것입니다. Win32s에서 모든 DLL은 여러 애플리케이션에서 로드하는 경우에도 전역 데이터를 공유합니다. 이는 각 DLL이 DLL에 연결하는 각 프로세스에서 데이터 공간의 별도 복사본을 가져오는 "실제" Win32 DLL 데이터 모델과는 매우 다릅니다. 복잡성을 더하기 위해 Win32s DLL의 힙에 할당된 데이터는 실제로 프로세스에 따라 다릅니다(적어도 소유권이 있는 한). 다음 데이터 및 코드를 고려합니다.

static CString strGlobal; // at file scope

__declspec(dllexport)
void SetGlobalString(LPCTSTR lpsz)
{
    strGlobal = lpsz;
}

__declspec(dllexport)
void GetGlobalString(LPCTSTR lpsz, size_t cb)
{
    StringCbCopy(lpsz, cb, strGlobal);
}

위의 코드가 DLL에 있고 DLL이 두 프로세스 A와 B에 의해 로드되는 경우(실제로 동일한 애플리케이션의 두 인스턴스일 수 있음) 어떤 일이 발생하는지 고려합니다. 호출 SetGlobalString("Hello from A")합니다. 결과적으로 메모리는 프로세스 A의 컨텍스트에서 데이터에 할당 CString 됩니다. CString 그 자체는 전역이며 A와 B 모두에 표시됩니다. 이제 B가 호출합니다 GetGlobalString(sz, sizeof(sz)). B는 A 집합의 데이터를 볼 수 있습니다. Win32s는 Win32와 같은 프로세스 간에 보호를 제공하지 않기 때문입니다. 이것이 첫 번째 문제입니다. 대부분의 경우 하나의 애플리케이션이 다른 애플리케이션에서 소유한 것으로 간주되는 글로벌 데이터에 영향을 주는 것은 바람직하지 않습니다.

추가 문제도 있습니다. 이제 A가 종료되는 경우를 가정해 보겠습니다. A가 종료되면 'strGlobal' 문자열에서 사용하는 메모리를 시스템에 사용할 수 있습니다. 즉, 프로세스 A에 의해 할당된 모든 메모리는 운영 체제에서 자동으로 해제됩니다. 소멸자가 호출되고 있기 때문에 CString 해제되지 않습니다. 아직 호출되지 않았습니다. 할당한 애플리케이션이 장면을 떠났기 때문에 해제됩니다. 이제 B가 호출 GetGlobalString(sz, sizeof(sz))되면 유효한 데이터를 얻지 못할 수 있습니다. 다른 응용 프로그램에서는 해당 메모리를 다른 용도로 사용했을 수 있습니다.

분명히 문제가 있습니다. MFC 3.x는 TLS(스레드 로컬 스토리지)라는 기술을 사용했습니다. MFC 3.x는 호출되지 않더라도 Win32s에서 실제로 프로세스 로컬 스토리지 인덱스 역할을 하는 TLS 인덱스를 할당한 다음 해당 TLS 인덱스를 기반으로 모든 데이터를 참조합니다. 이는 Win32에 스레드 로컬 데이터를 저장하는 데 사용된 TLS 인덱스와 유사합니다(해당 주제에 대한 자세한 내용은 아래 참조). 이로 인해 모든 MFC DLL이 프로세스당 두 개 이상의 TLS 인덱스를 활용하게 됩니다. 많은 OLE 컨트롤 DLL(OCX)을 로드하는 경우 TLS 인덱스가 빠르게 부족합니다(64개만 사용 가능). 또한 MFC는 이 모든 데이터를 단일 구조로 한 곳에 배치해야 했습니다. 매우 확장 가능하지 않았으며 TLS 인덱스의 사용과 관련하여 이상적이지 않았습니다.

MFC 4.x는 로컬로 처리해야 하는 데이터를 "래핑"할 수 있는 클래스 템플릿 집합으로 이 문제를 해결합니다. 예를 들어 위에서 멘션 문제는 다음을 작성하여 해결할 수 있습니다.

struct CMyGlobalData : public CNoTrackObject
{
    CString strGlobal;
};
CProcessLocal<CMyGlobalData> globalData;

__declspec(dllexport)
void SetGlobalString(LPCTSTR lpsz)
{
    globalData->strGlobal = lpsz;
}

__declspec(dllexport)
void GetGlobalString(LPCTSTR lpsz, size_t cb)
{
    StringCbCopy(lpsz, cb, globalData->strGlobal);
}

MFC는 이를 두 단계로 구현합니다. 먼저, 프로세스당 두 개의 TLS 인덱스만 사용하는 Win32 Tls* API(TlsAlloc, TlsSetValue, TlsGetValue 등)의 맨 위에는 DLL 수에 관계없이 두 개의 TLS 인덱스만 사용하는 계층이 있습니다. 둘째, CProcessLocal 이 데이터에 액세스하기 위해 템플릿이 제공됩니다. 위에서 볼 수 있는 직관적인 구문을 허용하는 연산> 자를 재정의합니다. 래핑 CProcessLocal 되는 모든 개체는 .에서 CNoTrackObject파생되어야 합니다. CNoTrackObject는 프로세스가 종료될 때 MFC가 프로세스 로컬 개체를 자동으로 삭제할 수 있도록 하위 수준 할당자(LocalAlloc/LocalFree) 및 가상 소멸자를 제공합니다. 추가 클린 필요한 경우 이러한 개체에 사용자 지정 소멸자가 있을 수 있습니다. 위의 예제에서는 컴파일러가 포함된 CString 개체를 삭제하는 기본 소멸자를 생성하므로 필요하지 않습니다.

이 접근 방식에는 다른 흥미로운 이점이 있습니다. 모든 CProcessLocal 개체는 자동으로 제거될 뿐만 아니라 필요할 때까지 생성되지 않습니다. CProcessLocal::operator-> 는 처음 호출될 때 연결된 개체를 인스턴스화하고 더 빨리 인스턴스화하지 않습니다. 위의 예제에서는 'strGlobal' 문자열이 처음 SetGlobalString 생성 GetGlobalString 되거나 호출될 때까지 생성되지 않음을 의미합니다. 경우에 따라 DLL 시작 시간을 줄이는 데 도움이 될 수 있습니다.

스레드 로컬 데이터

로컬 데이터를 처리하는 것과 마찬가지로 스레드 로컬 데이터는 데이터가 지정된 스레드에 로컬이어야 할 때 사용됩니다. 즉, 해당 데이터에 액세스하는 각 스레드에 대해 별도의 데이터 인스턴스가 필요합니다. 광범위한 동기화 메커니즘 대신 여러 번 사용할 수 있습니다. 여러 스레드에서 데이터를 공유할 필요가 없는 경우 이러한 메커니즘은 비용이 많이 들고 불필요할 수 있습니다. 위의 샘플과 CString 마찬가지로 개체가 있다고 가정해 보겠습니다. 템플릿으로 래핑하여 스레드를 로컬로 만들 수 있습니다.CThreadLocal

struct CMyThreadData : public CNoTrackObject
{
    CString strThread;
};
CThreadLocal<CMyThreadData> threadData;

void MakeRandomString()
{
    // a kind of card shuffle (not a great one)
    CString& str = threadData->strThread;
    str.Empty();
    while (str.GetLength() != 52)
    {
        unsigned int randomNumber;
        errno_t randErr;
        randErr = rand_s(&randomNumber);

        if (randErr == 0)
        {
            TCHAR ch = randomNumber % 52 + 1;
            if (str.Find(ch) <0)
            str += ch; // not found, add it
        }
    }
}

서로 다른 두 스레드에서 호출된 경우 MakeRandomString 각각은 다른 스레드를 방해하지 않고 서로 다른 방식으로 문자열을 "순서 섞습니다". 이는 실제로 strThread 하나의 전역 인스턴스가 아닌 스레드당 인스턴스가 있기 때문입니다.

루프 반복당 한 번이 아니라 주소를 한 번 캡처 CString 하는 데 참조를 사용하는 방법을 확인합니다. 루프 코드는 'str'가 사용되는 모든 곳에서 작성 threadData->strThread 되었을 수 있지만 코드 실행 속도가 훨씬 느려집니다. 이러한 참조가 루프에서 발생할 때 데이터에 대한 참조를 캐시하는 것이 가장 좋습니다.

클래스 템플릿은 CThreadLocal 동일한 메커니즘과 CProcessLocal 동일한 구현 기술을 사용합니다.

참고 항목

번호별 기술 참고 사항
범주별 기술 참고 사항