OpenGL, multithreading e lancio di distruttori

3

Come si fa a creare una classe che avverte correttamente uno sviluppatore in futuro di aver commesso un errore da qualche parte nella loro implementazione che ha portato a un oggetto che viene decostruito in uno stato che impedisce il rilascio delle sue risorse?

Sfondo:

Recentemente ho aggiornato a Visual Studio 2015 e ho iniziato a ricaricare e compilare il codice per un motore di gioco su cui sto lavorando e ho lanciato una nuova serie di avvertimenti "avviso C4297: '*': funzione presupposta per non generare un'eccezione ma ". Una rapida ricerca ha rivelato una convenzione C ++ che mi era sfuggita e le ragioni alla base di detta convenzione: i distruttori non dovrebbero lanciare eccezioni. Inoltre non posso davvero discutere delle ragioni, ma non sono nemmeno sicuro su come aggirare il problema.

All'interno di OpenGL un contesto contiene sostanzialmente tutte le informazioni di stato per il motore OpenGL. Solo un thread può avere un contesto in un dato momento e ogni thread può avere solo un contesto corrente. All'avvio del motore, crea il contesto e poi lascia il controllo sul contesto e avvia un altro thread che lo preleva e procede a gestire il rendering grafico per il motore. Per gestire tutto ciò, ho creato una classe del motore grafico che utilizza la semantica simile a un mutex per rivendicare e abbandonare il motore grafico e assicurarsi che non si commettano errori che potrebbero portare un giorno a qualcuno a tentare di fare cose con un contesto che non possiede.

Durante la distruzione, il motore grafico e un certo numero di altre classi che fanno affidamento su di esso controllano tutti per assicurarsi che il thread corrente abbia richiesto il motore grafico prima di eseguire le azioni necessarie alla loro distruzione. Se il thread non ha avuto il contesto grafico richiesto, il distruttore stava lanciando. Il mio obiettivo era in realtà quello di fornire una protezione di base contro la classe utilizzata in modo improprio in caso di incidente in futuro, per non rendere il motore grafico sicuro per i thread. Ora ... non sono sicuro del modo migliore per gestirlo.

Ho pensato solo di passare ad un approccio basato su mutex che potrei usare per bloccare l'accesso al contesto grafico fino a quando un thread è stato fatto, forse rendendo la classe del motore grafico pienamente capace di multi-threading (non che io possa capire perché dovresti eseguire il multithreading con un contesto OpenGL, poiché le chiamate necessarie per farlo sono abbastanza costose da vanificare qualsiasi vantaggio tu possa ottenere da ciò che ho capito).

L'opzione più allettante è stata quella di registrare un errore e terminare qualsiasi thread che tenta di utilizzare in modo errato la classe. Sfortunatamente, non riesco a trovare un modo indipendente dal sistema operativo di terminare solo il thread corrente. Se dovessi seguire questa strada, dovrei cercare modi appropriati per OS per terminare i thread attuali.

Inoltre non sono sicuro di non essere eccessivamente paranoico. Forse dovrei solo documentare l'uso corretto della classe e se qualcuno abusa, lasciali e spero che siano in grado di capire perché la loro applicazione non sta facendo quello che dovrebbe. Sono anche preoccupato che io sia il pazzo che abusa un giorno della classe in futuro.

    
posta Darinth 06.01.2016 - 22:23
fonte

6 risposte

3

Quindi capisco che tu voglia essere in grado di verificare questi potenziali problemi con il thread che non possiede il contesto, ma in che modo il lancio di un'eccezione può aiutarti? Come pensi di recuperare dal problema? E a che punto del programma? La mia ipotesi è che in generale non è possibile risolvere il problema nel momento in cui questo è successo, è solo un errore strutturale importante nel programma. Quindi, perché non creare solo il rapporto errori migliore possibile e quindi chiamare std::abort , invece di generare un'eccezione?

Lanciare le eccezioni dai distruttori viola l'idea fondamentale della gestione degli errori C ++, ovvero che i distruttori vengono utilizzati per ripulire gli oggetti quando viene lanciata un'eccezione e lo stack viene svolto. Se uno di questi distruttori genera un'eccezione durante lo sbobinamento dello stack per un'eccezione diversa , in modo che ci siano due eccezioni irrisolte nello stesso ambito, il programma viene terminato. In C ++ 11, per renderlo più sicuro, il tuo programma di solito termina ogni volta che un distruttore lancia un'eccezione, a meno che tu non faccia dei passi speciali per il piatto della caldaia per permetterlo. Ci sono molti altri grossi problemi con i distruttori che lanciano: devo ancora vedere una situazione in cui è una buona idea, o un espediente in qualsiasi modo per creare un distruttore di lancio.

    
risposta data 07.01.2016 - 03:29
fonte
9

Il tuo problema fondamentale è che hai progettato una contraddizione.

Da un lato, i tuoi oggetti sono incapsulati in RAII, gestendo le risorse da un altro sistema (OpenGL). D'altro canto, tutti i tuoi oggetti dipendono silenziosamente e invisibilmente da qualcosa che non è incapsulato: il contesto attuale. I tuoi oggetti ne hanno bisogno, ma non possono controllarlo .

Quindi hai solo due opzioni:

  1. Accetta la realtà. La tua interfaccia è fragile e gli utenti hanno di usarlo correttamente per ottenere risultati ragionevoli. Devono avere il giusto contesto quando usano i tuoi oggetti in qualsiasi modo, sia che li creino, li usino o che li distruggano. Se violano questo ... le cose esplodono.

    Personalmente, andrei con questa opzione. Se stai scrivendo un'applicazione grafica che ha bisogno di parlare con OpenGL, le prestazioni non sono probabilmente non importanti per te. E questo è probabilmente il modo più performante per farlo. Ottieni una ragionevole sicurezza RAII dall'uso sano dell'API.

  2. Porta il contesto nell'astrazione. Non dare agli utenti la libertà di rompere i tuoi oggetti. Un modo per farlo sarebbe quello di far sì che gli oggetti siano realmente posseduti dall'oggetto di contesto. Quindi, la distruzione di un oggetto è solo un segnale all'oggetto di contesto per eliminarlo in seguito. Ciò significa anche che ogni oggetto OpenGL con wrapper RAII diventa non funzionale una volta che il contesto è stato distrutto.

    In genere ciò richiederà un sacco di spese generali. Gli oggetti incapsulati con RAII devono comunicare molto con l'oggetto contesto. E l'oggetto contesto ha bisogno di sapere su ogni oggetto avvolto in RAII esistente. Sembra molto puntatore indiretto, riferimenti deboli e altre cose non banalmente costose.

risposta data 07.01.2016 - 00:38
fonte
1

Ho l'impressione che il tuo obiettivo nel far sì che il distruttore lanci un'eccezione fosse di rendere il più chiaro possibile agli utenti della biblioteca che il loro programma non era corretto nel modo in cui gestiva la durata dell'oggetto. Questo è un obiettivo nobile, ma ho alcuni problemi con l'approccio particolare che stai utilizzando per questo particolare problema.

  • Si verifica un'eccezione quando un programma rileva un errore in fase di esecuzione. Ma l'errore è stato commesso al momento della scrittura del programma (utenti della biblioteca mal gestiti la durata del tuo oggetto). Quindi il primo, il tempo più ovvio che puoi rendere gli utenti della biblioteca consapevoli del loro errore è in fase di compilazione.
  • La radice del problema è un problema di concorrenza. In fase di esecuzione, la via il sistema operativo pianifica l'esecuzione del programma è non deterministico (come per quanto ne sai tu). Quindi, in realtà, la tua indicazione per gli utenti delle librerie il loro codice utilizza la tua libreria in modo errato potrebbe anche non apparire, o esso potrebbe apparire solo un po 'di tempo. E gli utenti della biblioteca non saranno in grado riprodurre correttamente il problema (probabilmente). Quindi è davvero un indicatore problematico e inaffidabile che hanno fatto qualcosa di sbagliato!

Quindi il mio pensiero è che il tuo approccio con il lancio di un'eccezione nel distruttore non è davvero grandioso in quello che vuoi fare comunque. Non penso che dovresti combattere la corrente e cercare di continuare a farlo nel modo in cui lo stai facendo ora o in un modo simile registrando le cose e terminando i thread in fase di esecuzione. Penso che dovresti allontanarti dal tentativo di "gestire" questo problema in fase di esecuzione (con handle intendo "rendere ovvio al programmatore che devono correggere il loro programma").

Potresti voler ri-pensare la tua architettura un po '. Ho anche altre due raccomandazioni specifiche:

  • Fai in modo che il distruttore blocchi il thread corrente fino a quando non può acquisire il motore grafico per rilasciare la risorsa. Semplice, ma probabilmente indesiderato se non vuoi aspettare di acquisire il motore grafico.

  • Fai attività di motore grafico su un thread dedicato e utilizza una coda di messaggi in modo che altri thread possano invia alcune attività al motore grafico.

Come puoi vedere dai miei consigli, penso che rendere il tuo motore grafico sicuro per i thread ti risparmierà molti mal di testa in futuro anche se potresti avere qualche dubbio sul fare quel lavoro al momento. Entrambe queste raccomandazioni non dovrebbero richiedere una partenza radicale dalla tua attuale architettura (si spera).

Spero che questo aiuti.

    
risposta data 06.01.2016 - 22:56
fonte
1

Questo è il lato oscuro di OpenGL, proprio qui. Ho pensato a questo stesso problema.

Fortunatamente, esiste un numero relativamente piccolo di tipi di oggetti e potrebbe gestire la distruzione attraverso un semplice gestore di risorse. Questo gestore risorse gestirà le durate degli oggetti e li inoltrerà alla libreria OpenGL quando un contesto è attivo. Questo è uno schizzo di come potrebbe apparire il codice.

#include <GL/gl.h>
#include <mutex>
#include <vector>

class GLResourceManager {
private:
  // Types of resources
  enum class Type { Texture, Array, Buffer };
  // Record of a resource to be freed
  struct Resource {
    Type type;
    GLuint obj;
  };
  // Mutex for this object
  std::mutex m_mutex;
  // List of resources to be freed
  std::vector<Resource> m_objs;
  typedef std::lock_guard<std::mutex> lock_guard;

public:
  // Schedule a texture object for deletion
  void DeleteTexture(GLuint tex) {
    lock_guard lock(m_mutex);
    m_objs.push_back(Resource{Type::Texture, tex});
  }
  // Schedule a vertex array object for deletion
  void DeleteArray(GLuint array) {
    lock_guard lock(m_mutex);
    m_objs.push_back(Resource{Type::Array, array});
  }
  // Schedule a buffer object for deletion
  void DeleteBuffer(GLuint buffer) {
    lock_guard lock(m_mutex);
    m_objs.push_back(Resource{Type::Buffer, buffer});
  }
  // Process all pending deletions, requires an active context
  void Run() {
    lock_guard lock(m_mutex);
    for (const Resource &r : m_objs) {
      switch (r.type) {
      case Type::Texture:
        glDeleteTextures(1, &r.obj);
        break;
      case Type::Array:
        glDeleteVertexArrays(1, &r.obj);
        break;
      case Type::Buffer:
        glDeleteBuffers(1, &r.obj);
        break;
      }
    }
    m_objs.clear();
  }
};

Nella classe precedente, chiameresti DeleteTexture() nel tuo distruttore, quindi chiamerai Run() una volta per frame, o comunque spesso ti piace. Questo approccio ha una serie di svantaggi. La suddetta classe può ancora generare eccezioni, ma solo quando è esaurita la memoria, ed è spesso corretto terminare il programma quando la memoria è esaurita.

La mia preferenza personale è quella di evitare questo lavoro extra e di fare attenzione a usare le chiamate OpenGL e gli oggetti che fanno riferimento agli oggetti OpenGL e l'uso generoso di KHR_debug . Nelle mie applicazioni, solo il thread di rendering fa mai chiamate OpenGL e solo quel thread può costruire oggetti OpenGL. A volte questo significa fare cose come accodare le modifiche di stato fino a quando il thread di rendering non si avvicina a esso.

The most tempting option has been to just log an error terminate any thread that attempts to misuse the class. Unfortunately, I can't find an OS-independent way of terminating just the current thread. If I was to go this route, I'd have to look up OS-appropriate ways to terminate the current threads.

Questo è come dire: "Non voglio spararmi ai piedi, quindi lego un piede di dinamite al mio piede, e se mai dovessi spararmi ai piedi, la dinamite soffierà tutta la mia gamba off." La perdita di oggetti OpenGL è cattiva. Terminare il programma è cattivo. Terminare un thread sconosciuto è molto peggio. Lascerà il processo in uno stato conosciuto? Le probabilità sono basse. Il processo sarà in grado di recuperare? Probabilmente no. Potresti finire con una specie di deadlock o applicazione non responsiva? Forse.

    
risposta data 07.01.2016 - 01:08
fonte
0

In passato il modo in cui l'ho gestito è creare una coda degli oggetti OpenGL da eliminare. Quando viene chiamato il distruttore, invece di lanciare aggiungerà un nuovo oggetto alla coda che sa come distruggere la risorsa (chiamando glDeleteBuffers , glDeleteLists , ecc.).

Poi nella tua libreria, dopo che ogni wglSwapBuffers (o equivalente) è stato completato, sei nel codice che controlla il contesto, quindi controlla la coda "da eliminare". Se ci sono articoli nella lista, falli eseguire la loro pulizia e poi cancella la lista. Ovviamente avrai bisogno di un mutex o qualcosa per sincronizzare gli accessi a quell'elenco.

È un po 'complicato, ma a volte questa è la realtà quando si lavora con risorse esterne. Con questa strategia non avrai bisogno di cambiare la tua API e gli oggetti possono essere liberati su qualsiasi thread. L'ho usato per implementare i finalizzatori C # per gli oggetti che avvolgono le risorse OpenGL per prevenire questo problema di threading esatto.

    
risposta data 07.01.2016 - 01:12
fonte
0

Penso che si tratti di una questione OGL, e la soluzione non deve essere così difficile. Ho affrontato questo problema molte volte prima, incontrando gli stessi problemi anni prima, quando ho provato a ricoprire le risorse OGL in progetti orientati agli oggetti e sono stato bruciato in entrambi i modi, non solo cercando di distruggere risorse al di fuori di un contesto GL valido , ma anche cercando di crearli al di fuori di un contesto valido.

C'è una soluzione semplice per questo. Non è così complesso come sembra quando ti imbatti in questo problema. Basta centralizzare un tipo di GL resource manager che esegue effettivamente tutte le allocazioni / deallocazioni di risorse per le risorse GL di basso livello (VBO, shader, texture, ecc.)

I tuoi oggetti diventano quindi maniglie simili. Esempio semplificato:

class GpuTexture
{
public:
    explicit GpuTexture(GpuManager& imanager): 
        manager(&imanager), texture_id(new GLuint(invalid_texture))
    {
        // The manager may not create the texture immediately
        // if it's outside a valid context. It might push it to a
        // queue of things to initialize the next time it's in a
        // valid context.
        manager->initialize_texture(texture_id);
    }

    ~GpuTexture()
    {
        // The manager may not destroy the texture immediately
        // if it's outside a valid context. It might push it to a
        // queue of things to destroy the next time it's in a
        // valid context. If the texture hasn't been initialized
        // yet, this request will simply be ignored and the texture
        // will be removed from the "to initialize" queue.
        manager->destroy_texture(texture_id);
    }

    ...

private:
    GpuManager* manager;
    shared_ptr<GLuint> texture_id;
};

Questo è tutto ciò che c'è da fare. Puoi generalizzarlo e semplificare il gestore delle risorse semplicemente passando come std::function che dice cosa fare quando un contesto è valido, ad es. - falla come vuoi tu. So che le persone tendono a scoraggiare i nomi di "Manager", ma non possono pensare a uno più appropriato in questo caso per un "gestore" di risorse GPU centrale.

In realtà non ho usato shared_ptr nella versione di produzione finale (non avevamo ancora shared_ptr come parte dello standard allora), ma questo è il modo più semplice quando combinato con una struttura di dati concorrente sicura come una coda simultanea o Intel% TBB% co_de. Più efficiente potrebbe utilizzare un allocatore fisso e possibilmente rendere il gestore accessibile globalmente come una specie di singleton. Non ho trovato un bisogno di quel livello di efficienza dal momento che la maggior parte degli oggetti OGL mi hanno trattato di un piuttosto ingombrante (intera trama, VBO con almeno migliaia di triangoli che valgono per i dati degli attributi dei vertici, ecc.)

Per gli VBO, puoi portare questo concetto ancora di più e permetterne l'inizializzazione (come nella generazione di contenuti completi con ciò che normalmente sarebbe concurrent_vector e non solo VBO acqusition attraverso glBufferData ) del VBO al di fuori di un contesto GL valido. Basta scrivere il buffer sulla memoria di sistema ( glGenBuffers su un buffer privato, ad es.), Quindi inviare una richiesta per inizializzare il buffer su un buffer assegnato alla GPU tramite il gestore quando il contesto di visualizzazione è valido.

Tutto quello che devi fare per rendere la vita molto più semplice e trasformare quello che sembra essere questo terribile scenario da incubo in qualcosa che non diventerà mai una spina nella tua parte è semplicemente evitare la mentalità che il tuo oggetto deve essere quello questo effettivamente fa l'allocazione / deallocazione delle risorse della GPU. Basta centralizzarlo in un unico punto e sarà facile allocare e deallocare le risorse solo quando il contesto è valido e non dovrai più occuparti di distruttori che potrebbero fallire perché non si trovano in un contesto valido (sebbene c'è ancora un po 'di lavoro da fare se vogliamo rendere impossibile fallire poiché la spinta alla coda potrebbe fallire a causa di OOM, ad es.)

    
risposta data 15.01.2016 - 02:14
fonte

Leggi altre domande sui tag