Generalmente una coda o un array generici non sono, da soli, thread-safe, proprio come molti altri tipi di dati. La sicurezza del filo si ottiene in genere in due modi:
- Uso dei blocchi mutex - ogni thread che desidera modificare un valore deve attendere.
- Delega - solo il thread proprietario può modificare il valore.
I blocchi di Mutex sono abbastanza semplici: nessuno possiede il valore, ma solo un thread può modificarlo alla volta. Quando un thread modifica un valore, altri thread devono attendere. Il problema con questo approccio è contention . Più di un thread che desidera modificare un valore allo stesso tempo e il numero di thread in attesa di stack.
La gestione della delega dipende in realtà dalla lingua in questione. In Objective C è necessario che GCD invii richieste asincrone (o anche sincrone) sul thread proprietario. Hai la possibilità di (in modo asincrono) di modificare il valore senza tenere premuto il thread di richiesta. GCD programma internamente queste attività in una coda e le esegue quando il thread di destinazione non è occupato.
I canali Go sono in qualche modo una forma di delega. Puoi usarlo come coda non bloccante usando i canali bufferizzati. Ma la più grande differenza tra i canali di Go e altri approcci può probabilmente essere riassunta con la seguente frase:
Don't communicate by sharing, share by communicating.
Un canale Go è molto più di una semplice coda, è una pipeline di comunicazione. Il modo migliore per illustrarlo è mostrare un estratto di uno dei miei progetti:
for {
select {
case client := <-server.accept:
server.addClient(client)
go server.serveClient(client)
case client := <-server.remove:
server.removeClient(client)
case envelope := <-server.dropbox:
switch envelope.cmd {
case "pub":
server.publish(envelope)
case "sub":
server.subscribe(envelope)
case "unsub":
server.unsubscribe(envelope)
}
case err := <-server.errors:
return err
}
}
Il codice sopra riportato è in esecuzione sulla routine Go principale (i canali sono bufferizzati).
<-server.accept
- Una seconda routine Go (non mostrata qui) gestisce le nuove connessioni e le passa al canale di accettazione. La routine Go principale accetta il nuovo client e li aggiunge a una mappa di client connessi. Un'altra routine Go viene generata per gestire le richieste dal nuovo client.
<-server.remove
- Quando un client disconnette la routine Go che gestisce la sua connessione, la passa nuovamente alla routine Go principale da rimuovere.
<-server.dropbox
- Quando un client ha qualcosa che desidera inviare, lo mette nella "casella di riepilogo" per essere consegnato ad altri client, che sono registrati nella mappa.
Ogni client ha un canale inbox a cui il thread principale può consegnare "mail". La routine Go che gestisce la connessione client legge la posta in arrivo.
Come puoi vedere dall'esempio, il codice diventa in qualche modo riconoscibile: un ufficio postale con una casella di consegna per consegnare la posta e una casella di posta in arrivo per ogni cliente che la riceve. Questo è solo un esempio di come i canali possono essere usati.
I canali dovrebbero sempre essere usati in ogni situazione? No, ho visto situazioni in cui un blocco mutex aveva più senso di un canale. Le strutture di dati atomici sono un esempio in cui le serrature mutex hanno più senso.
Penso che l'obiettivo generale di un canale sia rendere il codice più facile da capire. Trasmette l'intenzione più precisamente, in alcune (ma molte) situazioni. Internamente un canale utilizza probabilmente dei blocchi mutex. Penso che un punto importante da portare a casa sia - i canali sono utili; solo non farti trasportare.