Procedure consigliate per la scrittura su file
API importanti
Gli sviluppatori a volte si imbattono in una serie di problemi comuni quando usano i metodi di scrittura delle classi FileIO e PathIO per eseguire operazioni I/O del file system. I problemi comuni, ad esempio, includono:
- Un file scritto parzialmente.
- L'app riceve un'eccezione quando chiama uno dei metodi.
- Le operazioni lasciano indietro file .TMP con un nome file simile al nome file di destinazione.
I metodi di scrittura delle classi FileIO e PathIO includono:
- WriteBufferAsync
- WriteBytesAsync
- WriteLinesAsync
- WriteTextAsync
Questo articolo fornisce informazioni dettagliate sul funzionamento di questi metodi per aiutare gli sviluppatori a comprendere meglio quando e come usarli. Questo articolo fornisce linee guida e non tenta di fornire una soluzione per tutti i possibili problemi I/O dei file.
Nota
Questo articolo illustra i metodi FileIO tramite esempi e discussioni. Tuttavia, i metodi PathIO seguono un modello simile e la maggior parte delle indicazioni fornite in questo articolo si applica anche a tali metodi.
Praticità versus controllo
Un oggetto StorageFile non è un punto di controllo dei file come il modello di programmazione Win32 nativo. Al contrario, un StorageFile è una rappresentazione di un file con i metodi che servono a modificarne il contenuto.
Comprendere questo concetto è utile quando si eseguono I/O con un StorageFile. Ad esempio, la sezione Writing to a file (Scrittura su file) illustra tre modi per scrivere in un file:
- Usando il metodo FileIO.WriteTextAsync.
- Creando un buffer e quindi chiamando il metodo FileIO.WriteBufferAsync.
- Il modello in quattro passaggi che usa un flusso:
- Aprire il file per ottenere un flusso.
- Ottenere un flusso di output.
- Creare un oggetto DataWriter e chiamare il metodo di scrittura corrispondente.
- Eseguire il commit dei dati nel processo di scrittura dei dati e scaricare il flusso di output.
I primi due scenari sono quelli più comunemente usati dalle app. La scrittura su file in una singola operazione semplifica la codifica e la manutenzione, e grazie ad essa l'app non sarà più responsabile della gestione delle complessità dell'I/O dei file. Tuttavia, questa praticità comporta un costo: la perdita di controllo sull'intera operazione e la possibilità di rilevare gli errori in punti specifici.
Il modello transazionale
I metodi di scrittura delle classi FileIO e PathIO aggiungono un ulteriore livello ai passaggi del terzo modello di scrittura descritto sopra. Questo livello è incapsulato in una transazione di archiviazione.
Per proteggere l'integrità del file originale nel caso in cui si verifichino problemi durante la scrittura dei dati, i metodi di scrittura usano un modello transazionale aprendo il file con OpenTransactedWriteAsync. Questo processo crea un oggetto StorageStreamTransaction. Dopo aver creato questo oggetto di transazione, le API scrivono i dati in modo simile nell'esempio Accesso al file o nell'esempio di codice nell'articolo StorageStreamTransaction.
Il diagramma seguente illustra le attività sottostanti eseguite dal metodo WriteTextAsync in un'operazione di scrittura con esito positivo. Questa illustrazione fornisce una vista semplificata dell'operazione. Ad esempio, salta passaggi come la codifica del testo e il completamento asincrono su thread diversi.
I vantaggi dell'uso dei metodi di scrittura delle classi FileIO e PathIO al posto del più complesso modello in quattro passaggi che usa un flusso sono:
- Una chiamata API per gestire tutti i passaggi intermedi, compresi gli errori.
- Il file originale viene mantenuto se si verificano problemi.
- Lo stato del sistema proverà a rimanere il più lineare possibile.
Tuttavia, con così tanti punti di errore intermedi possibili, esiste una forte probabilità di errore. Quando si verifica un errore potrebbe essere difficile comprendere dove il processo non è riuscito. Le sezioni seguenti illustrano alcuni degli errori riscontrati quando si usano i metodi di scrittura e le relative soluzioni possibili.
Codici errore comuni per i metodi di scrittura delle classi FileIO e PathIO
Questa tabella presenta i codici di errore comuni in cui gli sviluppatori di app si imbattono quando usano i metodi di scrittura. I passaggi nella tabella corrispondono ai passaggi nel diagramma precedente.
Nome errore (valore) | Passaggi | Cause | Soluzioni |
---|---|---|---|
ERROR_ACCESS_DENIED (0X80070005) | 5 | Il file originale potrebbe essere contrassegnato per l'eliminazione, probabilmente da un'operazione precedente. | Ripetere l'operazione. Verificare che l'accesso al file sia sincronizzato. |
ERROR_SHARING_VIOLATION (0x80070020) | 5 | Il file originale viene aperto da un'altra scrittura esclusiva. | Ripetere l'operazione. Verificare che l'accesso al file sia sincronizzato. |
ERROR_UNABLE_TO_REMOVE_REPLACED (0x80070497) | 19-20 | Il file originale (file. txt) non può essere sostituito perché è in uso. Prima che questo potesse essere sostituito, un altro processo o operazione ha ottenuto l'accesso al file. | Ripetere l'operazione. Verificare che l'accesso al file sia sincronizzato. |
ERROR_DISK_FULL (0x80070070) | 7, 14, 16, 20 | Il modello sottoposto a transazione crea un file aggiuntivo consumando memoria aggiuntiva. | |
ERROR_OUTOFMEMORY (0x8007000E) | 14, 16 | Questa situazione può verificarsi a causa di più operazioni I/O in attesa o di file di grandi dimensioni. | L'errore potrebbe essere risolto adottando un approccio più granulare tramite il controllo del flusso. |
E_FAIL (0x80004005) | Qualsiasi | Varie | Ripetere l'operazione. Se il problema persiste, potrebbe trattarsi di un errore della piattaforma e l'app deve essere terminata perché si trova in uno stato incoerente. |
Altre considerazioni per gli stati di file che potrebbero causare errori
Oltre agli errori restituiti dai metodi di scrittura, di seguito sono riportate alcune linee guida su che cosa può aspettarsi un'app quando effettua la scrittura su file.
I dati sono stati scritti nel file se e solo se l'operazione è stata completata
L'app non deve fare alcuna supposizione sui dati nel file durante l'operazione di scrittura. Il tentativo di accedere al file prima del completamento di un'operazione potrebbe causare incoerenze dei dati. L'app deve essere responsabile del rilevamento degli I/O in sospeso.
Readers
Se il file in cui si scrive viene usato anche da un lettore equilibrato (vale a dire, aperto con FileAccessMode.Read, le letture successive avranno esito negativo con un errore ERROR_OPLOCK_HANDLE_CLOSED (0x80070323). In alcuni casi le app provano a riaprire il file per leggerlo nuovamente durante l'operazione di scrittura. Ciò potrebbe comportare una race condition in cui il metodo di scrittura non riesce durante il tentativo di sovrascrivere il file originale perché non può essere sostituito.
File da KnownFolders
L'app potrebbe non essere l'unica app che sta tentando di accedere a un file che si trova in una qualsiasi delle KnownFolders. Non c'è garanzia che se l'operazione ha esito positivo, il contenuto che un'app ha scritto nel file rimarrà costante la volta successiva che tenta di leggere il file. Inoltre, errori di condivisione e di accesso negato diventano più comuni in questo scenario.
I/O in conflitto
È possibile ridurre le probabilità di errori di concorrenza se l'app usa i metodi di scrittura per i file nei suoi dati locali, ma è comunque necessario fare attenzione. Se più operazioni di scrittura vengono inviate contemporaneamente al file, non c'è garanzia su quali dati finiscono nel file. Per risolvere questo problema, è consigliabile che l'app serializzi le operazioni di scrittura sul file.
File ~TMP
In alcuni casi, se l'operazione viene annullata in modo forzato (ad esempio, se l'app è stata sospesa o terminata dal sistema operativo), la transazione non è stata eseguita o chiusa in modo appropriato. Ciò può tralasciare i file con un'estensione (. ~ TMP). Provare a eliminare questi file temporanei (se presenti nei dati locali dell'app) quando si gestisce l'attivazione dell'app.
Considerazioni in base ai tipi di file
Alcuni errori possono diventare più prevalenti in base al tipo di file, la frequenza con cui vi si accede e le dimensioni del file. In generale, esistono tre categorie di file a cui l'app può accedere:
- I file creati e modificati dall'utente nella cartella dei dati locali dell'app. Questi vengono creati e modificati solo se si usa l'app, e sono presenti solo all'interno dell'app.
- Metadati dell'app. L'app usa questi file per tenere traccia del proprio stato.
- Altri file in percorsi del file system in cui l'app ha dichiarato funzionalità di accesso. In genere si trovano in una delle KnownFolders.
L'app ha il pieno controllo sulle prime due categorie di file, perché fanno parte dei file del pacchetto dell'app e sono accessibili dall'app in modo esclusivo. Per i file nell'ultima categoria, l'app deve tenere presente che altre app e servizi del sistema operativo possono accedere al file contemporaneamente.
A seconda dell'app, l'accesso ai file può variare in base alla frequenza:
- Molto bassa. Si tratta in genere di file che vengono aperti una volta all'avvio dell'app e che vengono salvati quando l'app viene sospesa.
- Basso. Si tratta di file su cui l'utente sta eseguendo un'azione specifica (ad esempio salvare o caricare).
- Media o alta. Si tratta di file in cui l'app deve aggiornare continuamente i dati (ad esempio, le funzionalità di salvataggio automatico o di tracciamento costante dei metadati).
Per le dimensioni del file, prendere in considerazione i dati sulle prestazioni nel grafico seguente per il metodo WriteBytesAsync. Questo grafico confronta il tempo necessario per completare un'operazione rispetto alle dimensioni del file, su una prestazione media di 10000 operazioni per dimensioni del file in un ambiente controllato.
I valori temporali sull'asse y vengono omessi intenzionalmente da questo grafico, poiché configurazioni e hardware diversi producono valori temporali assoluti diversi. Tuttavia, durante i nostri test abbiamo osservato costantemente queste tendenze:
- Per file di dimensioni ridotte (<= 1 MB): il tempo per completare le operazioni è costantemente veloce.
- Per file di grandi dimensioni (> 1 MB): il tempo necessario per completare le operazioni inizia ad aumentare in maniera esponenziale.
I/O durante la sospensione dell'app
L'app deve essere progettata in modo tale da gestire la sospensione se si desidera mantenere le informazioni di stato o i metadati per l'utilizzo nelle sessioni successive. Per informazioni generali sulla sospensione delle app, vedere App lifecycle (Ciclo di vita dell'app) e questo post di blog.
A meno che il sistema operativo conceda l'esecuzione estesa per l'app, quando l'app viene sospesa ha 5 secondi per rilasciare tutte le relative risorse e salvare i dati. Per l'affidabilità e l'esperienza utente migliori, considerare sempre che il tempo a disposizione per gestire le attività di sospensione è limitato. Tenere presenti le linee guida seguenti durante il periodo di tempo di 5 secondi per la gestione delle attività di sospensione:
- Provare a mantenere I/O al minimo per evitare situazioni di race condition causate da operazioni di scaricamento e rilascio.
- Evitare di scrivere file che richiedono centinaia di millisecondi o più.
- Se l'app usa i metodi di scrittura, tenere presenti tutti i passaggi intermedi che questi metodi richiedono.
Se l'app opera su una piccola quantità di dati di stato durante la sospensione, nella maggior parte dei casi è possibile usare i metodi di scrittura per scaricare i dati. Tuttavia, se l'app usa una grande quantità di dati relativi allo stato, è consigliabile usare dei flussi per archiviare direttamente i dati. Questo può contribuire a ridurre il ritardo introdotto dal modello transazionale dei metodi di scrittura.
Per un esempio, vedere l'esempio BasicSuspension.
Altri esempi e risorse
Ecco alcuni esempi e altre risorse per scenari specifici.
Esempio di codice per riprovare l'I/O del file
Ecco un esempio di pseudocodice su come ripetere un'operazione di scrittura (C#), presupponendo che l'operazione di scrittura deve essere eseguita dopo che l'utente seleziona un file per il salvataggio:
Windows.Storage.Pickers.FileSavePicker savePicker = new Windows.Storage.Pickers.FileSavePicker();
savePicker.FileTypeChoices.Add("Plain Text", new List<string>() { ".txt" });
Windows.Storage.StorageFile file = await savePicker.PickSaveFileAsync();
Int32 retryAttempts = 5;
const Int32 ERROR_ACCESS_DENIED = unchecked((Int32)0x80070005);
const Int32 ERROR_SHARING_VIOLATION = unchecked((Int32)0x80070020);
if (file != null)
{
// Application now has read/write access to the picked file.
while (retryAttempts > 0)
{
try
{
retryAttempts--;
await Windows.Storage.FileIO.WriteTextAsync(file, "Text to write to file");
break;
}
catch (Exception ex) when ((ex.HResult == ERROR_ACCESS_DENIED) ||
(ex.HResult == ERROR_SHARING_VIOLATION))
{
// This might be recovered by retrying, otherwise let the exception be raised.
// The app can decide to wait before retrying.
}
}
}
else
{
// The operation was cancelled in the picker dialog.
}
Sincronizzare l'accesso al file
Il blog Programmazione parallela con .NET è un'ottima risorsa per consigli sulla programmazione parallela. In particolare, il post su AsyncReaderWriterLock descrive come mantenere l'accesso esclusivo a un file per operazioni di scrittura, consentendo l'accesso in lettura simultaneo. Tenere presente che la serializzazione degli I/O avrà un impatto sulle prestazioni.