Cocos2d-x multithreading and asynchronous loading implementation detailed explanation

Cocos2d-x is a single-threaded loop engine. The engine updates the state of each element in the game between each frame to ensure that they do not interfere with each other. Although the program seems to be running in parallel during this process, It is actually a serial process.

For example, when the game is in a scene jump, we usually release the resources of the current scene and load the resources of the next scene, so we need to load the textures needed by the next interface into the memory. This process requires Read and write operations on resource files, and this external storage operation is very time-consuming. If the amount of images that need to be loaded is large and the resolution is high, it is likely to cause our main thread to block, because the processor cannot be In such a short time interval (default frame rate 1/60) to complete such a large amount of calculation, and because there is only one thread that does not interrupt the current execution content to execute other content, so at this time we will observe the interface The frame rate dropped sharply and even the interface directly stuck.

In order to avoid such problems, Cocos2d-x provides developers with asynchronous loading functions in the engine. We can send an asynchronous file loading request to TextureCache, and TextureCache will help us create a new thread to complete the time-consuming process. Load texture operation, and we can continue to perform other calculations in our main thread.

In addition to resource loading, network reads and writes are also one of the common time-consuming operations, so the use of threads in client/server systems is also a common phenomenon, such as the asynchronous function in HttpClient.

2. Single-core and multi-core

Single-core means only one processor, and multi-core means multiple processors. Our current mobile devices are generally dual-core or quad-core, such as iPhone6 ​​and Samsung note4. Older devices such as iPhone4 have single-core CPUs. What I want to explain here is the difference between single-core multi-threading and multi-core multi-threading.

Multithreading in a single-core device is concurrent.

Multithreading in multi-core devices is parallel or concurrent.

Let’s explain the meaning of these two sentences. Single-core dual-threading is a very common practice. For example, we write a code with multiple threads and let it run on iphone4. Since iphone4 has only one processor, In fact, the new thread we created and the main thread are in a state of interleaved operation. For example, if we divide the time slice into 100 milliseconds, then the program executes the main thread in the current 100 milliseconds, and the program in the next 100 milliseconds It may execute another thread and return to the main thread after 100 milliseconds. The advantage of this is that it will not delay a thread indefinitely. Once the time slice is reached, the program will forcibly interrupt the current thread to execute Another thread. In this way, at a macro level, they seem to be executed at the same time, but in fact, they are still executed separately.

However, if this code is put on Samsung note4 to execute, note4 has a 4-core cpu. On this multi-processor device, our two threads can occupy one processor for each thread and execute independently. That is to run at the same time without the need to run interleaved. Such a state is called a parallel state. Therefore, concurrency is actually a pseudo-parallel state, it is just a state of pretending to perform multiple operations at the same time.

3. Thread safety issues

First we come to understand a concept, thread safety.

Thread safety means that the code can be called by multiple threads without disastrous results. Here we give a simple example to illustrate (here the author uses the thread function format of POSIX threads, just understand the general meaning)

staticintcount = 0; // count is a static global variable

    //A method thread function of thread 1

    void* A(void* data){

        while(1) {

            count += 1;

            printf(“%d\n”,count);

        }

    }

    //B method thread function of thread 2

    void* B(void* data){

        while(1) {

            count += 1;

            printf(“%d\n”,count);

        }

As shown in the above code, suppose we have started two threads now, and the thread functions of the two threads are set to A and B respectively (the thread functions are written separately for the sake of understanding, in fact, a thread function is written for two threads to execute That’s enough), the console output we expect after running the program is, 123456789……. (This code may not have any meaning for realizing the function…. Here is just an example)

But in fact, the result of running may not be the case. What we expect is that in each thread function, the count value is increased by one at a time, and then the count is output. However, because the execution order of different threads is unpredictable, the above code It is very likely that this situation (assuming the device is a single core): the initial value of count is 0, now it is the turn of thread 1 to run, and the execution of count +=1 in A, then the value of count is already equal to 1, which should be output afterwards 1, but the time slice just ended here, and now it is switched to thread 2. At this time, the value of count in thread 2 is already 1, and now it is increased by 1 again to become 2, and then the print statement is executed. Output a 2, and then the time slice ends, and returns to 1, and 1 continues to execute the output statement that was not executed just now, but because the count value has been changed again by thread 2, we may see the output on the screen at this time It is 223456789....

Of course, the situation I said may not necessarily occur. The reason is that the execution order of different threads is unpredictable, and each execution will produce different results. Perhaps the output is normal in most cases. This example just tells everyone that threads are not safe in this case.

So how to solve the above problems?

First of all, the count variable is a shared data for two threads, so there may be problems when two threads access this shared data at the same time. For example, in the previous code, the count value that thread 1 wants to output is 1, but because thread 2 changed this value when thread 1 did not output count, but thread 1 did not know that the count value was changed and then continued to execute the output. As a result, the value output by thread 1 was 2.

The most common way to solve this problem is to "synchronize" the threads. Note that the synchronization here does not mean letting the threads run together in unison. The thread synchronization we call refers to letting the threads run in order. You run it first, and I will run it again.

The most common way to use thread synchronization is to make the memory access of the same data "mutually exclusive". To explain with our above example, when thread 1 is performing count addition and output operations, thread 2 is not allowed to access count. At this time, count2 can only be in a blocked state, and wait for the operation of count in thread 1 to complete Later, thread 2 can access, only one thread is allowed to write data at a time, and other threads can only wait.

For example, suppose you and I each represent a thread, and now I want to perform an operation, go to the toilet, after I enter the toilet, in order to prevent you from trying to occupy the test, I will lock the toilet door, if You also want to use the toilet at this time, so you can only use the toilet at the door after I finish unlocking and leaving. The lock here is like what we call a mutex (mutex). We can ensure that only one thread can manipulate these data within a certain period of time by locking and unlocking the mutex.

The mutex type in pthread is represented by pthread_mutex_t, and std::mutex can be used in C++11.

For example, the code just now can be written as:

 staticintcount = 0; // count is a static global variable

    /* Protect the mutex for count operations, <span style=”font-family: Arial, Helvetica, sans-serif;”>THREAD_MUTEX_INITIALIZER is a special value for initializing mutex variables </span>*/

    pthread_mutex_t count_mutex = PTHREAD_MUTEX_INITIALIZER;

    //A method thread function of thread 1

    void* A(void* data){

        while(1) {

            /* Lock the mutex that protects the count operation. */

            pthread_mutex_lock (&count_mutex);

            count += 1;

            printf(“%d\n”,count);

            /* The processing of the count operation has been completed, so unlock the mutex. */

            pthread_mutex_nlock (&count_mutex);

        }

    }

In addition to mutexes, synchronization tools also have semaphores and condition variables. Although mutexes can sometimes meet our needs, they waste a lot of time. Using these tools can help us achieve more complex control modes.

4. Precautions for using multi-threading in Cocos2d-x

The memory management mechanism used by Cocos2d-x and the OpenGL interface functions are not thread-safe. Therefore, do not try to call the memory management method provided by the engine in a thread other than the main thread, such as in a new thread To create an element such as a sprite or a layer, these elements will call autorelease in the create() method. Autorelease, retain and release are not thread-safe, and the OpenGL context is not thread-safe, so don’t Use the OpenGL drawing function in the new thread.

5.pthread multithreading

pthread is a multi-threaded library, the full name is POSIX threads, because its API follows the official international standard POSIX. The pthread thread library is developed by the C language and can run on multiple platforms, including Andoird, iOS, and Windows. All thread functions and data types in pthread are declared in the <pthread.h> header file, which is also before Cocos2d-x Recommended multi-threaded library. Nowadays, after the introduction of C++11 features in 3.x, the reference of the pthread library is cancelled, and we can use the standard library thread for multi-threaded programming.

6. Asynchronous loading

The author's development environment is Xcode+Cocos2d-x 3.3beta0 version, let's briefly understand the process of asynchronous loading.

We can use a Loading interface to implement pre-loading of resources. Only after all resources are loaded into memory, when we use Sprite and ImageView to create objects, it will not cause the phenomenon of lag. So how to load the image asynchronously into the memory, Cocos2d-x provides us with the addImageAsync() method, which is located in the TextureCache class. Let's take a look at the work done in this method

/* Asynchronously add texture The parameters are the resource path of the image and the callback function to notify after loading */

voidTextureCache::addImageAsync(conststd::string &path, conststd::function<void(Texture2D*)>& callback)

{

    //Create a texture object pointer

    Texture2D *texture = nullptr;

    //Get resource path

    std::string fullpath = FileUtils::getInstance()->fullPathForFilename(path);

    //If this texture has been loaded, return

    auto it = _textures.find(fullpath);

    if( it != _textures.end() )

        texture = it->second;//second is the value in key-value

    if(texture != nullptr)

    {

        //After the texture is loaded, directly execute the callback method and terminate the function

        callback(texture);

        return;

    }

    // The first time the asynchronously loaded function is executed, the queue that saves the message structure needs to be initialized

    if(_asyncStructQueue == nullptr)

    {

        //The release of the two queues will be completed in addImageAsyncCallBack

        _asyncStructQueue = newqueue<AsyncStruct*>();

        _imageInfoQueue   = newdeque<ImageInfo*>();

        // Create a new thread to load the texture

        _loadingThread = newstd::thread(&TextureCache::loadImage, this);

        //Whether to exit the variable

        _needQuit = false;

    }

    if(0 == _asyncRefCount)

    {

        /* Register an update callback function with the Scheduler

           Cocos2d-x will check the loaded texture in this update function

           Then process a texture every frame and cache the texture information in TexutreCache

         */

        Director::getInstance()->getScheduler()->schedule(schedule_selector(TextureCache::addImageAsyncCallBack), this, 0, false);

    }

    //The number of texture data loaded asynchronously

    ++_asyncRefCount;

    //Generate a message structure for asynchronously loading texture information

    AsyncStruct *data = new(std::nothrow) AsyncStruct(fullpath, callback);

    //Add the generated structure to the queue

    _asyncStructQueueMutex.lock();

    _asyncStructQueue->push(data);

    _asyncStructQueueMutex.unlock();

    //Unblock the thread to indicate that there is an empty position

    _sleepCondition.notify_one();

}

In this code, an addImageAsyncCallBack method is involved. This method is used to check the texture after the asynchronous loading is completed. It will be turned on when addImageAsync is called for the first time:

voidTextureCache :: addImageAsyncCallBack (floatdt)

{

    // _imageInfoQueue double-ended queue is used to save the loaded texture in the new thread

    std::deque<ImageInfo*> *imagesQueue = _imageInfoQueue;

    _imageInfoMutex.lock(); //lock the mutex

    if(imagesQueue->empty())

    {

        _imageInfoMutex.unlock(); //The queue is empty to unlock

    }

    else

    {

        ImageInfo *imageInfo = imagesQueue->front(); //Remove the first element image information structure

        imagesQueue->pop_front();//Delete the first element

        _imageInfoMutex.unlock();//Unlock

        AsyncStruct *asyncStruct = imageInfo->asyncStruct;//Get asynchronously loaded message structure

        Image *image = imageInfo->image;//Get Image pointer to generate OpenGL texture map

        conststd::string& filename = asyncStruct->filename;//Get the resource file name

        //Create texture pointer

        Texture2D *texture = nullptr;

        //Image pointer is not empty

        if(image)

        {

            // Create a texture object

            texture = new(std::nothrow) Texture2D();

            //Generate OpenGL texture from Image pointer

            texture->initWithImage(image);

#if CC_ENABLE_CACHE_TEXTURE_DATA

            // cache the texture file name

            VolatileTextureMgr::addImageTexture(texture, filename);

#endif

            // Cache texture data

            _textures.insert( std::make_pair(filename, texture) );

            texture->retain();

            //Join the automatic release pool

            texture->autorelease();

        }

        else

        {

            auto it = _textures.find(asyncStruct->filename);

            if(it != _textures.end())

                texture = it->second;

        }

        //Get the function that needs to be notified after loading is completed and notify

        if(asyncStruct->callback)

        {

            asyncStruct->callback(texture);

        }

        //Release image

        if(image)

        {

            image->release();

        }

        //Release two structures

        deleteasyncStruct;

        deleteimageInfo;

        //Decrease the number of loaded textures by one

        –_asyncRefCount;

        /* All files are loaded, logout callback function */

        if(0 == _asyncRefCount)

        {

            Director::getInstance()->getScheduler()->unschedule(schedule_selector(TextureCache::addImageAsyncCallBack), this);

        }

    }

}

There are still many details that will not be analyzed in detail. After the load is successful, we only need to set the percentage of the progress bar in the callback function specified when calling the addImageAsync() method. E.g:

boolHelloWorld :: init ()

{

    //

    // 1. super init first

    if( !Layer::init() )

    {

        returnfalse;

    }

    /* Load texture asynchronously */

    for (inti = 0; i <10; i ++) {

        Director::getInstance()->getTextureCache()->addImageAsync(“HelloWorld.png”, CC_CALLBACK_1(HelloWorld::imageLoadedCallback, this));

    }

    returntrue;

}

voidHelloWorld::imageLoadedCallback(Ref* pSender)

{

    //Every time you load a texture successfully, you can set the progress of the progress bar in the callback method here, and then jump to the interface when all textures are loaded.

}

Guess you like

Origin blog.csdn.net/qq_21743659/article/details/108637362