Come eseguire in sicurezza una lista per rimuovere in sicurezza un elemento e gli elementi associati

0

Ho un singolo elenco di elementi collegati. Ogni elemento ha un ID univoco (numero intero positivo) e un tipo. Ogni elemento può avere un elemento "sorella", associato al suo ID, che può anche essere nella lista. L'elemento sorella è sempre di tipo fisso e non ha una sorella. L'elemento sorella potrebbe trovarsi in un'altra lista.

Ho una funzione per distruggere un elemento di un certo tipo. Questo esegue alcune operazioni di pulizia sull'elemento e lo rimuove dall'elenco. Quella funzione di distruzione dovrebbe anche distruggere il suo elemento sorella. La funzione può anche distruggere tutti gli elementi di un certo tipo, quando è passato ID==-1 .

Attualmente sto utilizzando SLIST di BSD, quindi sembra:

destroy_element(list, type, ID) {
   struct elem *item, *tmp_item 
   SLIST_FOREACH_SAFE(item, list, next, tmp_item) {
       if((item->type == type) && (ID==-1 || item->ID == ID) {
           cleanup_for_element(item);
           if (item->sister_ID != -1) {
               destroy_element(item->sister_list, item->sister_ID);
           }
           SLIST_REMOVE(list, item);
       }
   }
}

SLIST_FOREACH_SAFE rende possibile rimuovere l'elem corrente ('item'), ma sfortunatamente questo si bloccherà in alcuni casi, se l'elemento sister è nella stessa lista (es .: list==item->sister_list ) e nel punto sbagliato (in questo caso particolare, se l'elemento sister è subito dopo l'elemento nell'elenco)

Sto cercando un costrutto migliore qui. Per SLIST in particolare, ma in termini di algoritmo generale.

Modifica :

Mi stavo chiedendo se questa soluzione "walk the list twice" sia sicura:

destroy_element(list, type, ID) {
   struct elem *item, *tmp_item 
   SLIST_FOREACH_SAFE(item, list, next, tmp_item) {
       if((item->type == type) && (ID==-1 || item->ID == ID) {
           if (item->sister_ID != -1) {
               destroy_element(item->sister_list, item->sister_ID);
           }
       }
   }
   SLIST_FOREACH_SAFE(item, list, next, tmp_item) {
       if((item->type == type) && (ID==-1 || item->ID == ID) {
           cleanup_for_element(item);
           SLIST_REMOVE(list, item);
       }
   }
}

Sembra funzionare (cioè: crash è sparito), ma non sono sicuro che sia una garanzia in tutti i casi.

    
posta Droopycom 11.10.2018 - 00:03
fonte

1 risposta

3

Il bug è causato da un'interazione (piuttosto) complessa tra:

SLIST_FORACH_SAFE e l'invocazione ricorsiva di destroy_element e la rimozione che fanno ciascuno.

SLIST_FOREACH_SAFE si rende "sicuro" per eliminare l'elemento corrente, memorizzando nella cache una copia dell'elemento successivo dopo questo - e lo fa sostanzialmente prima della parte del corpo del ciclo. Pertanto, a differenza di SLIST_FOREACH , non si basa sull'attuale oggetto che è valido quando continua il ciclo per l'iterazione successiva, ma fa affidamento sull'elemento successivo dopo essere ancora valido, dal momento che sta memorizzando nella cache che

Tuttavia, nel tuo caso, la chiamata ricorsiva può distruggere l'elemento memorizzato nella cache, quindi quell'elemento non è più valido.

Non penso che il double traversal risolverà questo problema (potrebbe cambiare il comportamento ma avrà ancora un errore logico) poiché il valore memorizzato nella cache di next sarà ancora usato nel primo loop (come risultato di la rimozione della sorella durante l'invocazione ricorsiva).

Suppongo che potresti risolvere il problema in diversi modi:

(a) scrivi la tua versione del ciclo for. Copia dal modello di SLIST_FOREACH :

for (item = SLIST_FIRST(list);
    item;
    /*item = SLIST_NEXT(item, next)*/) {
    if((item->type == type) && (ID==-1 || item->ID == ID) {
        cleanup_for_element(item);
        if (item->sister_ID != -1) {
            destroy_element(item->sister_list, item->sister_ID);
        }
        item = SLIST_NEXT(item, next);
        SLIST_REMOVE(list, item);
   }
   else {
       item = SLIST_NEXT(item, next);
   }
}

(b) ricarica il valore memorizzato nella cache dopo l'invocazione ricorsiva

destroy_element(list, type, ID) {
   struct elem *item, *tmp_item 
   SLIST_FOREACH_SAFE(item, list, next, tmp_item) {
       if((item->type == type) && (ID==-1 || item->ID == ID) {
           cleanup_for_element(item);
           if (item->sister_ID != -1) {
               destroy_element(item->sister_list, item->sister_ID);
               tmp_item = SLIST_NEXT(item, field);  // update the next item
           }
           SLIST_REMOVE(list, item);
       }
   }
}

I seguenti due approcci catturano ciò che deve essere eliminato senza usare una chiamata ricorsiva, e attendono fino a dopo il ciclo principale per eliminare la sorella:

(c) catturare gli elementi della sorella da cancellare in una lista separata e cancellarli dopo il ciclo principale. Ciò richiederebbe un elenco secondario.

(d) contrassegnare gli elementi come "dovrebbe eliminare" e dopo il ciclo principale eseguire attraverso la lista ancora una volta cancellando qualcosa contrassegnato "dovrebbe eliminare". Ciò richiederebbe un valore booleano per acquisire questo stato.

Ognuno di questi modi potrebbe essere reso più semplice mettendo a parte il -1 valore magico che stai utilizzando che significa "tutto" dal caso più semplice di rimozione di un solo elemento. Vorrei separare delete_element da delete_all . Quindi delete_element può eliminare una sorella dopo il ciclo principale, senza una lista (e senza ricorsione se lo desideri).

Il delete_all , può utilizzare un elenco secondario o, con una solida conoscenza del dominio, ad esempio, scorrere su tutti gli elenchi disponibili in modo che non sia necessario alcun elenco secondario.

Poiché questi tipi di problemi non sono univoci, alcuni linguaggi offrono costrutti iterativi che non consentono la modifica della struttura dati che viene attraversata e si guasteranno immediatamente se vengono apportate modifiche, anche se "sicure".

Dovresti essere consapevole che la tua versione a ciclo singolo attraversa l'elenco molte volte:

(1) il ciclo esterno è un attraversamento

(2) la chiamata ricorsiva è un'altra traversata

(3) SLIST_REMOVE esegue un altro attraversamento

Tra i lati positivi, escono presto così quando trovano l'elemento che interrompono.

Quindi, per rimuovere un elemento con una sorella, ottieni 4 travesali della lista, anche se presto.

Riferimento: link

    
risposta data 11.10.2018 - 01:58
fonte

Leggi altre domande sui tag