Explicação detalhada da implementação de multithreading e carregamento assíncrono Cocos2d-x

O Cocos2d-x é um mecanismo de loop de thread único. O mecanismo atualiza o estado de cada elemento no jogo entre cada quadro para garantir que eles não interfiram um com o outro. Embora o programa pareça estar funcionando em paralelo durante esse processo, Na verdade, é um processo serial.

Por exemplo, quando o jogo está em um salto de cena, geralmente liberamos os recursos da cena atual e carregamos os recursos da próxima cena, portanto, precisamos carregar as texturas necessárias para a próxima interface na memória. Este processo requer Operações de leitura e gravação em arquivos de recursos, e esta operação de armazenamento externo é muito demorada. Se a quantidade de imagens que precisam ser carregadas for grande e a resolução for alta, é provável que nosso thread principal bloqueie, porque o processador não pode ser Em um intervalo de tempo tão curto (taxa de quadros padrão 1/60) para completar uma quantidade tão grande de cálculos, e porque há apenas um thread que não interrompe o conteúdo de execução atual para executar outro conteúdo, então neste momento vamos observar a interface A taxa de quadros caiu drasticamente e até mesmo a interface travou diretamente.

Para evitar esses problemas, o Cocos2d-x fornece aos desenvolvedores funções de carregamento assíncrono no mecanismo. Podemos enviar uma solicitação de carregamento de arquivo assíncrono para TextureCache, e TextureCache nos ajudará a criar um novo thread para concluir o processo demorado. Carregue a operação de textura e podemos continuar a realizar outros cálculos em nosso thread principal.

Além do carregamento de recursos, as leituras e gravações na rede também são uma das operações demoradas comuns, portanto, o uso de threads em sistemas cliente / servidor também é um fenômeno comum, como a função assíncrona em HttpClient.

2. Single-core e multi-core

Single-core significa apenas um processador, e multi-core significa vários processadores. Nossos dispositivos móveis atuais são geralmente dual-core ou quad-core, como iPhone 6 e Samsung note4. Dispositivos mais antigos, como iPhone4, têm CPUs de núcleo único. O que eu quero explicar aqui é a diferença entre multi-threading single-core e multi-core multi-core.

O multithreading em um dispositivo de núcleo único é simultâneo.

O multithreading em dispositivos multi-core é paralelo ou simultâneo.

Vamos explicar o significado dessas duas frases. Single-core dual-threading é uma prática muito comum. Por exemplo, escrevemos um código com vários threads e o deixamos rodar no iphone4. Como o iphone4 tem apenas um processador, Na verdade, o novo encadeamento que criamos e o encadeamento principal estão em um estado de operação intercalada. Por exemplo, se dividirmos o intervalo de tempo em 100 milissegundos, o programa executa o encadeamento principal nos atuais 100 milissegundos e o programa nos próximos 100 milissegundos Ele pode executar outro encadeamento e retornar ao encadeamento principal após 100 milissegundos. A vantagem disso é que ele não atrasará um encadeamento indefinidamente. Assim que a fatia de tempo for atingida, o programa interromperá forçosamente o encadeamento atual para executar Outro tópico. Desta forma, em um nível macro, eles parecem ser executados ao mesmo tempo, mas na verdade, eles ainda são executados separadamente.

No entanto, se esse código for colocado no Samsung note4 para execução, o note4 terá uma CPU de 4 núcleos. Neste dispositivo multiprocessador, nossos dois threads podem ocupar um processador para cada thread e executar independentemente. Isso deve ser executado ao mesmo tempo, sem a necessidade de executar intercalado. Esse estado é chamado de estado paralelo. Portanto, a simultaneidade é, na verdade, um estado pseudo-paralelo, é apenas um estado de fingir realizar várias operações ao mesmo tempo.

3. Problemas de segurança da linha

Primeiro, entendemos um conceito, segurança de thread.

Segurança de thread significa que o código pode ser chamado por vários threads sem resultados desastrosos. Aqui, damos um exemplo simples para ilustrar (aqui o autor usa o formato de função thread de threads POSIX, apenas entenda o significado geral)

staticintcount = 0; // a contagem é uma variável global estática

    // Uma função de thread de método da thread 1

    void * A (void * data) {

        enquanto (1) {

            contagem + = 1;

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

        }

    }

    // Função de thread do método B do thread 2

    void * B (void * data) {

        enquanto (1) {

            contagem + = 1;

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

        }

Conforme mostrado no código acima, suponha que iniciamos dois threads agora, e as funções de thread dos dois threads são definidas como A e B respectivamente (as funções de thread são escritas separadamente para fins de compreensão, de fato, uma função de thread é escrita para dois threads executarem Isso é o suficiente), a saída do console que esperamos após a execução do programa é 123456789 ... (Este código pode não ter nenhum significado para realizar a função .... Aqui está apenas um exemplo)

Mas, na verdade, o resultado da execução pode não ser o caso. O que esperamos é que em cada função de thread, o valor de contagem seja aumentado em um por vez e, em seguida, a contagem seja gerada. No entanto, como a ordem de execução de diferentes threads é imprevisível, o código acima É muito provável que esta situação (supondo que o dispositivo seja um único núcleo): o valor inicial de contagem é 0, agora é a vez de o thread 1 ser executado, e a execução de contagem + = 1 em A, então o valor de contagem já é igual a 1, que deve ser emitido posteriormente 1, mas a fração de tempo acabou aqui, e agora é mudada para o encadeamento 2. Neste momento, o valor da contagem no encadeamento 2 já é 1 e agora é aumentado em 1 novamente para se tornar 2 e, em seguida, a instrução de impressão é executada. Emita um 2 e, em seguida, a fração de tempo termina e retorna a 1, e 1 continua a executar a instrução de saída que não foi executada agora, mas porque o valor de contagem foi alterado novamente pelo thread 2, podemos ver a saída na tela neste momento É 223456789 ....

Claro, a situação que eu disse pode não ocorrer necessariamente. O motivo é que a ordem de execução de diferentes threads é imprevisível e cada execução produzirá resultados diferentes. Talvez a saída seja normal na maioria dos casos. Este exemplo apenas diz a todos que os threads não são seguros neste caso.

Então, como resolver os problemas acima?

Em primeiro lugar, a variável de contagem é um dado compartilhado por dois threads, então pode haver problemas quando dois threads acessam esses dados compartilhados ao mesmo tempo. Por exemplo, no código anterior, o valor de contagem que o thread 1 deseja produzir é 1, mas porque o encadeamento 2 alterou este valor quando o encadeamento 1 não produziu contagem, mas o encadeamento 1 não sabia que o valor de contagem foi alterado e continuou a executar a saída. Como resultado, o valor de saída do encadeamento 1 foi 2.

A maneira mais comum de resolver esse problema é "sincronizar" os threads. Observe que a sincronização aqui não significa permitir que os encadeamentos sejam executados juntos em uníssono. A sincronização de encadeamentos que chamamos refere-se a permitir que os encadeamentos sejam executados em ordem. Você executa primeiro e eu irei executá-lo novamente.

A maneira mais comum de usar a sincronização de threads é tornar o acesso dos mesmos dados à memória "mutuamente exclusivo". Para explicar com nosso exemplo acima, quando o encadeamento 1 está realizando operações de adição e saída de contagem, o encadeamento 2 não tem permissão para acessar a contagem. Neste momento, o contador2 só pode estar em um estado bloqueado e esperar a operação de contagem no encadeamento 1 ser concluída Posteriormente, o thread 2 pode acessar, apenas um thread tem permissão para gravar dados por vez, e outros threads podem apenas esperar.

Por exemplo, suponha que você e eu representemos cada um um fio, e agora eu quero fazer uma operação, ir ao banheiro, depois de entrar no banheiro, para evitar que você tente ocupar o teste, vou trancar a porta do banheiro, se Você também quer usar o banheiro neste momento, então você só pode usar o banheiro na porta depois que eu terminar de destrancar e sair. O bloqueio aqui é como o que chamamos de mutex (mutex). Podemos garantir que apenas um thread pode manipular esses dados dentro de um determinado período de tempo, bloqueando e desbloqueando o mutex.

O tipo mutex em pthread é representado por pthread_mutex_t, e std :: mutex pode ser usado em C ++ 11.

Por exemplo, o código agora pode ser escrito como:

 staticintcount = 0; // a contagem é uma variável global estática

    / * Protege o mutex para operações de contagem, <span style = ”font-family: Arial, Helvetica, sans-serif;”> THREAD_MUTEX_INITIALIZER é um valor especial para inicializar variáveis ​​mutex </span> * /

    pthread_mutex_t count_mutex = PTHREAD_MUTEX_INITIALIZER;

    // Uma função de thread de método da thread 1

    void * A (void * data) {

        enquanto (1) {

            / * Bloqueia o mutex que protege a operação de contagem. * /

            pthread_mutex_lock (& ​​count_mutex);

            contagem + = 1;

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

            / * O processamento da operação de contagem foi concluído, portanto, desbloqueie o mutex. * /

            pthread_mutex_nlock (& ​​count_mutex);

        }

    }

Além dos mutexes, as ferramentas de sincronização também têm semáforos e variáveis ​​de condição. Embora os mutexes às vezes possam atender às nossas necessidades, eles perdem muito tempo. O uso dessas ferramentas pode nos ajudar a alcançar modos de controle mais complexos.

4. Precauções para usar multi-threading no Cocos2d-x

O mecanismo de gerenciamento de memória usado pelo Cocos2d-x e as funções da interface OpenGL não são seguros para thread. Portanto, não tente chamar o método de gerenciamento de memória fornecido pelo mecanismo em um thread diferente do thread principal, como em um novo thread Para criar um elemento como um sprite ou uma camada, esses elementos irão chamar autorelease no método create (). Autorelease, reter e liberar não são thread-safe, e o contexto OpenGL não é thread-safe, então não Use a função de desenho OpenGL no novo thread.

5.pthread multithreading

pthread é uma biblioteca multi-threaded, o nome completo é POSIX threads, porque sua API segue o padrão internacional oficial POSIX. A biblioteca de threads de pthread é desenvolvida pela linguagem C e pode ser executada em várias plataformas, incluindo Andoird, iOS e Windows. Todas as funções de thread e tipos de dados em pthread são declarados no arquivo de cabeçalho <pthread.h>, que também é anterior a Cocos2d-x Biblioteca multi-thread recomendada. Hoje em dia, após a introdução dos recursos do C ++ 11 no 3.x, a referência da biblioteca pthread é cancelada e podemos usar o thread da biblioteca padrão para programação multi-threaded.

6. Carregamento assíncrono

O ambiente de desenvolvimento do autor é a versão Xcode + Cocos2d-x 3.3beta0, vamos entender brevemente o processo de carregamento assíncrono.

Podemos usar uma interface de carregamento para implementar o pré-carregamento de recursos, só depois que todos os recursos forem carregados na memória, quando usarmos Sprite e ImageView para criar objetos, não causará o fenômeno de lag. Então, como carregar a imagem de forma assíncrona na memória, Cocos2d-x nos fornece o método addImageAsync (), que está localizado na classe TextureCache. Vamos dar uma olhada no trabalho realizado neste método

/ * Adicionar textura de forma assíncrona Os parâmetros são o caminho do recurso da imagem e a função de retorno de chamada para notificar após o carregamento * /

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

{

    // Cria um ponteiro de objeto de textura

    Texture2D * texture = nullptr;

    // Obter caminho do recurso

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

    // Se esta textura foi carregada, retorna

    auto it = _textures.find (fullpath);

    if (it! = _textures.end ())

        texture = it-> second; // segundo é o valor no valor-chave

    if (textura! = nullptr)

    {

        // Depois que a textura é carregada, execute diretamente o método de retorno de chamada e encerre a função

        retorno de chamada (textura);

        Retorna;

    }

    // Na primeira vez que a função carregada de forma assíncrona é executada, a fila que salva a estrutura da mensagem precisa ser inicializada

    if (_asyncStructQueue == nullptr)

    {

        // A liberação das duas filas será concluída em addImageAsyncCallBack

        _asyncStructQueue = newqueue <AsyncStruct *> ();

        _imageInfoQueue = newdeque <ImageInfo *> ();

        // Cria uma nova thread para carregar a textura

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

        // Se deve sair da variável

        _needQuit = falso;

    }

    if (0 == _asyncRefCount)

    {

        / * Registrar uma função de retorno de chamada de atualização com o Scheduler

           O Cocos2d-x irá verificar a textura carregada nesta função de atualização

           Em seguida, processe uma textura a cada quadro e armazene em cache as informações de textura no TexutreCache

         * /

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

    }

    // O número de dados de textura carregados de forma assíncrona

    ++ _ asyncRefCount;

    // Gera uma estrutura de mensagem para carregar informações de textura de forma assíncrona

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

    // Adicione a estrutura gerada à fila

    _asyncStructQueueMutex.lock ();

    _asyncStructQueue-> push (dados);

    _asyncStructQueueMutex.unlock ();

    // Desbloqueia o tópico para indicar que há uma posição vazia

    _sleepCondition.notify_one ();

}

Neste código, um método addImageAsyncCallBack está envolvido. Esse método é usado para verificar a textura depois que o carregamento assíncrono for concluído. Ele será ativado quando addImageAsync for chamado pela primeira vez:

voidTextureCache :: addImageAsyncCallBack (floatdt)

{

    // _imageInfoQueue fila dupla é usada para salvar a textura carregada no novo thread

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

    _imageInfoMutex.lock (); // bloquear o mutex

    if (imagesQueue-> empty ())

    {

        _imageInfoMutex.unlock (); // A fila está vazia para desbloquear

    }

    outro

    {

        ImageInfo * imageInfo = imagesQueue-> front (); // Remover o primeiro elemento da estrutura de informação da imagem

        imagesQueue-> pop_front (); // Excluir o primeiro elemento

        _imageInfoMutex.unlock (); // Desbloquear

        AsyncStruct * asyncStruct = imageInfo-> asyncStruct; // Obter estrutura de mensagem carregada de forma assíncrona

        Image * image = imageInfo-> image; // Obter ponteiro de imagem para gerar mapa de textura OpenGL

        conststd :: string & filename = asyncStruct-> filename; // Obtenha o nome do arquivo de recurso

        // Criar ponteiro de textura

        Texture2D * texture = nullptr;

        // O ponteiro da imagem não está vazio

        if (imagem)

        {

            // Cria um objeto de textura

            textura = novo (std :: nothrow) Texture2D ();

            // Gera textura OpenGL a partir do ponteiro de imagem

            textura-> initWithImage (imagem);

#if CC_ENABLE_CACHE_TEXTURE_DATA

            // armazenar em cache o nome do arquivo de textura

            VolatileTextureMgr :: addImageTexture (textura, nome do arquivo);

#fim se

            // Dados de textura de cache

            _textures.insert (std :: make_pair (nome do arquivo, textura));

            textura-> reter ();

            // Junte-se ao pool de liberação automática

            textura-> liberação automática ();

        }

        outro

        {

            auto it = _textures.find (asyncStruct-> nome do arquivo);

            if (it! = _textures.end ())

                textura = it-> segundo;

        }

        // Obtenha a função que precisa ser notificada após o carregamento ser concluído e notifique

        if (asyncStruct-> callback)

        {

            asyncStruct-> callback (textura);

        }

        // Liberar imagem

        if (imagem)

        {

            imagem-> liberação ();

        }

        // Libere duas estruturas

        deleteasyncStruct;

        deleteimageInfo;

        // Diminui o número de texturas carregadas em um

        –_AsyncRefCount;

        / * Todos os arquivos são carregados, função de retorno de chamada de logout * /

        if (0 == _asyncRefCount)

        {

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

        }

    }

}

Ainda há muitos detalhes que não serão analisados ​​detalhadamente. Depois que o carregamento for bem-sucedido, só precisamos definir a porcentagem da barra de progresso na função de retorno de chamada especificada ao chamar o método addImageAsync (). Por exemplo:

boolHelloWorld :: init ()

{

    //

    // 1. super init primeiro

    if (! Layer :: init ())

    {

        retorna falso;

    }

    / * Carregar textura de forma assíncrona * /

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

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

    }

    returntrue;

}

voidHelloWorld :: imageLoadedCallback (Ref * pSender)

{

    // Cada vez que você carrega uma textura com sucesso, você pode definir o progresso da barra de progresso no método de retorno de chamada aqui, e então pular para a interface quando todas as texturas forem carregadas.

}

Acho que você gosta

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