- Home /
How to update a progress bar for ANY asynchronous operation?
I'm not talking about loading a scene, I'm talking about ANY kind of asynchronous operation.
To be specific, I need to read a file and to go through very long iterations with some heavy calculus, and I'd like to have a progress bar showing the progress of this operations.
Unity is not thread safe, and I don't fully understand how coroutines work.
Can somebody help?
Answer by TreyH · May 02, 2018 at 09:01 PM
I think the most common way to make a progress bar is to bastardize the Slider UI Control. After that, you could just hook up a coroutine to update your slider as it ran through some lengthy operation.
IEnumerator HeavyCalculus(int ticks) {
var yieldInstruction = new WaitForEndOfFrame();
for (float k=0; k < ticks; k++) {
this.slider.value = k / ticks;
yield return yieldInstruction;
}
}
As for coroutines, you can read up on them in the official docs, or watch Unity's youtube guide on them.
The problem with coroutine solutions is that you have to be specific about the amount of operations you want to do in a single frame, and I don't want that.
For instance: I don't want to read one (or n) line(s) from a file, update the progress bar and then wait for the end of the frame. It would considerably slow down the file read process. I want to read AS $$anonymous$$ANY LINES AS POSSIBLE, update the progress bar and then wait.
That's what I mean by asynchronous operation, and I don't believe I can do it by means of coroutines. Or am I missing something?
"The problem with coroutine solutions is that you have to be specific about the amount of operations you want to do in a single frame"
That is not the case at all. You don't need to use WaitForEndOfFrame
as your yield instruction. There are several kinds of yields available to you during coroutines. See here.
How you break up a large task is up to you as the developer. If you want to set up a system where you read 100 lines then wait a few seconds, coroutines can do that trivially.
Alternatively, if you're looking for something that will just do some enormous / async task without clear breakpoints, then you could always use the native C# ThreadStart / Thread setup, but then you would need to either 1- fake a progress bar using whatever breakpoints your operation did have, or 2- relay that task's progress to some external monitor which then feeds a progress bar.
Well "processing as much as possible" is a bad criteria. Just run your for loop until it's complete within a single frame and you get "as much as possible" on a single thread. Of course your frame rate will drop to 0 at that point. Coroutines are not asynchronous at all. They implement synchronous cooperative multitasking. If a coroutine does not "cooperate" it will block just like a normal method would. Perfor$$anonymous$$g heavy tasks on the main thread and keeping a "good" frame rate is always a balance between how much work you can do to not drop below a certain framerate. This is generally significantly worse than "as much as possible".
Yes balancing work with a coroutine is quite difficult. Depending on the actual work using threads is the better approach
Yes, coroutines and/or threads are the way to go for long operations.
+1 for bastardizing Sliders... they also make pretty good health bars :D
i tried this but it says
ArgumentException: method return type is incompatible
Because you probably did not return "IEnumerator" which is required for a coroutine. So the error you mentioned certainly is not related to the code presented in the answer.
Answer by RandomJ · May 05, 2018 at 03:18 PM
Solved:
I have a progress variable, a lock, a thread and a coroutine.
Inside the thread, I lock the progress variable and update it at each iteration. Inside the coroutine, I check if the thread is running, lock the progress variable and update the progress bar.
Code snippet for whoever may be interested:
public string FileName;
public int MaxRows;
private Thread _thread;
private Object _locker;
private float _progress;
void Awake() {
_thread = new Thread(LoadFromFile);
_locker = new Object();
_progress = 0;
}
private void Start() {
GUIManager.Instance.ProgressBar.Begin();
_thread.Start();
StartCoroutine(LoadingRoutine());
}
IEnumerator LoadingRoutine() {
while (_thread.IsAlive) {
lock (_locker) {
GUIManager.Instance.ProgressBar.SetProgress(_progress);
}
yield return null;
}
GUIManager.Instance.ProgressBar.End();
}
void LoadFromFile() {
StreamReader reader = new StreamReader(FileName);
string line = reader.ReadLine();
for (int i = 0; (line = reader.ReadLine()) != null && i < MaxRows; i++) {
//...
//some heavy stuff here
//...
lock (_locker) {
_progress = (float)i / MaxRows;
}
}
}
EDIT: It's even simpler than that. It appears that you don't need to lock when doing operations with simple types, because they already are atomic. I just tried without locks and it still works.
Sorry if my question wasn't clear. I now updated it from "how to implement" to "how to update", 'cause that was the real question.
Don't be deceived. Just because a type is a primitive type doesn't mean operations are automatically atomic. For example the i++
operator is not an atomic operation. It's a read-modify-write operator and those 3 steps are not executed as an atomic operation. However you have to think about what can actually go wrong. As soon as two or more threads can write the same value you get an actual race-condition. However when you only have one thread actually writing a value while another one is exclusively reading it you can't get any data corruptions. The worst thing that can happen is that the reading thread doesn't get the latest value in some cases. Though this doesn't matter in the case of a progress value.
Your reasoning is actually a fallacy. Just because it works without a lock doesn't mean it's working in general. That's the main issue with threading that you can't just test run it to verify it's safe. You have to use logical reasoning only. If you can't do this, better be safe than sorry and use a lock.
I didn't say that primitives operations are atomic because I tested it, I wanted to say that I heard they are atomic so I tested it. But yeah, I was incorrect: in CLI, simple reads and writes on primitive types in native word size are atomic, not any operation.
Your answer
Follow this Question
Related Questions
Loading scene with LoadSceneAsync freezes and progress jumps from 0% to 90% 2 Answers
How to make functions async? 2 Answers
Multi-threading Alternative 1 Answer
Problem with Coroutines and waitforseconds. 1 Answer
Reset-Cancel Coroutine in Unity 1 Answer