// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. //========================================================================= // // ThreadPoolRequest.cpp // // // //========================================================================= #include "common.h" #include "comdelegate.h" #include "comthreadpool.h" #include "threadpoolrequest.h" #include "win32threadpool.h" #include "class.h" #include "object.h" #include "field.h" #include "excep.h" #include "eeconfig.h" #include "corhost.h" #include "nativeoverlapped.h" #include "appdomain.inl" BYTE PerAppDomainTPCountList::s_padding[MAX_CACHE_LINE_SIZE - sizeof(LONG)]; // Make this point to unmanaged TP in case, no appdomains have initialized yet. // Cacheline aligned, hot variable DECLSPEC_ALIGN(MAX_CACHE_LINE_SIZE) LONG PerAppDomainTPCountList::s_ADHint = -1; // Move out of from preceeding variables' cache line DECLSPEC_ALIGN(MAX_CACHE_LINE_SIZE) UnManagedPerAppDomainTPCount PerAppDomainTPCountList::s_unmanagedTPCount; //The list of all per-appdomain work-request counts. ArrayListStatic PerAppDomainTPCountList::s_appDomainIndexList; void PerAppDomainTPCountList::InitAppDomainIndexList() { LIMITED_METHOD_CONTRACT; s_appDomainIndexList.Init(); } //--------------------------------------------------------------------------- //AddNewTPIndex adds and returns a per-appdomain TP entry whenever a new appdomain //is created. Our list count should be equal to the max number of appdomains created //in the system. // //Assumptions: //This function needs to be called under the SystemDomain lock. //The ArrayListStatic data dtructure allows traversing of the counts without a //lock, but addition to the list requires synchronization. // TPIndex PerAppDomainTPCountList::AddNewTPIndex() { STANDARD_VM_CONTRACT; DWORD count = s_appDomainIndexList.GetCount(); DWORD i = FindFirstFreeTpEntry(); if (i == UNUSED_THREADPOOL_INDEX) i = count; TPIndex index(i+1); if(count > i) { IPerAppDomainTPCount * pAdCount = dac_cast(s_appDomainIndexList.Get(i)); pAdCount->SetTPIndex(index); return index; } #ifdef _MSC_VER // Disable this warning - we intentionally want __declspec(align()) to insert trailing padding for us #pragma warning(disable:4316) // Object allocated on the heap may not be aligned for this type. #endif ManagedPerAppDomainTPCount * pAdCount = new ManagedPerAppDomainTPCount(index); #ifdef _MSC_VER #pragma warning(default:4316) // Object allocated on the heap may not be aligned for this type. #endif pAdCount->ResetState(); IfFailThrow(s_appDomainIndexList.Append(pAdCount)); return index; } DWORD PerAppDomainTPCountList::FindFirstFreeTpEntry() { CONTRACTL { NOTHROW; MODE_ANY; GC_NOTRIGGER; } CONTRACTL_END; DWORD DwnumADs = s_appDomainIndexList.GetCount(); DWORD Dwi; IPerAppDomainTPCount * pAdCount; DWORD DwfreeIndex = UNUSED_THREADPOOL_INDEX; for (Dwi=0;Dwi < DwnumADs;Dwi++) { pAdCount = dac_cast(s_appDomainIndexList.Get(Dwi)); _ASSERTE(pAdCount); if(pAdCount->IsTPIndexUnused()) { DwfreeIndex = Dwi; STRESS_LOG1(LF_THREADPOOL, LL_INFO1000, "FindFirstFreeTpEntry: reusing index %d\n", DwfreeIndex + 1); break; } } return DwfreeIndex; } //--------------------------------------------------------------------------- //ResetAppDomainIndex: Resets the AppDomain ID and the per-appdomain // thread pool counts // //Arguments: //index - The index into the s_appDomainIndexList for the AppDomain we're // trying to clear (the AD being unloaded) // //Assumptions: //This function needs to be called from the AD unload thread after all domain //bound objects have been finalized when it's safe to recycle the TPIndex. //ClearAppDomainRequestsActive can be called from this function because no // managed code is running (If managed code is running, this function needs //to be called under a managed per-appdomain lock). // void PerAppDomainTPCountList::ResetAppDomainIndex(TPIndex index) { CONTRACTL { NOTHROW; MODE_ANY; GC_TRIGGERS; } CONTRACTL_END; IPerAppDomainTPCount * pAdCount = dac_cast(s_appDomainIndexList.Get(index.m_dwIndex-1)); _ASSERTE(pAdCount); STRESS_LOG2(LF_THREADPOOL, LL_INFO1000, "ResetAppDomainIndex: index %d pAdCount %p\n", index.m_dwIndex, pAdCount); pAdCount->ResetState(); pAdCount->SetTPIndexUnused(); } //--------------------------------------------------------------------------- //AreRequestsPendingInAnyAppDomains checks to see if there any requests pending //in other appdomains. It also checks for pending unmanaged work requests. //This function is called at end of thread quantum to see if the thread needs to //transition into a different appdomain. This function may also be called by //the scheduler to check for any unscheduled work. // bool PerAppDomainTPCountList::AreRequestsPendingInAnyAppDomains() { CONTRACTL { NOTHROW; MODE_ANY; GC_NOTRIGGER; } CONTRACTL_END; DWORD DwnumADs = s_appDomainIndexList.GetCount(); DWORD Dwi; IPerAppDomainTPCount * pAdCount; bool fRequestsPending = false; for (Dwi=0;Dwi < DwnumADs;Dwi++) { pAdCount = dac_cast(s_appDomainIndexList.Get(Dwi)); _ASSERTE(pAdCount); if(pAdCount->IsRequestPending()) { fRequestsPending = true; break; } } if(s_unmanagedTPCount.IsRequestPending()) { fRequestsPending = true; } return fRequestsPending; } //--------------------------------------------------------------------------- //GetAppDomainIndexForThreadpoolDispatch is essentailly the //"AppDomain Scheduler". This function makes fairness/policy decisions as to //which appdomain the thread needs to enter to. This function needs to guarantee //that all appdomain work requests are processed fairly. At this time all //appdomain requests and the unmanaged work requests are treated with the same //priority. // //Return Value: //The appdomain ID in which to dispatch the worker thread,nmanaged work items //need to be processed. // LONG PerAppDomainTPCountList::GetAppDomainIndexForThreadpoolDispatch() { CONTRACTL { NOTHROW; MODE_ANY; GC_NOTRIGGER; } CONTRACTL_END; LONG hint = s_ADHint; DWORD count = s_appDomainIndexList.GetCount(); IPerAppDomainTPCount * pAdCount; DWORD Dwi; if (hint != -1) { pAdCount = dac_cast(s_appDomainIndexList.Get(hint)); } else { pAdCount = &s_unmanagedTPCount; } //temphint ensures that the check for appdomains proceeds in a pure round robin fashion. LONG temphint = hint; _ASSERTE( pAdCount); if (pAdCount->TakeActiveRequest()) goto HintDone; //If there is no work in any appdomains, check the unmanaged queue, hint = -1; for (Dwi=0;Dwi(s_appDomainIndexList.Get(temphint)); if (pAdCount->TakeActiveRequest()) { hint = temphint; goto HintDone; } temphint++; _ASSERTE( temphint <= (LONG)count); if(temphint == (LONG)count) { temphint = 0; } } if (hint == -1 && !s_unmanagedTPCount.TakeActiveRequest()) { //no work! return 0; } HintDone: if((hint+1) < (LONG)count) { s_ADHint = hint+1; } else { s_ADHint = -1; } if (hint == -1) { return hint; } else { return (hint+1); } } void UnManagedPerAppDomainTPCount::SetAppDomainRequestsActive() { WRAPPER_NO_CONTRACT; #ifndef DACCESS_COMPILE LONG count = VolatileLoad(&m_outstandingThreadRequestCount); while (count < (LONG)ThreadpoolMgr::NumberOfProcessors) { LONG prevCount = FastInterlockCompareExchange(&m_outstandingThreadRequestCount, count+1, count); if (prevCount == count) { ThreadpoolMgr::MaybeAddWorkingWorker(); ThreadpoolMgr::EnsureGateThreadRunning(); break; } count = prevCount; } #endif } bool FORCEINLINE UnManagedPerAppDomainTPCount::TakeActiveRequest() { LIMITED_METHOD_CONTRACT; LONG count = VolatileLoad(&m_outstandingThreadRequestCount); while (count > 0) { LONG prevCount = FastInterlockCompareExchange(&m_outstandingThreadRequestCount, count-1, count); if (prevCount == count) return true; count = prevCount; } return false; } FORCEINLINE void ReleaseWorkRequest(WorkRequest *workRequest) { ThreadpoolMgr::RecycleMemory( workRequest, ThreadpoolMgr::MEMTYPE_WorkRequest ); } typedef Wrapper< WorkRequest *, DoNothing, ReleaseWorkRequest > WorkRequestHolder; void UnManagedPerAppDomainTPCount::QueueUnmanagedWorkRequest(LPTHREAD_START_ROUTINE function, PVOID context) { CONTRACTL { THROWS; GC_TRIGGERS; MODE_ANY; } CONTRACTL_END;; #ifndef DACCESS_COMPILE WorkRequestHolder pWorkRequest; //Note, ideally we would want to use our own queues instead of those in //the thread pool class. However, the queus in thread pool class have //caching support, that shares memory with other commonly used structures //in the VM thread pool implementation. So, we decided to leverage those. pWorkRequest = ThreadpoolMgr::MakeWorkRequest(function, context); //MakeWorkRequest should throw if unable to allocate memory _ASSERTE(pWorkRequest != NULL); PREFIX_ASSUME(pWorkRequest != NULL); if (ETW_EVENT_ENABLED(MICROSOFT_WINDOWS_DOTNETRUNTIME_PROVIDER_DOTNET_Context, ThreadPoolEnqueue) && !ThreadpoolMgr::AreEtwQueueEventsSpeciallyHandled(function)) FireEtwThreadPoolEnqueue(pWorkRequest, GetClrInstanceId()); m_lock.Init(LOCK_TYPE_DEFAULT); { SpinLock::Holder slh(&m_lock); ThreadpoolMgr::EnqueueWorkRequest(pWorkRequest); pWorkRequest.SuppressRelease(); m_NumRequests++; } SetAppDomainRequestsActive(); #endif //DACCESS_COMPILE } PVOID UnManagedPerAppDomainTPCount::DeQueueUnManagedWorkRequest(bool* lastOne) { CONTRACTL { NOTHROW; GC_TRIGGERS; MODE_ANY; } CONTRACTL_END;; *lastOne = true; WorkRequest * pWorkRequest = ThreadpoolMgr::DequeueWorkRequest(); if (pWorkRequest) { m_NumRequests--; if(m_NumRequests > 0) *lastOne = false; } return (PVOID) pWorkRequest; } //--------------------------------------------------------------------------- //DispatchWorkItem manages dispatching of unmanaged work requests. It keeps //processing unmanaged requests for the "Quanta". Essentially this function is //a tight loop of dequeueing unmanaged work requests and dispatching them. // void UnManagedPerAppDomainTPCount::DispatchWorkItem(bool* foundWork, bool* wasNotRecalled) { #ifndef DACCESS_COMPILE *foundWork = false; *wasNotRecalled = true; bool enableWorkerTracking = CLRConfig::GetConfigValue(CLRConfig::INTERNAL_ThreadPool_EnableWorkerTracking) ? true : false; DWORD startTime; DWORD endTime; startTime = GetTickCount(); //For all practical puposes, the unmanaged part of thread pool is treated //as a special appdomain for thread pool purposes. The same logic as the //one in managed code for dispatching thread pool requests is repeated here. //Namely we continue to process requests until eithere there are none, or //the "Quanta has expired". See threadpool.cs for the managed counterpart. WorkRequest * pWorkRequest=NULL; LPTHREAD_START_ROUTINE wrFunction; LPVOID wrContext; bool firstIteration = true; bool lastOne = false; while (*wasNotRecalled) { m_lock.Init(LOCK_TYPE_DEFAULT); { SpinLock::Holder slh(&m_lock); pWorkRequest = (WorkRequest*) DeQueueUnManagedWorkRequest(&lastOne); } if (NULL == pWorkRequest) break; if (firstIteration && !lastOne) SetAppDomainRequestsActive(); firstIteration = false; *foundWork = true; if (GCHeapUtilities::IsGCInProgress(TRUE)) { // GC is imminent, so wait until GC is complete before executing next request. // this reduces in-flight objects allocated right before GC, easing the GC's work GCHeapUtilities::WaitForGCCompletion(TRUE); } PREFIX_ASSUME(pWorkRequest != NULL); _ASSERTE(pWorkRequest); wrFunction = pWorkRequest->Function; wrContext = pWorkRequest->Context; if (ETW_EVENT_ENABLED(MICROSOFT_WINDOWS_DOTNETRUNTIME_PROVIDER_DOTNET_Context, ThreadPoolDequeue) && !ThreadpoolMgr::AreEtwQueueEventsSpeciallyHandled(wrFunction)) FireEtwThreadPoolDequeue(pWorkRequest, GetClrInstanceId()); ThreadpoolMgr::FreeWorkRequest(pWorkRequest); if (enableWorkerTracking) { ThreadpoolMgr::ReportThreadStatus(true); (wrFunction) (wrContext); ThreadpoolMgr::ReportThreadStatus(false); } else { (wrFunction) (wrContext); } ThreadpoolMgr::NotifyWorkItemCompleted(); if (ThreadpoolMgr::ShouldAdjustMaxWorkersActive()) { DangerousNonHostedSpinLockTryHolder tal(&ThreadpoolMgr::ThreadAdjustmentLock); if (tal.Acquired()) { ThreadpoolMgr::AdjustMaxWorkersActive(); } else { // the lock is held by someone else, so they will take care of this for us. } } *wasNotRecalled = ThreadpoolMgr::ShouldWorkerKeepRunning(); Thread *pThread = GetThread(); if (pThread) { if (pThread->IsAbortRequested()) { pThread->EEResetAbort(Thread::TAR_ALL); } pThread->InternalReset(); } endTime = GetTickCount(); if ((endTime - startTime) >= TP_QUANTUM) { break; } } // if we're exiting for any reason other than the queue being empty, then we need to make sure another thread // will visit us later. if (NULL != pWorkRequest) { SetAppDomainRequestsActive(); } #endif //DACCESS_COMPILE } void ManagedPerAppDomainTPCount::SetAppDomainRequestsActive() { //This function should either be called by managed code or during AD unload, but before //the TpIndex is set to unused. // // Note that there is a separate count in managed code that stays in sync with this one over time. // The manage count is incremented before this one, and this one is decremented before the managed // one. // _ASSERTE(m_index.m_dwIndex != UNUSED_THREADPOOL_INDEX); #ifndef DACCESS_COMPILE LONG count = VolatileLoad(&m_numRequestsPending); while (true) { LONG prev = FastInterlockCompareExchange(&m_numRequestsPending, count+1, count); if (prev == count) { ThreadpoolMgr::MaybeAddWorkingWorker(); ThreadpoolMgr::EnsureGateThreadRunning(); break; } count = prev; } #endif } void ManagedPerAppDomainTPCount::ClearAppDomainRequestsActive() { LIMITED_METHOD_CONTRACT; //This function should either be called by managed code or during AD unload, but before //the TpIndex is set to unused. _ASSERTE(m_index.m_dwIndex != UNUSED_THREADPOOL_INDEX); LONG count = VolatileLoad(&m_numRequestsPending); while (count > 0) { LONG prev = FastInterlockCompareExchange(&m_numRequestsPending, 0, count); if (prev == count) break; count = prev; } } bool ManagedPerAppDomainTPCount::TakeActiveRequest() { LIMITED_METHOD_CONTRACT; LONG count = VolatileLoad(&m_numRequestsPending); while (count > 0) { LONG prev = FastInterlockCompareExchange(&m_numRequestsPending, count-1, count); if (prev == count) return true; count = prev; } return false; } #ifndef DACCESS_COMPILE //--------------------------------------------------------------------------- //DispatchWorkItem makes sure the right exception handling frames are setup, //the thread is transitioned into the correct appdomain, and the right managed //callback is called. // void ManagedPerAppDomainTPCount::DispatchWorkItem(bool* foundWork, bool* wasNotRecalled) { *foundWork = false; *wasNotRecalled = true; HRESULT hr; Thread * pThread = GetThread(); if (pThread == NULL) { ClrFlsSetThreadType(ThreadType_Threadpool_Worker); pThread = SetupThreadNoThrow(&hr); if (pThread == NULL) { return; } } { CONTRACTL { MODE_PREEMPTIVE; THROWS; GC_TRIGGERS; } CONTRACTL_END; GCX_COOP(); // // NOTE: there is a potential race between the time we retrieve the app // domain pointer, and the time which this thread enters the domain. // // To solve the race, we rely on the fact that there is a thread sync (via // GC) between releasing an app domain's handle, and destroying the // app domain. Thus it is important that we not go into preemptive gc mode // in that window. // { // This TPIndex may have been recycled since we chose it for workitem dispatch. // If so, the new AppDomain will necessarily have zero requests // pending (because the destruction of the previous AD that used this TPIndex // will have reset this object). We don't want to call into such an AppDomain. // TODO: fix this another way! // if (IsRequestPending()) { ManagedThreadBase::ThreadPool(QueueUserWorkItemManagedCallback, wasNotRecalled); } if (pThread->IsAbortRequested()) { // thread was aborted, and may not have had a chance to tell us it has work. ThreadpoolMgr::SetAppDomainRequestsActive(); ThreadpoolMgr::QueueUserWorkItem(NULL, NULL, 0, FALSE); } } // We should have released all locks. _ASSERTE(g_fEEShutDown || pThread->m_dwLockCount == 0 || pThread->m_fRudeAborted); *foundWork = true; } } #endif // !DACCESS_COMPILE