Coroutine in Skynet

Skynet is essentially a message distributor, serving a single service, can send a message from any service to another from any service.

On this basis, we access the Lua virtual machine in the service and encapsulate the API of the message into the LuA module. The service written with Lua is only one entry in the bottom layer, which is the message that receives and processes a SKYNET framework forward. We can pass SkyNet.core.Callback (this is written with C, usually by Skynet.Start call) to set a Lua function to the service module. Each service must be set and can only set a callback function. This callback function receives 5 parameters: message type, message pointer, message length, message session, message source.

The news is roughly divided into two categories. One type is the request you launched by others. One is the response you have received by you in the past. No matter which type, it is entered through the same callback function.

When you actually use the SKYNET, you can directly use the RPC syntax, initiate a remote call to the external service, and wait for the other party to send a response message, logic will follow. So, how is the framework converts the mode of the callback function to the form of blocking API calls?

This is a lot of lua supports Coroutine. You can hang when a code is running half, and then running in the following time.

To achieve this, we need to create a COROUTINE when you receive each request message, to run the DISPATCH function of such messages in COROUTINE (when using the framework, you can use the Skynet.dispatch to set the message). The reason why you must first create COROUTINE without calling message processing functions, because we cannot predict that the execution process will be suspended during the message processing. Wait until the first time I need to hang, it is not possible to bind the execution process to COROUTINE.

All blocks are then hanging current Coroutine via Coroutine.Yield, and pass the suspend type and the data that may be used. The framework will catch these parameters, and it will further know what to do. This also explains why the blocking API must call in the message processing function, and cannot directly write the reason for the main code of the service. Because the code of the initialization section does not run in the COROUTINE created in the frame, Coroutine.Yield is now unwinding.

For example, for SkyNet.Call, it is actually generated a SESSION number called the current service call Yield to send “Call” instruction to the frame. After the Resume in the frame, the session and the COROUTINE object will be recorded in the table and then hang the COROUTINE to end the current callback function. Wait a follow-up message after the SkyNet underlayer framework is processed. (In fact, here will also handle additional threads created by SkyNet.fork)

When a response message is received, it will find the previously recorded COROUTINE object according to the SESSION number. Then Resume has not been completed before. From the application layer angle, it is just a blocking call.

The above is just a way to convert callbacks into blocking calls with Coroutine when the SKYNET is built. It is not the only solution. For example, you can also pass any data to the frame without passing Yield, all plug in additional table. The scheduler is enough to end the current callback function after receiving Yield. But if this is done, you will encounter some small trouble when you meet the following needs (you may need to get some extra global marker variables):


What if we want to use the COROUTINE library in the message function under the SkyNet framework?

That is to use the SkyNet framework and normal Coroutine mechanism. If you use Coroutine directly, all SkyNet blocking APIs cannot work properly. They will trigger Yield, and the Resume written by the user is captured, and cannot be handled correctly.

At this time, we introduced SkyNet.coroutine library. You can use SkyNet.Coroutine to fully replace Lua native COROUTINE libraries, the API is consistent.

What it do is in Yield Coroutine, plus a “user” type in front of the outgoing parameters (which is in the Skynet framework, all Yield must give the first parameter to give a hanging type) When RESUME COROUTINE, once it is found to be “User” type, then the type value is removed, and the remaining parameters are returned directly, prevent the external framework end message processing; and if it is other type, spread up to the framework, Let the frame hang up the current message processing process and wait for the underlying response to continue. This kind of Coroutine that allows the application layer does not appear to be interrupted by the obstruction of the SKYNET framework. Of course, this also needs to add a COROUTINE state to SkyNet.Coroutine.status in the “Normal” “Suspended” “DEAD”, “DEAD”, called “blocked”. It means that Coroutine hangs the underlying frame, but it is not possible from the reference layer Resume.

This status is similar to “Normal” and is a special case under the Skynet framework. Because two separate request message processing flows under the Skynet framework can be considered parallel processing threads. The data between threads is shared, which means that the COROUTINE object created by a thread is visible to another thread, or the resume can also be called. The “Blocked” state can prevent errors from calling.

Realizing SkyNet.coroutine packages is not complicated, the biggest difficulty inside is actually analyzing the time analysis of Pofile to the SkyNET thread. PROFILE will pause the timing when Yield, and continue at Resume. This is likely to correctly count the full process of request to consume much CPU time. The introduction of user Coroutine increases the complexity of statistics. We need to track the call of Resume, which message is back to the end, indirectly execute the code in COROUTINE, in order to correctly add it. Students who are interested in understanding details can read the code directly.

Generally we don’t need to use the Coroutine module directly. Skynet itself provides a SkyNet.Fork () method to create a new business thread, you can hang with SkyNet.Wait (Co) and wake up with SkyNet.wakeup (Co). The difference is that Wakeup only sends a signal to the frame, and needs to wait for the frame to schedule; and unlike Coroutine.Resume (Co), it will continue to pending Coroutine.


So when can I need to use the COROUTINE module?

I think the biggest use is to use COROUTINE as an iterator. There is a good example in the PIL.

Just half a year ago I also encountered an actual demand to use Coroutine to realize iterators, and share this case (not big in SkyNet).

Since Lua-Customized Memory Manager provides more information than the standard CRT’s memory management API agre, the memory-use mode of memory of Lua VM itself can be predicted, so it is possible to customize a better memory management than general purpose dispensers. . (There are additional significance for SkyNet: you can use different Lua VMs to use multiple blocks of different blocks, reduce memory fragmentation when VM is closed)

The memory manager is easy to write, and if you want to do, it is not so easy to determine if it is really efficient. So I thought of a way to make the data used.

I have customized a special memory manager in the actual operation of the project, and the log has all memory manager calling behavior. After the actual project is running a long period of time, it has been a number of groups, and the data of each set of gs. These data strictly reacted how the memory is in use during operation.

I can use this data to strictly test the custom memory management module, which will use the online product to completely consistently use. I can add an additional check, that is, fill the unique data after the memory is assigned, and strictly check the fill when it is released. You can also detect the fragmentation rate, peak memory, and the like. Of course, it is also possible to turn over a higher level after turning off the detection. Or doing algorithms for the project to be fine-tuning to achieve results.

However, it is not easy to use this LOG data directly. The log is recorded in the address information of each memory and release. If I built a big Hash table in the test code, it is very expensive to save them, this overhead is likely to have an impact on the performance measurement of the memory distributor. Because the memory distribution module itself runs very quickly, even if the implementation of the Hash table is faster. And if it is a dynamic HASH table, it itself needs to use a memory management function so that the interference is even more. I tend to build a sufficiently static array in the test function and read the test data in streaming. In theory, the size of the array does not exceed the number of memory block entries in the actual operation process. Just do a little processing on the original log data, the memory address is converted to the serial number in the array.

The test program reads the processed log, just know that a memory allocation request should be placed in a second in the static array, and the release request should release the first few items in the static array. This is the least interference of the test itself.

My task is to process the original log.

At the beginning I thought is relatively simple, these logs will be converted once, just write a script, just. It is nothing more than loading the entire log into a Lua Table, and the address is converted into a number.

When I did it, I found that I have encountered a little bit of trouble: log file is really too big, processed quickly exceeded my physical memory limit, becoming very slow. Then I thought I used a COROUTINE to make an iterator, and the source data stream is handled while switching, while outputting.

Although this can also be done with Coroutine, it will be troublesome. Complete these requirements very natural with Coroutine. If you don’t mind letting I read the script I wrote, I can find them on gist.