Async in Unity

Recently, in the process of using Unity, various problems involving asynchronous calls in some high concurrency states have arisen, so there are some doubts about asynchrony in Unity, but in the end, the most important conclusion is first thrown, asynchronous is not equal to multithreading.

First of all, we need to understand that C #in unity is different from C #we usually say. The C # we usually say that his running virtual machine is .Net Framework, while Unity is Mono VM, so the syntax of the two It looks the same, but the actual principle may not be the same, such as await to be understood later in this article, and Mono’s syntax support for C #is only 2.8, and some new C #syntax cannot be used. For the specific comparison of the two and what IL and IL2CPP are, you can refer to this article:Unity将来时:IL2CPP

In fact, when I first saw these two keywords, as a person with a little Web front-end development experience, my first reaction was to compare with JS. In JS, these two keywords started a new coroutine on the main thread of JS. coroutine.

So is this true in dotnet and unity? Are they coroutines? Are they coroutines of the main thread?

If you don’t understand what a coroutine is, you can search for it yourself. I will explain the relationship between processes, threads, and coroutines in a more official language (well, it doesn’t seem to be very simple), which is:

A process is a unit of computer resource scheduling other than the CPU. You apply for a piece of memory and a device, all of which are based on the process. Therefore, the exchange of information between processes requires special processing because they are completely isolated. This process is called Inter-Process Communication.

  • After the process has applied for resources, it is a thread to apply for CPU resources. In theory, core solo CPU is a process that only one thread is running at the same time. The so-called multi-threading means that the CPU may select a thread for execution through a certain scheduling algorithm within a period of time. This switching is automatic by the operating system, so the system needs to automatically maintain the context of the thread executed before, such as which line was executed last time. However, the multi-threads of the unified process share memory, so there is no so-called inter-thread communication.

  • The above switch requires the system to help maintain the context of the thread, which consumes a lot, so there is a way for the thread to maintain the context itself and then switch the execution content, which is called a coroutine. For example, now the CPU has allocated a time slice to me, and I haven’t used it up yet, but the current task is blocked because I need to wait for something, and I don’t want to give up the time slice, so I switch internally. This process does not require system participation.

Coroutine

Keywords

This keyword is not unique to Unity, unity also comes from c #, so it is more appropriate to find an example of c #to understand. First look at the definition of IEnumerator:

1
2
3
4
5
6
public interface IEnumerator
{
bool MoveNext();
void Reset();
Object Current{get;}
}

It can be understood from the definition that an iterator has three basic operations: Current/MoveNext/Reset. Here is a brief description of the process of its operation. In common collections, when we use enumeration operations such as foreach, at the beginning, the enumerator is set before the first element of the collection, and the Reset operation returns the enumerator to this position.

When iterating, the iterator will first execute a MoveNext. If it returns true, it means that there is an object at the next position. Then set Current to the next object at this time, and Current points to the next object at this time. Of course, how c #compiles this IEnumrator into an object example to execute, will be explained below.

Keywords

The yield keyword in C #is followed by two basic expressions:

1
2
yield return <expresion>
yiled break

Yield break is the operation of jumping out of the coroutine, which is generally used where an error is reported or where it is necessary to exit the coroutine.

Yield return is a relatively common expression. The specific expresion can be as follows:

1
2
3
4
WWW: Common web operation, called at the end of each frame, checks isDone/isError and if true, calls MoveNext
WaitForSeconds: Check whether the interval has expired, return true, then call MoveNext
Null: call MoveNext directly
WaitForEndOfFrame: called after rendering, called MoveNext

Well, with an understanding of a few keywords, let’s look at how the C # compiler generates the coroutine call compile that we wrote.

C #compile results for coroutines

1
2
3
4
5
6
7
8
9
10
class Test
{
static IEnumerator GetCounter()
{
for(int count = 0; count < 10; count++)
{
yiled return count;
}
}
}

The result after compiling into C++ is:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
internal class Test 
{
//GetCounter returns an instance object as the result
private static IEnumerator GetCounter()
{
return new <GetCounter>d__0(0);
}

// Nested type automatically created by the compiler to implement the iterator
[CompilerGenerated]
private sealed class <GetCounter>d__0 : IEnumerator<object>, IEnumerator, IDisposable
{
// Fields: there'll always be a "state" and "current", but the "count"
// comes from the local variable in our iterator block.
private int <>1__state;
private object <>2__current;
public int <count>5__1;

[DebuggerHidden]
public <GetCounter>d__0(int <>1__state)
{
//Initial state setting
this.<>1__state = <>1__state;
}

// Almost all of the real work happens here
//Similar to a Finite-State Machine, by switching this state, environment information such as stack during the entire iterator execution can be shared and saved
private bool MoveNext()
{
switch (this.<>1__state)
{
case 0:
this.<>1__state = -1;
this.<count>5__1 = 0;
While (this. < count > 5__1 < 10)//this is for loop processing
{
this.<>2__current = this.<count>5__1;
this.<>1__state = 1;
return true;
Label_004B:
this.<>1__state = -1;
this.<count>5__1++;
}
break;

case 1:
goto Label_004B;
}
return false;
}

[DebuggerHidden]
void IEnumerator.Reset()
{
throw new NotSupportedException();
}

void IDisposable.Dispose()
{
}

object IEnumerator<object>.Current
{
[DebuggerHidden]
get
{
return this.<>2__current;
}
}

object IEnumerator.Current
{
[DebuggerHidden]
get
{
return this.<>2__current;
}
}
}
}

So when we start a coroutine, its essence is to return an iterator instance, and then in the main thread, every time update, this instance will be updated to determine whether it performs the MoveNext operation, if it can be executed (such as file download is completed), then execute MoveNext and assign the next object to Current (MoveNext needs to return true, if false indicates that the iterative execution is completed).

Through this, we can get a conclusion that the coroutine is not multi-threaded, its essence is still executed in the main thread of Unity, and each update will trigger whether to execute MoveNext.

[Unity

This is the official doc of Unity for its coroutine. There are several key points:

  • Any variable or parameter can be correctly kept between yield statements.

  • By default, coroutines will resume on frames after performing yield, but can also be used WaitForSeconds To introduce Time Lag

  • Many tasks in the game need to be performed regularly. The easiest way to think of it is to include the task in the Update function. However, usually, the function will be called multiple times per second. When you don’t need to repeat the task with such frequency, you can put it in a coroutine for regular updates instead of updating every frame
    Coroutines are similar to a life cycle function that is called at a fixed time and cannot be understood as coroutines of the operating system

  • Once the gameobject where a coroutine is located becomes inactive, the coroutine will be interrupted, and even if it becomes active again, the coroutine will not continue

Can be used StopCoroutine And StopAllCoroutines To stop the coroutine. when used SetActive(false) When you disable a coroutine attached to a game object, the coroutine also stops. A call to Destroy (example) (where example is a MonoBehaviour instance) immediately triggers OnDisable and processes the coroutine, effectively stopping it. Finally, OnDestroy is called at the end of the frame.

By putting on a MonoBehaviour instance enabled When set to false to disable MonoBehaviour, the coroutine will not stop.

从多角度分析Unity中的协程

Task

Tasks in C

https://docs.microsoft.com/zh-cn/dotnet/standard/asynchronous-programming-patterns/

https://docs.microsoft.com/zh-cn/dotnet/api/system.threading.tasks.task?view=net-6.0

Simply put, Task just indicates that this thing is asynchronous, it does not necessarily start another thread to do it

Moreover, this Task, whether it is used or maintained in its internal state, is very similar to the JS Promise.

Await is used in conjunction with Task, await is Task, and async indicates that there will be await in this function.

Unity中的Task

Compared with C #Task, Unity Task has a feature that the context before and after await is the context of the main thread, which ensures that it can be called to Unity’s API, so it also brings some problems

https://zhuanlan.zhihu.com/p/86168785