Explicación detallada de la implementación de carga asíncrona y multiproceso de Cocos2d-x

Cocos2d-x es un motor de bucle de un solo subproceso. El motor actualiza el estado de cada elemento del juego entre cada cuadro para asegurarse de que no interfieran entre sí. Aunque el programa parece ejecutarse en paralelo durante este proceso, En realidad, es un proceso en serie.

Por ejemplo, cuando el juego está en un salto de escena, generalmente liberamos los recursos de la escena actual y cargamos los recursos de la siguiente escena, por lo que necesitamos cargar las texturas necesarias para la siguiente interfaz en la memoria. Este proceso requiere Operaciones de lectura y escritura en archivos de recursos, y esta operación de almacenamiento externo consume mucho tiempo. Si la cantidad de imágenes que deben cargarse es grande y la resolución es alta, es probable que nuestro hilo principal se bloquee, porque el procesador no puede ser En un intervalo de tiempo tan corto (frecuencia de cuadro por defecto 1/60) para completar una cantidad tan grande de cálculo, y porque solo hay un hilo que no interrumpe el contenido de ejecución actual para ejecutar otro contenido, por lo que en este momento observaremos la interfaz La velocidad de fotogramas se redujo drásticamente e incluso la interfaz se atascó directamente.

Para evitar tales problemas, Cocos2d-x proporciona a los desarrolladores funciones de carga asíncrona en el motor. Podemos enviar una solicitud de carga de archivo asíncrona a TextureCache, y TextureCache nos ayudará a crear un nuevo hilo para completar el proceso que consume mucho tiempo. Cargue la operación de textura y podremos continuar realizando otros cálculos en nuestro hilo principal.

Además de la carga de recursos, las lecturas y escrituras de red también son una de las operaciones comunes que consumen mucho tiempo, por lo que el uso de subprocesos en sistemas cliente / servidor también es un fenómeno común, como la función asincrónica en HttpClient.

2. De un solo núcleo y de varios núcleos

Un solo núcleo significa un solo procesador y multinúcleo significa varios procesadores. Nuestros dispositivos móviles actuales son generalmente de dos o cuatro núcleos, como iPhone6 ​​y Samsung note 4. Los dispositivos más antiguos, como iPhone4, tienen CPU de un solo núcleo. Lo que quiero explicar aquí es la diferencia entre subprocesos múltiples de un solo núcleo y subprocesos múltiples de varios núcleos.

El subproceso múltiple en un dispositivo de un solo núcleo es simultáneo.

El subproceso múltiple en dispositivos de varios núcleos es paralelo o concurrente.

Expliquemos el significado de estas dos oraciones. El subproceso dual de un solo núcleo es una práctica muy común. Por ejemplo, escribimos un código con varios subprocesos y dejamos que se ejecute en iphone4. Dado que el iphone4 solo tiene un procesador, De hecho, el nuevo hilo que creamos y el hilo principal están en un estado de operación intercalada. Por ejemplo, si dividimos el intervalo de tiempo en 100 milisegundos, el programa ejecuta el hilo principal en los 100 milisegundos actuales y el programa en los siguientes 100 milisegundos. Puede ejecutar otro hilo y volver al hilo principal después de 100 milisegundos. La ventaja de esto es que no retrasará un hilo indefinidamente. Una vez que se alcanza el intervalo de tiempo, el programa interrumpirá por la fuerza el hilo actual para ejecutar Otro hilo. De esta forma, a nivel macro, parecen ejecutarse al mismo tiempo, pero de hecho, todavía se ejecutan por separado.

Sin embargo, si este código se coloca en Samsung note4 para que se ejecute, note4 tiene una cpu de 4 núcleos. En este dispositivo multiprocesador, nuestros dos subprocesos pueden ocupar un procesador para cada subproceso y ejecutarse de forma independiente. Eso es correr al mismo tiempo sin la necesidad de ejecutar intercalado. Tal estado se llama estado paralelo. Por lo tanto, la concurrencia es en realidad un estado pseudo-paralelo, es solo un estado de pretender realizar múltiples operaciones al mismo tiempo.

3. Problemas de seguridad de los hilos

Primero llegamos a comprender un concepto, seguridad de subprocesos.

La seguridad de subprocesos significa que el código puede ser llamado por varios subprocesos sin resultados desastrosos. Aquí damos un ejemplo simple para ilustrar (aquí el autor usa el formato de función de hilo de hilos POSIX, solo entienda el significado general)

staticintcount = 0; // count es una variable global estática

    // Una función de hilo de método del hilo 1

    void * A (void * datos) {

        while (1) {

            contar + = 1;

            printf ("% d \ n", cuenta);

        }

    }

    // Función del hilo del método B del hilo 2

    void * B (void * datos) {

        while (1) {

            contar + = 1;

            printf ("% d \ n", cuenta);

        }

Como se muestra en el código anterior, suponga que hemos iniciado dos subprocesos ahora, y las funciones de subproceso de los dos subprocesos están configuradas en A y B respectivamente (las funciones de subproceso se escriben por separado para entender, de hecho, una función de subproceso se escribe para que dos subprocesos se ejecuten Eso es suficiente), la salida de la consola que esperamos después de ejecutar el programa es, 123456789 ……. (Este código puede no tener ningún significado para realizar la función…. Aquí es solo un ejemplo)

Pero, de hecho, el resultado de la ejecución puede no ser el caso. Lo que esperamos es que en cada función de subproceso, el valor de conteo aumente de uno en uno, y luego se emite el conteo. Sin embargo, debido a que el orden de ejecución de diferentes subprocesos es impredecible, el código anterior Es muy probable que esta situación (suponiendo que el dispositivo sea un solo núcleo): el valor inicial de count es 0, ahora es el turno del hilo 1 para ejecutarse, y la ejecución de count + = 1 en A, entonces el valor de count ya es igual a 1, que debería salir luego 1, pero el segmento de tiempo acaba de terminar aquí, y ahora se cambia al subproceso 2. En este momento, el valor del recuento en el subproceso 2 ya es 1, y ahora se incrementa en 1 nuevamente para convertirse en 2, y luego se ejecuta la declaración de impresión. Salida a 2, y luego finaliza el segmento de tiempo, y vuelve a 1, y 1 continúa ejecutando la declaración de salida que no se ejecutó hace un momento, pero debido a que el valor de conteo ha sido cambiado nuevamente por el hilo 2, podemos ver la salida en la pantalla en este momento. Es 223456789 ....

Por supuesto, la situación que dije puede no ocurrir necesariamente. La razón es que el orden de ejecución de diferentes subprocesos es impredecible, y cada ejecución producirá resultados diferentes. Quizás la salida sea normal en la mayoría de los casos. Este ejemplo solo les dice a todos que los hilos no son seguros en este caso.

Entonces, ¿cómo resolver los problemas anteriores?

En primer lugar, la variable de recuento es un dato compartido para dos subprocesos, por lo que puede haber problemas cuando dos subprocesos acceden a estos datos compartidos al mismo tiempo. Por ejemplo, en el código anterior, el valor de recuento que el subproceso 1 quiere generar es 1, pero porque el subproceso 2 cambió este valor cuando el subproceso 1 no emitió el recuento, pero el subproceso 1 no sabía que el valor del recuento se cambió y luego continuó ejecutando la salida. Como resultado, el valor de salida del subproceso 1 fue 2.

La forma más común de resolver este problema es "sincronizar" los hilos. Tenga en cuenta que la sincronización aquí no significa dejar que los subprocesos se ejecuten juntos al unísono. La sincronización de subprocesos que llamamos se refiere a dejar que los subprocesos se ejecuten en orden. Usted lo ejecuta primero y yo lo ejecutaré de nuevo.

La forma más común de utilizar la sincronización de subprocesos es hacer que el acceso a la memoria de los mismos datos sea "mutuamente excluyente". Para explicar con nuestro ejemplo anterior, cuando el subproceso 1 está realizando operaciones de suma y salida de conteo, el subproceso 2 no puede acceder al conteo. En este momento, el conteo2 solo puede estar en un estado bloqueado y esperar a que se complete la operación de conteo en el subproceso 1 Más tarde, el hilo 2 puede acceder, solo un hilo puede escribir datos a la vez y otros hilos solo pueden esperar.

Por ejemplo, suponga que usted y yo representamos un hilo, y ahora quiero realizar una operación, ir al baño, después de entrar al baño, para evitar que intente ocupar la prueba, cerraré la puerta del baño, si También quieres usar el baño en este momento, por lo que solo puedes usar el baño en la puerta después de que termine de abrir y salir. El bloqueo aquí es como lo que llamamos mutex (mutex). Podemos asegurarnos de que solo un subproceso pueda manipular estos datos dentro de un cierto período de tiempo bloqueando y desbloqueando el mutex.

El tipo de mutex en pthread está representado por pthread_mutex_t, y std :: mutex se puede usar en C ++ 11.

Por ejemplo, el código de ahora se puede escribir como:

 staticintcount = 0; // count es una variable global estática

    / * Protege el mutex para operaciones de conteo, <span style = ”font-family: Arial, Helvetica, sans-serif;”> THREAD_MUTEX_INITIALIZER es un valor especial para inicializar variables mutex </span> * /

    pthread_mutex_t count_mutex = PTHREAD_MUTEX_INITIALIZER;

    // Una función de hilo de método del hilo 1

    void * A (void * datos) {

        while (1) {

            / * Bloquea el mutex que protege la operación de conteo. * /

            pthread_mutex_lock (& ​​count_mutex);

            contar + = 1;

            printf ("% d \ n", cuenta);

            / * El procesamiento de la operación de conteo se ha completado, así que desbloquee el mutex. * /

            pthread_mutex_nlock (& ​​count_mutex);

        }

    }

Además de los mutex, las herramientas de sincronización también tienen semáforos y variables de condición. Aunque los mutex a veces pueden satisfacer nuestras necesidades, desperdician mucho tiempo. El uso de estas herramientas puede ayudarnos a lograr modos de control más complejos.

4. Precauciones para el uso de subprocesos múltiples en Cocos2d-x

El mecanismo de administración de memoria que usa Cocos2d-x y las funciones de la interfaz OpenGL no son seguras para subprocesos. Por lo tanto, no intente llamar al método de administración de memoria proporcionado por el motor en un subproceso que no sea el subproceso principal, como en un subproceso nuevo. Para crear un elemento como un sprite o una capa, estos elementos llamarán a la liberación automática en el método create (). Liberación automática, retención y liberación no son seguros para subprocesos, y el contexto OpenGL no es seguro para subprocesos, así que no lo haga Utilice la función de dibujo de OpenGL en el nuevo hilo.

5.pthread multihilo

pthread es una biblioteca multiproceso, el nombre completo es POSIX threads, porque su API sigue el estándar internacional oficial POSIX. La biblioteca de subprocesos pthread está desarrollada por el lenguaje C y puede ejecutarse en múltiples plataformas, incluidas Andoird, iOS y Windows. Todas las funciones de subprocesos y tipos de datos en pthread se declaran en el archivo de encabezado <pthread.h>, que también es anterior a Cocos2d-x Biblioteca de subprocesos múltiples recomendada. Hoy en día, después de la introducción de las características de C ++ 11 en 3.x, la referencia de la biblioteca pthread se cancela y podemos usar el hilo de la biblioteca estándar para la programación multiproceso.

6. Carga asincrónica

El entorno de desarrollo del autor es Xcode + Cocos2d-x 3.3beta0 versión, entendamos brevemente el proceso de carga asincrónica.

Podemos usar una interfaz de carga para implementar la precarga de recursos. Solo después de que todos los recursos se cargan en la memoria, cuando usamos Sprite e ImageView para crear objetos, no causará el fenómeno de lag. Entonces, cómo cargar la imagen de forma asíncrona en la memoria, Cocos2d-x nos proporciona el método addImageAsync (), que se encuentra en la clase TextureCache. Echemos un vistazo al trabajo realizado en este método.

/ * Agregar textura asincrónicamente Los parámetros son la ruta del recurso de la imagen y la función de devolución de llamada para notificar después de la carga * /

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

{

    // Crea un puntero de objeto de textura

    Texture2D * textura = nullptr;

    // Obtener la ruta del recurso

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

    // Si esta textura ha sido cargada, regresa

    auto it = _textures.find (ruta completa);

    si (es! = _textures.end ())

        texture = it-> second; // segundo es el valor en clave-valor

    si (textura! = nullptr)

    {

        // Después de que se cargue la textura, ejecute directamente el método de devolución de llamada y finalice la función

        devolución de llamada (textura);

        regreso;

    }

    // La primera vez que se ejecuta la función cargada asincrónicamente, se debe inicializar la cola que guarda la estructura del mensaje

    si (_asyncStructQueue == nullptr)

    {

        // La liberación de las dos colas se completará en addImageAsyncCallBack

        _asyncStructQueue = nueva cola <AsyncStruct *> ();

        _imageInfoQueue = newdeque <ImageInfo *> ();

        // Crea un nuevo hilo para cargar la textura

        _loadingThread = newstd :: hilo (& TextureCache :: loadImage, esto);

        // Si salir de la variable

        _needQuit = falso;

    }

    si (0 == _asyncRefCount)

    {

        / * Registrar una función de devolución de llamada de actualización con el Programador

           Cocos2d-x comprobará la textura cargada en esta función de actualización

           Luego, procese una textura en cada fotograma y almacene en caché la información de la textura en TexutreCache

         * /

        Director :: getInstance () -> getScheduler () -> schedule (schedule_selector (TextureCache :: addImageAsyncCallBack), esto, 0, falso);

    }

    // El número de datos de textura cargados de forma asincrónica

    ++ _ asyncRefCount;

    // Genera una estructura de mensaje para cargar de forma asincrónica información de textura

    AsyncStruct * datos = nuevo (std :: nothrow) AsyncStruct (ruta completa, devolución de llamada);

    // Agrega la estructura generada a la cola

    _asyncStructQueueMutex.lock ();

    _asyncStructQueue-> push (datos);

    _asyncStructQueueMutex.unlock ();

    // Desbloquea el hilo para indicar que hay una posición vacía

    _sleepCondition.notify_one ();

}

En este código, está involucrado un método addImageAsyncCallBack. Este método se utiliza para verificar la textura después de que se completa la carga asincrónica. Se activará cuando se llame a addImageAsync por primera vez:

voidTextureCache :: addImageAsyncCallBack (floatdt)

{

    // _imageInfoQueue cola de dos extremos se usa para guardar la textura cargada en el nuevo hilo

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

    _imageInfoMutex.lock (); // bloquear el mutex

    if (imagesQueue-> empty ())

    {

        _imageInfoMutex.unlock (); // La cola está vacía para desbloquear

    }

    más

    {

        ImageInfo * imageInfo = imagesQueue-> front (); // Elimina la estructura de información de la imagen del primer elemento

        imagesQueue-> pop_front (); // Elimina el primer elemento

        _imageInfoMutex.unlock (); // Desbloquear

        AsyncStruct * asyncStruct = imageInfo-> asyncStruct; // Obtener la estructura del mensaje cargado de forma asincrónica

        Image * image = imageInfo-> image; // Obtener puntero de imagen para generar mapa de textura OpenGL

        conststd :: string & filename = asyncStruct-> filename; // Obtiene el nombre del archivo de recursos

        // Crear puntero de textura

        Texture2D * textura = nullptr;

        // El puntero de la imagen no está vacío

        si (imagen)

        {

            // Crea un objeto de textura

            textura = nuevo (estándar :: nothrow) Texture2D ();

            // Generar textura OpenGL desde el puntero de imagen

            textura-> initWithImage (imagen);

#if CC_ENABLE_CACHE_TEXTURE_DATA

            // caché el nombre del archivo de textura

            VolatileTextureMgr :: addImageTexture (textura, nombre de archivo);

#terminara si

            // caché de datos de textura

            _textures.insert (std :: make_pair (nombre de archivo, textura));

            textura-> retener ();

            // Únete al grupo de lanzamiento automático

            textura-> autorelease ();

        }

        más

        {

            auto it = _textures.find (asyncStruct-> nombre de archivo);

            si (es! = _textures.end ())

                textura = it-> segundo;

        }

        // Obtenga la función que debe notificarse después de que se complete la carga y notifique

        si (asyncStruct-> devolución de llamada)

        {

            asyncStruct-> callback (textura);

        }

        // Liberar imagen

        si (imagen)

        {

            imagen-> lanzamiento ();

        }

        // Libera dos estructuras

        deleteasyncStruct;

        deleteimageInfo;

        // Disminuye el número de texturas cargadas en uno

        –_AsyncRefCount;

        / * Todos los archivos están cargados, función de devolución de llamada de cierre de sesión * /

        si (0 == _asyncRefCount)

        {

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

        }

    }

}

Todavía hay muchos detalles que no se analizarán en detalle. Después de que la carga sea exitosa, solo necesitamos establecer el porcentaje de la barra de progreso en la función de devolución de llamada especificada al llamar al método addImageAsync (). P.ej:

boolHelloWorld :: init ()

{

    //

    // 1. super init primero

    si (! Layer :: init ())

    {

        falso retorno;

    }

    / * Cargar textura de forma asincrónica * /

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

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

    }

    returntrue;

}

voidHelloWorld :: imageLoadedCallback (Ref * pSender)

{

    // Cada vez que carga una textura con éxito, puede establecer el progreso de la barra de progreso en el método de devolución de llamada aquí, y luego saltar a la interfaz cuando se cargan todas las texturas.

}

Supongo que te gusta

Origin blog.csdn.net/qq_21743659/article/details/108637362
Recomendado
Clasificación