- Home /
Wait in a coroutine without busy-waiting?
I'm in the middle of implementing a texture caching system and I'd like to implement it through coroutines (since they're a convenient control-flow mechanism for this workflow, and since network requests use IEnumerators). Essentially, my cache keeps three lists. One is the images that it has retrieved that are in the cache currently, another is a list of images that failed to load, and the last is the list of images that it's sent a request for. The workflow should be like this:
1) Request resource X
2) Resource X is in cache/marked as failed? -> Return resource X or failure
3) Resource X is on waiting list? -> Wait until resource X is retrieved, then return resource
4) Send a request for the resource, put it on the waiting list, then return resource
So, this works fine, except for part (3). I can use a WaitUntil to continue checking whether or not the resource is still on the waiting list, but that's busy waiting. I don't want the coroutine to wake up every frame and check the predicate; the better workflow for efficiency would be to keep a reference to every waiting coroutine, then make the coroutine who asked for the resource first to go and wake up every other coroutine that was waiting.
Answer by Bunny83 · Apr 12, 2018 at 08:11 PM
Well, busy waiting isn't really an issue. Coroutines do not really need to be "woken up". Coroutines are just statemachines. Internally the coroutine scheduler most likely does the same thing as the actual web request is carried out on a seperate thread.
Though a common implementation of loading resources is to use a "loading" class for each resource. If you are happy using an on complete callback you can simply do something like this:
public class TextureItem
{
public enum State { None, Requested, Loaded, Failed }
public string URL;
public Texture2D texture;
public string error;
public State state;
List<System.Action<TextureItem>> m_Callbacks = new List<System.Action<TextureItem>>();
public TextureItem(string aURL)
{
state = State.None;
URL = aURL;
}
public void Get(System.Action<TextureItem> aCallback)
{
if (state == State.Requested)
m_Callbacks.Add(aCallback);
else // in case of error or success, just call the callback directly
aCallback(this);
}
public IEnumerator Load()
{
WWW www = new WWW(URL);
state = State.Requested;
yield return www;
if (string.IsNullOrEmpty(www.error))
{
texture = www.texture;
state = State.Loaded;
foreach(var cb in m_Callbacks)
cb(this);
m_Callbacks.Clear();
}
else
{
error = www.error;
state = State.Failed;
}
}
}
public static class TextureCache
{
private Dictionary<string, TextureItem> m_Cache = new Dictionary<string, TextureItem>();
public static void Request(string aURL, System.Action<TextureItem> aCallback)
{
TextureItem item;
if (!m_Cache.TryGetValue(aURL, out item))
{
item = new TextureItem(aURL);
SomeMonobehaviourSingleton.Instance.StartCoroutine(item.Load());
m_Cache.Add(aURL, item);
}
item.Get(aCallback);
}
}
Note i quickly hacked this together so there may be some errors ^^. Also note that this requires a monobehaviour singleton in order to run the loading coroutines. Now you can simply request any texture like this:
TextureCache.Request("http://my.server/some/path", a=>{
if (a.state == TextureItem.State.Loaded)
Debug.Log("Texture loaded: " + a.texture.width + " / " + a.texture.height);
else
Debug.Log("Some error: " + a.error);
});
The fact that coroutines don't need to be woken up is what I'm trying to avoid here. When you use a yield instruction, Unity has to handle that internally. Is WaitForSecondsRealtime really implemented by setting a timer and waking up every frame to check if the timer has gone off? Why wouldn't it be implemented by yielding, and having the timer itself wake you up when it's done? That's what I'm trying to accomplish here. Waking up every frame makes sense in the context of actions that you'd call in Update() or FixedUpdate(). It does not make sense when trying to use a coroutine as a control flow mechanism; I want it to pause until told to start again.
Do you realise that all scripting code is run on the main thread? That means there has to be some sort of polling going on. $$anonymous$$y example here doesn't do any busy waiting on "our" side, however when you yield a WWW object Unity has to have some sort of check in Update in order to react to a finished download or an expired timer
As i said there is no "waking up" here. A coroutine IEnumerator is just an object with a $$anonymous$$oveNext method. Each time it's called you will move from one yield to the next. Btw: WaitForSecondsRealtime is actually a CustomYieldInstruction which is implemented as seperate coroutine that does exactly this: busy-waiting until "Time.realtimeSinceStartup" is greater than the desired timeout. Note that WaitForSeconds is not a CustomYieldInstruction but is handled internally on the native side. Though there has to be some kind of code that actually does the expire check for all coroutines which are currently "waiting". Of course with a central place in the coroutine scheduler we can do some optimisations. If we sort the waiting coroutine objects based on their expiration time we only have to check the first element each frame even when 100 coroutines are waiting.
Pending web requests can be handled with a threadsafe completion List. So when the actual downloading thread has finished it adds the coroutine back into the update chain. Though we don't know how Unity has implemented the coroutine scheduler on the native side so it's pointless to speculate.
Again, keep in $$anonymous$$d that Coroutines are not threads. It's cooperative multitasking and that involves cooperation of all tasks. Coroutines run on the main thread just like pretty much any other callback you get from Unity (Update, FixedUpdate, On$$anonymous$$ouseDown, ...)
What do you actually mean by "timer"? A PC has an actual timer hardware, however this belongs to the OS as there are very limited timers on our mainboard / chipsets. Almost all "timers" an OS provides to the user are actually some sort of polling timers through software which are based on a single hardware timer which runs at a constant frequency.
Your answer
