Swift Concurrency Série 06|Problemas comuns na simultaneidade Swift: condições de corrida, solicitações repetidas e confusão de estado
O verdadeiro problema é que estes problemas muitas vezes se manifestam como interrupções esporádicas nos negócios, em vez de falhas explícitas.
A coisa mais frustrante sobre os bugs de simultaneidade é que eles geralmente não parecem bugs.
Manifesta-se com mais frequência online como estas questões ambíguas:
- O usuário disse “às vezes pisca”
- O teste diz “Ocasionalmente aparecem dados antigos”
- O produto dizia “Acabei de cortar o filtro, por que ele saltou de novo?”
- Não há nenhuma falha clara no log, mas o status da página está errado.
Em outras palavras, muitos problemas de simultaneidade parecem mais “exceções de negócios ocasionais” do que “obviamente tecnicamente quebrados”.
Portanto, neste artigo, não quero falar apenas sobre a definição de termos, mas focar diretamente em um cenário de página de lista mais real e detalhar os três tipos de problemas mais comuns:
- Competição
- Repetir solicitação
- estado de confusão
E como eles crescem em código real.
1. Primeiro olhe para uma página que é tão real que não poderia ser mais real.
Suponha que haja uma página de lista de artigos que suporte estas operações:
- Carregamento automático quando a página entra pela primeira vez
- Puxe para baixo para atualizar
- Alternar categorias
- Insira a pesquisa por palavra-chave
- Clique em “Tentar novamente”
Muitos projetos são escritos assim no início:
@MainActor
final class ArticlesViewModel: ObservableObject {
@Published var items: [Article] = []
@Published var isLoading = false
@Published var errorMessage: String?
@Published var selectedCategory: String = "all"
@Published var keyword: String = ""
let repository: ArticlesRepository
init(repository: ArticlesRepository) {
self.repository = repository
}
func onAppear() {
Task {
await load()
}
}
func refresh() {
Task {
await load()
}
}
func retry() {
Task {
await load()
}
}
func categoryChanged(to value: String) {
selectedCategory = value
Task {
await load()
}
}
func keywordChanged(to value: String) {
keyword = value
Task {
await load()
}
}
func load() async {
isLoading = true
errorMessage = nil
do {
items = try await repository.fetchArticles(
category: selectedCategory,
keyword: keyword
)
} catch {
errorMessage = error.localizedDescription
}
isLoading = false
}
}
Quando este código é escrito pela primeira vez, todos geralmente pensam que é “bastante suave”:
- Sim
async/await - O código é direto
- Toda entrada funciona
Mas enquanto a página for realmente usada, surgirão problemas de simultaneidade em breve.
2. O primeiro tipo de problema: a condição de corrida é uma ordem padrão que não existe.
Ainda este código.
Seu principal problema não é que ele abra muitos Task, mas que o padrão é que essas coisas aconteçam na ordem que você deseja:
- A solicitação enviada primeiro será devolvida primeiro.
- Quando a solicitação antiga retorna, as condições de filtragem atuais não foram alteradas.
- O início e o fim do carregamento correspondem sempre um a um
Mas os sistemas assíncronos não garantem essas ordens para a equipe.
Por exemplo, o usuário opera da seguinte forma:
- Entre na página e solicite que A emita
- Mude imediatamente para a categoria “iOS” e solicite que B envie
- Digite a palavra-chave
swiftnovamente para solicitar que C emita
Neste momento, se a ordem de devolução for:
- C volta primeiro
- Volte depois de A
- B volta por último
De acordo com o código atual, os três resultados serão alterados para items.
Em outras palavras, o que é exibido na página final depende de quem volta por último, e não de quem corresponde à intenção atual do usuário.
Esta é a condição de corrida mais típica:
O código depende secretamente da ordem, mas a ordem não é restrita de forma alguma.
3. O segundo tipo de problema: A causa raiz das solicitações repetidas geralmente é que a entrada não está fechada.
Olhando para o ViewModel acima, há pelo menos cinco entradas que irão acionar load():
-onAppear
-refresh
-retry
-categoryChanged
-keywordChanged
Cada entrada possui seu próprio Task.
Isto é certamente legal do ponto de vista da sintaxe, mas do ponto de vista da engenharia significa:
- Não existe um ponto de agendamento unificado para tarefas semelhantes
- Ninguém sabe se já existe uma tarefa semelhante em execução
- Quando novas tarefas aparecem, as tarefas antigas não têm um destino claro
Então, os “pedidos repetidos” não são mais acidentais, mas um produto natural da estrutura.
Portanto, no gerenciamento de simultaneidade, raramente pergunto:
“Por que há um pedido extra aqui?”
Eu pergunto com mais frequência:
“Quantas entradas existem para o mesmo tipo de tarefas? Existe alguma relação de substituição entre elas?”
Se você não conseguir responder a essas duas perguntas, solicitações repetidas serão quase inevitáveis.
4. O terceiro tipo de problema: O status está desordenado, muitas vezes porque os resultados expirados ainda podem ser gravados.
Uma situação comum é que, desde que a solicitação retorne com sucesso, o resultado deverá ser aceito.
Isso geralmente funciona bem em sistemas síncronos, mas geralmente é errado em sistemas simultâneos.
Porque o problema mais crítico em um cenário simultâneo é:
**Este resultado ainda é considerado válido para a página atual? **
Por exemplo:
- A página atual foi alterada para
keyword = "swift" - O resultado é da solicitação antiga
keyword = ""
O resultado é real, bem sucedido e no formato certo, mas expirou. Se ainda for permitido escrever a UI, o estado estará errado.
Portanto, num sistema concorrente, “o resultado está correto” e “o resultado é válido” são duas coisas diferentes. Superficialmente, muitos problemas de página parecem ser resultados errados, mas, na verdade, estão mais perto de não sermos capazes de julgar se ainda estão qualificados para serem implementados.
5. Não se apresse em usar ferramentas complexas primeiro. O primeiro passo é encerrar tarefas semelhantes.
O que o código acima mais precisa é fazer primeiro uma coisa muito simples:
**Dê a tarefas semelhantes uma entrada unificada. **
Por exemplo, primeiro carregue a lista assim:
@MainActor
final class ArticlesViewModel: ObservableObject {
@Published private(set) var state: ViewState = .idle
@Published private(set) var items: [Article] = []
@Published var selectedCategory: String = "all"
@Published var keyword: String = ""
private let repository: ArticlesRepository
private var loadTask: Task<Void, Never>?
init(repository: ArticlesRepository) {
self.repository = repository
}
func reload() {
let request = RequestContext(
category: selectedCategory,
keyword: keyword
)
loadTask?.cancel()
loadTask = Task {
await performLoad(request: request)
}
}
private func performLoad(request: RequestContext) async {
state = .loading
do {
let result = try await repository.fetchArticles(
category: request.category,
keyword: request.keyword
)
guard !Task.isCancelled else { return }
guard request.category == selectedCategory,
request.keyword == keyword else { return }
items = result
state = .loaded
} catch is CancellationError {
// 取消不更新页面
} catch {
guard !Task.isCancelled else { return }
state = .failed(error.localizedDescription)
}
}
}
Este código faz várias coisas muito críticas:
- Existe apenas um ponto de espera para tarefas de carregamento semelhantes
loadTask - Quando uma nova tarefa chegar, a tarefa antiga será cancelada primeiro
- Congelar o “contexto atual” para
RequestContextao enviar uma solicitação - Após o resultado ser retornado, será verificado se ainda corresponde à página atual
Observe que o que é realmente importante aqui é que as relações entre tarefas comecem a ficar claras.
6. “Contexto de solicitação de congelamento” é muito crítico
Muitos artigos de simultaneidade falam sobre cancelamento de tarefas, mas não dão ênfase suficiente ao “instantâneo de contexto”. Mas no negócio de páginas é muito importante.
Por exemplo, ao solicitar:
-selectedCategory = "ios"
-keyword = "swift"
Então, esses dois valores não devem ler dinamicamente os valores mais recentes no ViewModel atual após a solicitação ser interrompida. Caso contrário, você frequentemente obterá um estado muito estranho:
- Ao enviar uma solicitação, é um conjunto de parâmetros
- Outro conjunto de parâmetros é usado ao verificar os resultados
Portanto, um princípio muito prático é:
Ao iniciar uma tarefa assíncrona, congele o contexto de negócios do qual a tarefa realmente depende.
Desta forma, haverá uma base clara para julgar “se este resultado ainda é o resultado atual” posteriormente.
7. Muitos bugs de simultaneidade resultam em “muitas entradas de gravação de status”
Uma situação comum é que, ao encontrar um problema de simultaneidade, você imediatamente pense em:
- Você quer trancá-lo?
- Você quer ser ator?
- Você quer mudar de assunto?
É claro que isso às vezes é importante, mas em cenários de nível de página, os problemas mais comuns são, na verdade:
- Existem muitos lugares para escrever
items - Muitos lugares podem ser alterados
isLoading - Muitas entradas podem enviar solicitações diretamente
Uma vez que as entradas de gravação do estado estejam espalhadas, mesmo que não haja competição real de dados, ocorrerá o fenômeno de “a combinação está errada”.
Portanto, quando faço esse tipo de solução de problemas, geralmente faço primeiro as seguintes perguntas:
- Quais códigos têm autoridade para alterar esse status
- Quais tarefas têm o direito de encerrar o carregamento atual
- Quais resultados têm o direito de substituir a lista atual
Uma vez que esses problemas não são resolvidos, geralmente é apenas uma questão de tempo até que os bugs se desenvolvam.
8. Uma sequência de evolução mais próxima do projeto real
Se você realmente quer resolver esse tipo de problema, sugiro evoluir nesta ordem ao invés de introduzir muitos mecanismos no início:
1. Feche a entrada para tarefas semelhantes
Primeiro, deixe o “carregamento da lista” ter apenas uma entrada unificada, em vez de enviar sua própria solicitação para cada evento da UI.
2. Esclareça a relação de substituição de tarefas
Quais tarefas devem ser simultâneas e quais devem cancelar tarefas antigas e manter apenas a última.
3. Congelar contexto de solicitação
Colete os principais parâmetros de negócios nos quais você confia ao fazer solicitações em um objeto claro.
4. Adicione julgamento de validade ao resultado
Nem todos os resultados retornados com sucesso são elegíveis para alterar a página atual.
5. Finalmente, considere um isolamento de estado compartilhado mais complexo
Por exemplo, cache compartilhado entre páginas, coordenação de recursos entre módulos e, em seguida, observe o ator, o coordenador unificado e outras soluções.
Essa ordem é mais estável porque resolve primeiro o relacionamento de concorrência de negócios, em vez de introduzir primeiro um vocabulário técnico mais complexo.
9. Conclusão: A essência da maioria dos problemas de simultaneidade de negócios é “nenhuma modelagem de relacionamentos de tarefas”
Condições de corrida, solicitações duplicadas e confusão de estado parecem ser três problemas, mas as causas raízes reais costumam ser muito próximas:
- Quem tem a mesma tarefa que quem, sem modelagem
- Novas tarefas estão chegando, o que fazer com tarefas antigas, não há modelagem
- O resultado ainda é válido? Não há modelagem.
- Onde posso escrever meu status sem fechá-lo?
Então, para reformular este artigo de uma forma mais curta, eu diria:
A maioria dos problemas de simultaneidade nos negócios parece ser incompetente com a sintaxe de simultaneidade, mas na verdade eles estão mais próximos de falhar ao modelar claramente relacionamentos de tarefas, validade de resultados e permissões de gravação de estado.
Assim que essas três coisas começarem a ficar claras, muita “confusão acidental” desaparecerá mais facilmente do que você pensa.
What to read next
Want more posts about Swift Concurrency?
Posts in the same category are usually the best next step for reading more on this topic.
View same categoryWant to keep following #Swift Concurrency?
Tags are useful for related tools, specific problems, and similar troubleshooting notes.
View same tagWant to explore another direction?
If you are not sure what to read next, return to the homepage and start from categories, topics, or latest updates.
Back home