This is a creation in Article, where the information may have evolved or changed.
Select can be used to manage multiple channel reads and writes, as well as channel read-write timeout. Select is not provided as a library, but is a language-level supported syntax feature, so the implementation of select is primarily done by the compiler and runtime, and this article focuses on the runtime section.
The execution of a SELECT statement consists primarily of 4 phases, in order to create a Select object, register all case conditions, execute a SELECT statement, and finally release the Select object. The Select object mentioned here is a select structure maintained by the underlying runtime, which is basically transparent to the GO program. In the following section, I will call this select object as 选择器
.
Selector memory model (SELECT)
The memory model here mainly describes how the selectors are laid out in memory and what data structures are maintained. The source code is in runtime/chan.c, and the function describing the memory model is mostly newselect
. Newselect is to create a selector in memory.
The two most important structural bodies that describe the selector memory model are defined as follows:
structScase{SudoGsg;// must be first member (cast to Scase)Hchan*chan;// chanbyte*pc;// return pcuint16kind;uint16so;// vararg of selected boolbool*receivedp;// pointer to received bool (recv2)};structSelect{uint16tcase;// total count of scase[]uint16ncase;// currently filled scase[]uint16*pollorder;// case poll orderHchan**lockorder;// channel lock orderScasescase[1];// one per case (in order of appearance)};
Scase
Describes the case condition defined in the GO Program SELECT statement, which means that a case in the GO program is maintained using the SCASE structure in runtime. You can see that Scase
there is a Hchan *chan
field, which is clearly the channel that operates on each case condition.
Select
is to define the core structure of the selector, and each field is of course important, but you can focus on the Pollorder, Lockorder, scase three fields. Here is a look at the Scase scase[1]
definition of this field, you can guess that the Scase field is used to store all the case conditions, but here is just a definition of an array of only one element, how can this be enough to store the extra 1 cases???
This graph is the memory model of the entire selector, this whole block of memory structure is also 头部结构
数据结构
composed of +, the head is the select part, corresponding to the above mentioned struct Select
, the data structure part is composed of arrays.
scase
is an array, the array elements are scase types, and each case condition is stored.
lockorder
The pointer is also an array, the element is Hchan *
type, and the channel is stored in each case condition.
pollorder
is an array of uint16.
Starting from the head this whole block of memory is allocated by the one-time class malloc (why is the class malloc, because go has its own memory management interface, not an ordinary malloc) call assigned, The Lockorder and Pollorder two pointers in the select header structure are then pointed to the correct position respectively. Of course, before you can allocate this memory in one breath, it is the size of all the memory that needs to be calculated beforehand. This particular emphasis is placed on the need for malloc to allocate all the required memory, just to express what language in addition to C + + has such a strong memory control capability? Other languages (including go) in dealing with this situation, the technique should almost always be the first new object, and then the object needed in the main object field. Of course, you might tell me that this language has a good memory management system and doesn't care about creating such objects ... Oh. The full control of the memory is also the reason why the system software is written in C + +. The problem cannot continue to be torn down.
The problem with the Scase field being defined as an array of 1 elements has not been resolved. Showing a selector memory model with 6 case conditions, you can see that Lockorder, Pollorder, and Scase (the black part ) are arrays of 6 cells. note that the first cell of the scase in the Black section is located in the memory space of the select head structure, which is the struct Select
scase array of only one element defined in, and when malloc allocates this memory, Scase just need to allocate a unit less, so it can be seen that only 5 additional scase storage units. In this way, the Scase field and Lockorder, Pollorder are no different, the same. In fact, the Scase scase[1]
field can be completely defined as well Scase *scase
, but this will waste a lot of memory space of a pointer. I still prefer this kind of deduction byte-type implementation way.
After creating this memory space in the Newselect function, we can no longer find the process of populating the Scase, Lockorder, and pollorder three arrays, that is, the creation of a good memory model is over, not yet filled with data, what is this? The fill selector is actually the process of registering a case.
Here, the selector is created, and the rest is how the selector works.
Registering a case condition
Understand the memory layout of the selector, that is, create a selector, and then see how to register all case condition data into the selector, focus on the two functions:
static voidselectsend(Select *sel, Hchan *c, void *pc, void *elem, int32 so){i = sel->ncase;……………..sel->ncase = i+1;cas = &sel->scase[i];cas->pc = pc;cas->chan = c;cas->so = so;cas->kind = CaseSend;cas->sg.elem = elem;}
This selectsend function is called when the case condition is written to the channel. It will pass the data of this case on the Go program and the channel information to the selector, fill in the specific scase structure, complete the channel of the letter to register.
static voidselectrecv(Select *sel, Hchan *c, void *pc, void *elem, bool *received, int32 so){i = sel->ncase;sel->ncase = i+1;cas = &sel->scase[i];cas->pc = pc;cas->chan = c;cas->so = so;cas->kind = CaseRecv;cas->sg.elem = elem;cas->receivedp = received;}
Selectrecv and Selectsend are like, it is called when the case condition is read from a channel to complete the case registration of the Read channel. Also, the channel and the memory that holds the data are passed to the selector in advance, populated in a scase. Because it is waiting for the data to be read, the memory address of the stored data is given to the selector, and then the selector copies the data into the memory after it has been fetched from the channel to the data.
The registration process for case conditions is particularly simple, with no complicated content, but this part is actually very relevant to the compiler, such as only one case of select can be optimized to directly manipulate the channel.
Execute Selector
This section is the core of select, which mainly includes how the selector manages the case condition and how to read and write the corresponding channel.
The interaction of the selector and channel is selectgo()
implemented by this function, which is a little bit long, but the process is actually very simple. A code skeleton for this function is affixed below.
Static Void*selectgo (Select **selp) {sel = *selp;//here is a very important place, the Pollorder array fills in the number of each case sequentially [0, N], then the second for IS//is a shuffle operation, Randomly scrambles the numbers in the Pollorder array. The purpose of course is to implement the case condition of the//randomness. for (i=0; i
Ncase; i++) Sel->pollorder[i] = i;for (i=1; i
Ncase; i++) {...}//here again two loops to traverse all the case, the egg hurts. The thing to do this time is to sort the elements in the Lockorder//. Note that the elements in the Lockorder array are the address of the channel corresponding to each case. for (i=0; i
Ncase; i++) {...}....} for (i=sel->ncase; i-->0;) {.........} Sellock is to iterate through the Lockorder array, and then add the lock to each channel in the array, since it is not known which channel to//to operate, simply add all of them.?? It's violent. The above for the purpose of lockorder sorting out//, is convenient here to lock, the channel in the Lockorder to heavy. Because two case is completely possible to simultaneously//make the same channel, the Lockorder may store duplicate channel. Sellock (SEL);//Come here, finally finish the preparation work, will begin to really work. loop://this for loop is to traverse all the case in the order of Pollorder, and break the loop after encountering a case that can be executed. The number in the Pollorder has been shuffled in the initialization phase, so a case can be executed randomly. for (i=0; i
Ncase; i++) {o = Sel->pollorder[i];cas = &sel->scase[o];.....switch (cas->kind) {case Caserecv:........case CaseSend: ... . Case casedefault: ...}} No case could be found, but with the default condition, the if will exit directly. if (DFL! = nil) {...} Here, there is no case to be executed, and no default condition. The current goroutine is queued to the waiting queue of the channel corresponding to each case. The waiting queue for the channel is described in detail in the//channel implementation. for (i=0; i
Ncase; i++) {... switch (cas->kind) {case caserecv:enqueue (&C->RECVQ, SG); Break;case Casesend:enqueue (&c- >SENDQ, SG); break;}} After the team is finished, the current goroutine is suspended waiting for a case to execute. It also unlocks all//channel locking. Runtime Park ((void (*) (lock*)) Selunlock, (lock*) sel, "select");//The current goroutine is awakened to start execution, and again locks all channel. or violence. Sellock (SEL);//This is an interesting for loop that iterates through the case. Here is the case that this select will not execute corresponding//channel gives the team the current goroutine. It is no matter what they are, we have found a target case for execution. for (i=0; i
ncase; i++) {cas = &sel->scase[i];if (cas! = (scase*) sg) {c = cas->chan;if (Cas->kind = = casesend) Dequeueg (&c-> SENDQ); Elsedequeueg (&C->RECVQ);}} Or did not find the case, re-loop the execution again. This should be the case that Goroutine was awakened by a number of other factors. if (sg = = nil) goto loop;...........//unlocks the exit and completes the execution of the Select. Selunlock (SEL); goto retc;//These goto tags are the process of operating the channel specifically for each case. and the channel implementation of the similar introduction. Asyncrecv:.........goto Retc;asyncsend:................goto Retc;syncrecv:....................goto retc;syncsend:.................//Select executes the exit, Not only is the selector object released, it also returns to the PC. This PC is the address of the case//that this select executes. Only return this stack address to continue executing the statement in the case condition. RETC:PC = Cas->pc;runtime free (sel); return pc;}
Although just a code skeleton, but also very long, estimates are only a comparison of the source code to better understand. In short, the execution logic of select is a little bit more complicated. At first glance, not particularly good understanding. Let me summarize some of the areas I think I should know:
- The execution of the SELECT statement will lock all the channel involved, not just the channel that needs to be manipulated.
- Prior to all channel locking, there was a process of heap sequencing of all the channel involved, in order to be heavy.
- Select does not randomly select an executable case, but instead shuffle all the case in advance and select the first executable case from beginning to end.
- If the SELECT statement is placed in a for loop for long execution, will each cycle experience the creation of the selector to 4 stages of release??? I can clearly tell you that this is necessarily the case, so the use of select is a cost, not low.
The core of the implementation of select is actually finished, the optimization of the space should be quite a lot.
Compiler optimizations
Although the compiler is not understood, it is probably a glance at the implementation portion of the compiler for select. If you look at the comments in the CMD/GC/SELECT.C code, you can see that the compiler actually has some optimizations for select. For example, if you write a select without any case conditions, then create a selector for what, or if you have only one instance condition, this obviously can be used without a select, even if there is a case+default, it can be optimized for non-blocking direct operation Channel AH.
This part of the compiler-related optimizations can look at the functions in detail walkselect()
.
Note: This article is based on the go1.1.2 version code.