Cooperative Fiber Mode Sample - Day 4
Last time we left off with the CLR calling into the host to create a task. So far everything’s been very simple. For each interface we’ve only needed to implement a couple of methods to get the bulk of the work done. While I’ve certainly left out a couple of lines of code here and there, nearly every other API in the interface merely returns S_OK.
Now things start to get really interesting! The IHostTask interface has just a few functions which are exposed to the runtime. But there’s a lot of other functionality related to the task embedded in here.
We’ll start off at the natural place to start a task: Start. It’s pretty simple:
FlagSet(TASK_FLAG_NOTSTARTED,false);
_ASSERTE(m_fiberAddr == NULL);
m_fiberAddr = ::CreateFiber(0,(LPFIBER_START_ROUTINE )CHostFiberProc,(PVOID)this);
// create the new thread
::CreateThread(NULL,NULL,reinterpret_cast<LPTHREAD_START_ROUTINE>(::StartNewThread),this,NULL,NULL);
return(S_OK);
We create a fiber for the new task, and we create a new thread. This new thread starts in StartNewThread, defined in callbacks.cpp. It looks something like:
CHostTaskManager *myManager = CHostTask::GetManager();
CHostTask *curTask = static_cast<CHostTask*>(lpParameter);
LPVOID fiberAddr = ConvertThreadToFiber(NULL);
curTask->SwitchIn();
BOOL fResult = ConvertFiberToThread();
Here we simply convert the new thread over to fibers, and then switch in the fiber passed as the argument. This brings us back to our CHostTask implementation where we need to look at the SwitchIn logic. This is the most complicated piece of code we’ve encountered yet.
CHostTask *curTask = GetCurrentTask();
_ASSERTE(curTask != this);
TlsSetValue(CHostTask::CurTaskTlsIndex,this);
this->AddRef();
// Save our thread handle so we can queue APCs
if(!DuplicateHandle(GetCurrentProcess(),
GetCurrentThread(),
GetCurrentProcess(),
&m_hCurThread, 0 , FALSE,
DUPLICATE_SAME_ACCESS))
{
m_hCurThread = INVALID_HANDLE_VALUE;
_ASSERTE(!"Duplicate handle failed");
}
FlagSet(TASK_FLAG_RUNNING, true);
::SwitchToFiber(GetFiberAddress());
// when the fiber switches back we need to switch in our
// previous task.
if(curTask->m_pCallback!=NULL)
{
HRESULT hr = curTask->m_pCallback->SwitchIn(GetCurrentThread());
_ASSERTE(SUCCEEDED(hr));
}
if(curTask->FlagCheck(TASK_FLAG_EXITING))
{
_ASSERTE(curTask->GetSwitchingTo());
curTask->ExitTask();
curTask->GetSwitchingTo()->SwitchIn();
}
// release the ref for TLS from the previous thread
CloseHandle(m_hCurThread);
m_hCurThread = INVALID_HANDLE_VALUE;
Release();
SetThreadPriority(GetCurrentThread(), curTask->m_iPriority);
SetThreadLocale(curTask->m_lcid);
What’s going on here? We have a couple of things to worry about when switching tasks. We have the task that we’re switching to (this) and we have the task we’re switching from (curTask).
One of the issues we’re concerned with is the lifetime of the task. While it’s running we don’t want it cleaned up, so we hold a reference to it (in Thread Local Store). A more complicated host would probably have a pool of tasks rather than the simple TLS mechanism.
If we need to alert a task we’ll queue an Asynchronous Procedure Call (APC) to it. Therefore the next thing we do is save the current thread’s handle into this task. We’ll use this in IHostTask::Alert. Finally we set the running flag and switch over to the new task.
The interesting thing to note about this method is there are 2 halves to it. After we call SwitchToFiber we are running on a different fiber on a different stack. We’ll only return to the bottom half after someone has switched back to “curTask”. This is the task that we switched away from. When this happens it’s now the bottom half’s job to tell the runtime that “curTask” is once again running.
It’s possible when we get to the bottom half “curTask” was only switched in to exit. If so we’ll notify the runtime and immediately re-run the task we’ve been set to re-run. This is an implementation detail of CoopFiber to allow calling ExitTask on the managed fiber implementation.
We’re nearly done so we clean up the resources we allocated in the top half for “this”. It’s no longer running, so we don’t need a reference to it. Finally we restore the settings for the thread that were stored in the task. These would have been changed when we did the intital SwitchToFiber which either switched in a task that was at the bottom half of SwitchIn, or the top of CHostFiberProc (in callbacks.cpp).
Wow, so that’s how we start a task! We create a new thread, that thread gets converted over to fibers, and we switch in the newly created task (which has a fiber already associated with it). That’ll end up in CHostFiber proc which we’ll cover in a future edition.
There’s just one more detail to cap off the life time of a task, and that is our internal SwitchOut API. All it essentially does is set some state and notify the runtime of the switch out:
FlagSet(TASK_FLAG_RUNNING,false);
if(m_pCallback!=NULL)
{
HRESULT hr = m_pCallback->SwitchOut();
}
Next time we’ll go over the remaining APIs on IHostTask that we haven’t covered yet.