WIN run main thread redirects ignoring SolarMutex

This way we can drop all the special nReleased handling. Instead
we use the same mechanism as on Mac, where we keep the lock, but
disable it for the main thread. As a security measure we assert on
duplicate redirects, which should not happen.

As a result we can't use SendMessage on the main thread itself,
which would normally just call the WinProc directly. This could be
accomplished by converting the redirect bool into a counter, which
should be safe, as no other thread could acquire the SolarMutex,
as we don't release it.

Change-Id: Icd87b3da37a2489f3cad2bc80215bf93fc41d388
Reviewed-on: https://gerrit.libreoffice.org/42583
Tested-by: Jenkins <ci@libreoffice.org>
Reviewed-by: Jan-Marek Glogowski <glogow@fbihome.de>
This commit is contained in:
Jan-Marek Glogowski 2017-08-28 19:58:32 +02:00
parent f633dcdfc0
commit 4baec725e0
16 changed files with 138 additions and 99 deletions

View file

@ -495,12 +495,6 @@ public:
*/
static void EndYield();
/** Acquire SolarMutex after it has been temporarily dropped completely.
This will Reschedule() on WNT and just acquire on other platforms.
*/
static void ReAcquireSolarMutex(sal_uLong nReleased);
/** @brief Get the Solar Mutex for this thread.
Get the Solar Mutex that prevents other threads from accessing VCL
@ -1481,7 +1475,7 @@ class SolarMutexReleaser
const sal_uInt32 mnReleased;
public:
SolarMutexReleaser(): mnReleased(Application::ReleaseSolarMutex()) {}
~SolarMutexReleaser() { Application::ReAcquireSolarMutex(mnReleased); }
~SolarMutexReleaser() { Application::AcquireSolarMutex( mnReleased ); }
};
VCL_DLLPUBLIC Application* GetpApp();

View file

@ -89,6 +89,21 @@ can be added to the scheduler reasonably.
= Implementation details =
== General: main thread deferral ==
Currently for Mac and Windows, we run main thread deferrals by disabling the
SolarMutex using a boolean. In the case of the redirect, this makes
tryToaAcquire and doAcquire return true or 1, while a release is ignored.
Also the IsCurrentThread() mutex check function will act accordingly, so all
the DBG_TESTSOLARMUTEX won't fail.
Since we just disable the locks when we start running the deferred code in the
main thread, we won't let the main thread run into stuff, where it would
normally wait for the SolarMutex.
Eventually this will move into the GenericSolarMutex. KDE / Qt also does main
thread redirects using Qt::BlockingQueuedConnection.
== MacOS implementation details ==
Generally the Scheduler is handled as expected, except on resize, which is
@ -100,9 +115,9 @@ Like the Windows backend, all Cocoa / GUI handling also has to be run in
the main thread. We're emulating Windows out-of-order PeekMessage processing,
via a YieldWakeupEvent and two conditionals. When in a RUNINMAIN call, all
the DBG_TESTSOLARMUTEX calls are disabled, as we can't release the SolarMutex,
but we can prevent running any other SolarMutex based code. Same for all the
SolarMutex acquire and release calls, so the calling and the main thread
don't deadlock. Those wakeup events must be ignored to prevent busy-locks.
but we can prevent running any other SolarMutex based code. Those wakeup
events must be ignored to prevent busy-locks. For more info read the "General:
main thread deferral" section.
We can neither rely on MacOS dispatch_sync code block execution nor the
message handling, as both can't be prioritized or filtered and the first
@ -136,6 +151,10 @@ the timer callback message, which is checked before starting the Scheduler.
This way we can end with multiple timer callback message in the queue, which
we were asserting.
To run the required GUI code in the main thread without unlocking the
SolarMutex, we "disable" it. For more infos read the "General: main thread
deferral" section.
== KDE implementation details ==
This implementation also works as intended. But there is a different Yield

View file

@ -304,12 +304,9 @@ SalBitmap* SvpSalInstance::CreateSalBitmap()
#endif
}
bool SvpSalInstance::DoYield(bool bWait, bool bHandleAllCurrentEvents, sal_uLong const nReleased)
bool SvpSalInstance::DoYield(bool bWait, bool bHandleAllCurrentEvents)
{
(void) nReleased;
assert(nReleased == 0); // not implemented
// first, check for already queued events.
std::list< SalUserEvent > aEvents;
{
osl::MutexGuard g(m_aEventGuard);

View file

@ -155,7 +155,7 @@ public:
// wait next event and dispatch
// must returned by UserEvent (SalFrame::PostEvent)
// and timer
virtual bool DoYield(bool bWait, bool bHandleAllCurrentEvents, sal_uLong nReleased) override;
virtual bool DoYield(bool bWait, bool bHandleAllCurrentEvents) override;
virtual bool AnyInput( VclInputFlags nType ) override;
virtual bool IsMainThread() const override { return true; }

View file

@ -114,8 +114,7 @@ public:
virtual comphelper::SolarMutex* GetYieldMutex() override;
virtual sal_uInt32 ReleaseYieldMutex( bool bUnlockAll = false ) override;
virtual void AcquireYieldMutex( sal_uInt32 nCount = 1 ) override;
virtual bool DoYield(bool bWait, bool bHandleAllCurrentEvents,
sal_uLong nReleased) override;
virtual bool DoYield(bool bWait, bool bHandleAllCurrentEvents) override;
virtual bool AnyInput( VclInputFlags nType ) override;
virtual SalMenu* CreateMenu( bool bMenuBar, Menu* pVCLMenu ) override;
virtual void DestroyMenu( SalMenu* ) override;

View file

@ -133,7 +133,7 @@ public:
* If bHandleAllCurrentEvents - dispatch multiple posted
* user events. Returns true if events were processed.
*/
virtual bool DoYield(bool bWait, bool bHandleAllCurrentEvents, sal_uLong nReleased) = 0;
virtual bool DoYield(bool bWait, bool bHandleAllCurrentEvents) = 0;
virtual bool AnyInput( VclInputFlags nType ) = 0;
// menus

View file

@ -208,7 +208,7 @@ public:
const SystemGraphicsData* = nullptr ) override;
virtual SalBitmap* CreateSalBitmap() override;
virtual bool DoYield(bool bWait, bool bHandleAllCurrentEvents, sal_uLong nReleased) override;
virtual bool DoYield(bool bWait, bool bHandleAllCurrentEvents) override;
virtual bool AnyInput( VclInputFlags nType ) override;
// impossible to handle correctly, as "main thread" depends on the dispatch mutex
virtual bool IsMainThread() const override { return false; }

View file

@ -74,7 +74,7 @@ public:
virtual SalSession* CreateSalSession() override;
virtual OpenGLContext* CreateOpenGLContext() override;
virtual bool DoYield(bool bWait, bool bHandleAllCurrentEvents, sal_uLong nReleased) override;
virtual bool DoYield(bool bWait, bool bHandleAllCurrentEvents) override;
virtual bool AnyInput( VclInputFlags nType ) override;
virtual bool IsMainThread() const override { return true; }

View file

@ -34,6 +34,9 @@ public:
/// The Yield mutex ensures that only one thread calls into VCL
SalYieldMutex* mpSalYieldMutex;
osl::Condition maWaitingYieldCond;
bool mbNoYieldLock;
public:
WinSalInstance();
virtual ~WinSalInstance() override;
@ -63,7 +66,7 @@ public:
virtual void AcquireYieldMutex( sal_uInt32 nCount = 1 ) override;
virtual bool IsMainThread() const override;
virtual bool DoYield(bool bWait, bool bHandleAllCurrentEvents, sal_uLong nReleased) override;
virtual bool DoYield(bool bWait, bool bHandleAllCurrentEvents) override;
virtual bool AnyInput( VclInputFlags nType ) override;
virtual SalMenu* CreateMenu( bool bMenuBar, Menu* ) override;
virtual void DestroyMenu( SalMenu* ) override;

View file

@ -533,10 +533,8 @@ SAL_WNODEPRECATED_DECLARATIONS_PUSH
SAL_WNODEPRECATED_DECLARATIONS_POP
}
bool AquaSalInstance::DoYield(bool bWait, bool bHandleAllCurrentEvents, sal_uLong const nReleased)
bool AquaSalInstance::DoYield(bool bWait, bool bHandleAllCurrentEvents)
{
(void) nReleased;
assert(nReleased == 0); // not implemented
bool bHadEvent = false;
// ensure that the per thread autorelease pool is top level and

View file

@ -148,7 +148,7 @@ void TimerTest::testIdleMainloop()
// can't test this via Application::Yield since this
// also processes all tasks directly via the scheduler.
pSVData->maAppData.mnDispatchLevel++;
pSVData->mpDefInst->DoYield(true, false, 0);
pSVData->mpDefInst->DoYield(true, false);
pSVData->maAppData.mnDispatchLevel--;
}
CPPUNIT_ASSERT_MESSAGE("mainloop idle triggered", bTriggered);

View file

@ -451,12 +451,12 @@ void Application::Execute()
pSVData->maAppData.mbInAppExecute = false;
}
inline bool ImplYield(bool i_bWait, bool i_bAllEvents, sal_uLong const nReleased)
inline bool ImplYield(bool i_bWait, bool i_bAllEvents)
{
ImplSVData* pSVData = ImplGetSVData();
SAL_INFO("vcl.schedule", "Enter ImplYield: " << (i_bWait ? "wait" : "no wait") <<
": " << (i_bAllEvents ? "all events" : "one event") << ": " << nReleased);
": " << (i_bAllEvents ? "all events" : "one event"));
// TODO: there's a data race here on WNT only because ImplYield may be
// called without SolarMutex; if we can get rid of LazyDelete (with VclPtr)
@ -466,10 +466,8 @@ inline bool ImplYield(bool i_bWait, bool i_bAllEvents, sal_uLong const nReleased
// do not wait for events if application was already quit; in that
// case only dispatch events already available
bool bProcessedEvent =
pSVData->mpDefInst->DoYield(
i_bWait && !pSVData->maAppData.mbAppQuit,
i_bAllEvents, nReleased);
bool bProcessedEvent = pSVData->mpDefInst->DoYield(
i_bWait && !pSVData->maAppData.mbAppQuit, i_bAllEvents );
pSVData->maAppData.mnDispatchLevel--;
@ -485,7 +483,7 @@ inline bool ImplYield(bool i_bWait, bool i_bAllEvents, sal_uLong const nReleased
bool Application::Reschedule( bool i_bAllEvents )
{
return ImplYield(false, i_bAllEvents, 0);
return ImplYield(false, i_bAllEvents);
}
void Scheduler::ProcessEventsToSignal(bool& bSignal)
@ -537,27 +535,7 @@ SAL_DLLPUBLIC_EXPORT void unit_lok_process_events_to_idle()
void Application::Yield()
{
ImplYield(true, false, 0);
}
void Application::ReAcquireSolarMutex(sal_uLong const nReleased)
{
// 0 would mean that events/timers will be handled without locking
// SolarMutex (racy)
SAL_WARN_IF(nReleased == 0, "vcl", "SolarMutexReleaser without SolarMutex");
#ifdef _WIN32
if (nReleased == 0 || ImplGetSVData()->mbDeInit) //do not Yield in DeInitVCL
AcquireSolarMutex(nReleased);
else
ImplYield(false, false, nReleased);
#else
// a) Yield is not needed on non-WNT platforms
// b) some Yield implementations for X11 (e.g. kde4) make it non-obvious
// how to use nReleased
// c) would require a review of what all Yield implementations do
// currently _before_ releasing SolarMutex that would run without lock
AcquireSolarMutex(nReleased);
#endif
ImplYield(true, false);
}
IMPL_STATIC_LINK_NOARG( ImplSVAppData, ImplQuitMsg, void*, void )

View file

@ -166,10 +166,8 @@ bool X11SalInstance::AnyInput(VclInputFlags nType)
return bRet;
}
bool X11SalInstance::DoYield(bool bWait, bool bHandleAllCurrentEvents, sal_uLong const nReleased)
bool X11SalInstance::DoYield(bool bWait, bool bHandleAllCurrentEvents)
{
(void) nReleased;
assert(nReleased == 0); // not implemented
return mpXLib->Yield( bWait, bHandleAllCurrentEvents );
}

View file

@ -411,10 +411,8 @@ void GtkInstance::RemoveTimer ()
m_pTimer = nullptr;
}
bool GtkInstance::DoYield(bool bWait, bool bHandleAllCurrentEvents, sal_uLong const nReleased)
bool GtkInstance::DoYield(bool bWait, bool bHandleAllCurrentEvents)
{
(void) nReleased;
assert(nReleased == 0); // not implemented
EnsureInit();
return GetGtkSalData()->Yield( bWait, bHandleAllCurrentEvents );
}

View file

@ -112,6 +112,7 @@ public:
explicit SalYieldMutex();
virtual bool IsCurrentThread() const override;
virtual bool tryToAcquire() override;
};
SalYieldMutex::SalYieldMutex()
@ -139,6 +140,8 @@ void SalYieldMutex::doAcquire( sal_uInt32 nLockCount )
WinSalInstance* pInst = GetSalData()->mpFirstInstance;
if ( pInst && pInst->IsMainThread() )
{
if ( pInst->mbNoYieldLock )
return;
// tdf#96887 If this is the main thread, then we must wait for two things:
// - the mpSalYieldMutex being freed
// - SendMessage() being triggered
@ -167,15 +170,31 @@ void SalYieldMutex::doAcquire( sal_uInt32 nLockCount )
sal_uInt32 SalYieldMutex::doRelease( const bool bUnlockAll )
{
sal_uInt32 nCount = comphelper::GenericSolarMutex::doRelease( bUnlockAll );
WinSalInstance* pInst = GetSalData()->mpFirstInstance;
if ( pInst && pInst->mbNoYieldLock && pInst->IsMainThread() )
return 1;
sal_uInt32 nCount = comphelper::GenericSolarMutex::doRelease( bUnlockAll );
// wake up ImplSalYieldMutexAcquireWithWait() after release
if ( 0 == m_nCount )
m_condition.set();
return nCount;
}
bool SalYieldMutex::tryToAcquire()
{
WinSalInstance* pInst = GetSalData()->mpFirstInstance;
if ( pInst )
{
if ( pInst->mbNoYieldLock && pInst->IsMainThread() )
return true;
else
return comphelper::GenericSolarMutex::tryToAcquire();
}
else
return false;
}
void ImplSalYieldMutexAcquireWithWait( sal_uInt32 nCount )
{
WinSalInstance* pInst = GetSalData()->mpFirstInstance;
@ -186,10 +205,7 @@ void ImplSalYieldMutexAcquireWithWait( sal_uInt32 nCount )
bool ImplSalYieldMutexTryToAcquire()
{
WinSalInstance* pInst = GetSalData()->mpFirstInstance;
if ( pInst )
return pInst->mpSalYieldMutex->tryToAcquire();
else
return false;
return pInst ? pInst->mpSalYieldMutex->tryToAcquire() : false;
}
void ImplSalYieldMutexRelease()
@ -204,8 +220,11 @@ void ImplSalYieldMutexRelease()
bool SalYieldMutex::IsCurrentThread() const
{
// For the Windows backend, the LO identifier is the system thread ID
return m_nThreadId == GetCurrentThreadId();
if ( !GetSalData()->mpFirstInstance->mbNoYieldLock )
// For the Windows backend, the LO identifier is the system thread ID
return m_nThreadId == GetCurrentThreadId();
else
return GetSalData()->mpFirstInstance->IsMainThread();
}
void SalData::initKeyCodeMap()
@ -443,9 +462,10 @@ void DestroySalInstance( SalInstance* pInst )
}
WinSalInstance::WinSalInstance()
: mhComWnd( nullptr )
, mbNoYieldLock( false )
{
mhComWnd = nullptr;
mpSalYieldMutex = new SalYieldMutex();
mpSalYieldMutex = new SalYieldMutex();
mpSalYieldMutex->acquire();
}
@ -484,8 +504,7 @@ static void ImplSalDispatchMessage( MSG* pMsg )
ImplSalPostDispatchMsg( pMsg, lResult );
}
bool
ImplSalYield( bool bWait, bool bHandleAllCurrentEvents )
static bool ImplSalYield( bool bWait, bool bHandleAllCurrentEvents )
{
MSG aMsg;
bool bWasMsg = false, bOneEvent = false;
@ -535,38 +554,44 @@ bool WinSalInstance::IsMainThread() const
return pSalData->mnAppThreadId == GetCurrentThreadId();
}
bool WinSalInstance::DoYield(bool bWait, bool bHandleAllCurrentEvents, sal_uLong const nReleased)
bool WinSalInstance::DoYield(bool bWait, bool bHandleAllCurrentEvents)
{
bool bDidWork = false;
// NOTE: if nReleased != 0 this will be called without SolarMutex
// so don't do anything dangerous before releasing it here
sal_uInt32 const nCount = (nReleased != 0)
? nReleased : mpSalYieldMutex->release( true );
SolarMutexReleaser aReleaser;
if ( !IsMainThread() )
{
// #97739# A SendMessage call blocks until the called thread (here: the main thread)
// returns. During a yield however, messages are processed in the main thread that might
// result in a new message loop due to opening a dialog. Thus, SendMessage would not
// return which will block this thread!
// Solution: just give up the time slice and hope that messages are processed
// by the main thread anyway (where all windows are created)
// If the mainthread is not currently handling messages, then our SendMessage would
// also do nothing, so this seems to be reasonable.
if ( bWait )
{
maWaitingYieldCond.reset();
maWaitingYieldCond.wait();
bDidWork = true;
}
else {
// #97739# A SendMessage call blocks until the called thread (here: the main thread)
// returns. During a yield however, messages are processed in the main thread that might
// result in a new message loop due to opening a dialog. Thus, SendMessage would not
// return which will block this thread!
// Solution: just give up the time slice and hope that messages are processed
// by the main thread anyway (where all windows are created)
// If the mainthread is not currently handling messages, then our SendMessage would
// also do nothing, so this seems to be reasonable.
// #i18883# only sleep if potential deadlock scenario, ie, when a dialog is open
if( ImplGetSVData()->maAppData.mnModalMode )
Sleep(1);
else
// If you change the SendMessageW function, you might need to update
// the PeekMessage( ... PM_QS_POSTMESSAGE) calls!
bDidWork = SendMessageW( mhComWnd, SAL_MSG_THREADYIELD, (WPARAM)bWait, (LPARAM)bHandleAllCurrentEvents );
// #i18883# only sleep if potential deadlock scenario, ie, when a dialog is open
if( ImplGetSVData()->maAppData.mnModalMode )
Sleep(1);
else
// If you change the SendMessageW function, you might need to update
// the PeekMessage( ... PM_QS_POSTMESSAGE) calls!
bDidWork = SendMessageW( mhComWnd, SAL_MSG_THREADYIELD,
(WPARAM)bWait, (LPARAM)bHandleAllCurrentEvents );
}
}
else
{
if (nReleased == 0) // tdf#99383 ReAcquireSolarMutex shouldn't Yield
bDidWork = ImplSalYield( bWait, bHandleAllCurrentEvents );
bDidWork = ImplSalYield( bWait, bHandleAllCurrentEvents );
if ( bDidWork )
maWaitingYieldCond.set();
}
mpSalYieldMutex->acquire( nCount );
return bDidWork;
}
@ -574,6 +599,7 @@ bool WinSalInstance::DoYield(bool bWait, bool bHandleAllCurrentEvents, sal_uLong
LRESULT CALLBACK SalComWndProc( HWND, UINT nMsg, WPARAM wParam, LPARAM lParam, int& rDef )
{
LRESULT nRet = 0;
WinSalInstance *pInst = GetSalData()->mpFirstInstance;
switch ( nMsg )
{
@ -596,19 +622,31 @@ LRESULT CALLBACK SalComWndProc( HWND, UINT nMsg, WPARAM wParam, LPARAM lParam, i
static_cast<WinSalTimer*>(ImplGetSVData()->maSchedCtx.mpSalTimer)->ImplStop();
break;
case SAL_MSG_CREATEFRAME:
assert( !pInst->mbNoYieldLock );
pInst->mbNoYieldLock = true;
nRet = reinterpret_cast<LRESULT>(ImplSalCreateFrame( GetSalData()->mpFirstInstance, reinterpret_cast<HWND>(lParam), (SalFrameStyleFlags)wParam ));
pInst->mbNoYieldLock = false;
rDef = FALSE;
break;
case SAL_MSG_RECREATEHWND:
assert( !pInst->mbNoYieldLock );
pInst->mbNoYieldLock = true;
nRet = reinterpret_cast<LRESULT>(ImplSalReCreateHWND( reinterpret_cast<HWND>(wParam), reinterpret_cast<HWND>(lParam), false ));
pInst->mbNoYieldLock = false;
rDef = FALSE;
break;
case SAL_MSG_RECREATECHILDHWND:
assert( !pInst->mbNoYieldLock );
pInst->mbNoYieldLock = true;
nRet = reinterpret_cast<LRESULT>(ImplSalReCreateHWND( reinterpret_cast<HWND>(wParam), reinterpret_cast<HWND>(lParam), true ));
pInst->mbNoYieldLock = false;
rDef = FALSE;
break;
case SAL_MSG_DESTROYFRAME:
assert( !pInst->mbNoYieldLock );
pInst->mbNoYieldLock = true;
delete reinterpret_cast<SalFrame*>(lParam);
pInst->mbNoYieldLock = false;
rDef = FALSE;
break;
case SAL_MSG_DESTROYHWND:
@ -624,19 +662,31 @@ LRESULT CALLBACK SalComWndProc( HWND, UINT nMsg, WPARAM wParam, LPARAM lParam, i
rDef = FALSE;
break;
case SAL_MSG_CREATEOBJECT:
assert( !pInst->mbNoYieldLock );
pInst->mbNoYieldLock = true;
nRet = reinterpret_cast<LRESULT>(ImplSalCreateObject( GetSalData()->mpFirstInstance, reinterpret_cast<WinSalFrame*>(lParam) ));
pInst->mbNoYieldLock = false;
rDef = FALSE;
break;
case SAL_MSG_DESTROYOBJECT:
assert( !pInst->mbNoYieldLock );
pInst->mbNoYieldLock = true;
delete reinterpret_cast<SalObject*>(lParam);
pInst->mbNoYieldLock = false;
rDef = FALSE;
break;
case SAL_MSG_GETDC:
assert( !pInst->mbNoYieldLock );
pInst->mbNoYieldLock = true;
nRet = reinterpret_cast<LRESULT>(GetDCEx( reinterpret_cast<HWND>(wParam), nullptr, DCX_CACHE ));
pInst->mbNoYieldLock = false;
rDef = FALSE;
break;
case SAL_MSG_RELEASEDC:
assert( !pInst->mbNoYieldLock );
pInst->mbNoYieldLock = true;
ReleaseDC( reinterpret_cast<HWND>(wParam), reinterpret_cast<HDC>(lParam) );
pInst->mbNoYieldLock = false;
rDef = FALSE;
break;
case SAL_MSG_TIMER_CALLBACK:
@ -646,7 +696,7 @@ LRESULT CALLBACK SalComWndProc( HWND, UINT nMsg, WPARAM wParam, LPARAM lParam, i
MSG aMsg;
bool bValidMSG = pTimer->IsValidWPARAM( wParam );
// PM_QS_POSTMESSAGE is needed, so we don't process the SendMessage from DoYield!
while ( PeekMessageW(&aMsg, GetSalData()->mpFirstInstance->mhComWnd, SAL_MSG_TIMER_CALLBACK,
while ( PeekMessageW(&aMsg, pInst->mhComWnd, SAL_MSG_TIMER_CALLBACK,
SAL_MSG_TIMER_CALLBACK, PM_REMOVE | PM_NOYIELD | PM_QS_POSTMESSAGE) )
{
assert( !bValidMSG && "Unexpected non-last valid message" );

View file

@ -1054,10 +1054,15 @@ void WinSalFrame::ReleaseGraphics( SalGraphics* pGraphics )
if ( mpGraphics2->getDefPal() )
SelectPalette( mpGraphics2->getHDC(), mpGraphics2->getDefPal(), TRUE );
mpGraphics2->DeInitGraphics();
SendMessageW( pSalData->mpFirstInstance->mhComWnd,
SAL_MSG_RELEASEDC,
reinterpret_cast<WPARAM>(mhWnd),
reinterpret_cast<LPARAM>(mpGraphics2->getHDC()) );
// we don't want to run the WinProc in the main thread directly
// so we don't hit the mbNoYieldLock assert
if ( !pSalData->mpFirstInstance->IsMainThread() )
SendMessageW( pSalData->mpFirstInstance->mhComWnd,
SAL_MSG_RELEASEDC,
reinterpret_cast<WPARAM>(mhWnd),
reinterpret_cast<LPARAM>(mpGraphics2->getHDC()) );
else
ReleaseDC( mhWnd, mpGraphics2->getHDC() );
mpGraphics2->setHDC(nullptr);
pSalData->mnCacheDCInUse--;
}