Bom dia a todos. Quero falar um pouco sobre o meu projeto
qt-async , talvez pareça interessante ou até útil para alguém.
Assincronia e multithreading há muito que estão seriamente incluídas na vida cotidiana dos desenvolvedores. Muitas linguagens e bibliotecas modernas são projetadas com o uso assíncrono em mente. A linguagem C ++ também está se movendo lentamente nessa direção - apareceram std :: thread, std :: promessa / future, eles estão prestes a trazer corotinas e redes. A biblioteca Qt também não fica para trás, oferecendo seus análogos QThread, QRunnable, QThreadPool, QFuture, etc. Ao mesmo tempo, não encontrei widgets para exibir ações assíncronas no Qt (talvez estivesse parecendo mal, correto se estiver enganado).
Portanto, decidi compensar as deficiências e tentar implementar esse widget por conta própria. O desenvolvimento multithread é um negócio complexo, mas interessante.
Antes de prosseguir com a implementação do widget, é necessário descrever o modelo que ele apresentará ao usuário na forma de uma janela. Na sua forma mais geral, a operação do widget me parece a seguinte: em algum momento, o usuário ou o sistema inicia uma operação assíncrona. Neste ponto, o widget mostra o progresso da operação ou simplesmente uma indicação da operação. Opcionalmente, o usuário pode cancelar a operação. Em seguida, a operação assíncrona é concluída de duas maneiras: ocorreu um erro e nosso widget o mostra ou o widget mostra o resultado da operação bem-sucedida.
Assim, nosso modelo pode estar em um dos três estados:
- Progresso - uma operação assíncrona está em andamento
- Erro - falha na operação assíncrona
- Valor - operação assíncrona concluída com sucesso
Em cada um dos estados, o modelo deve armazenar os dados correspondentes, então chamei o modelo AsyncValue. É importante observar que a própria operação assíncrona não faz parte do nosso modelo, apenas altera seu estado. Acontece que o AsyncValue pode ser usado com qualquer biblioteca assíncrona, observando um padrão de uso simples:
- No início da operação assíncrona, defina AsuncValue como Progress
- No final - em Erro ou em Valor, dependendo do sucesso da operação
- Opcionalmente, durante a operação, você pode atualizar os dados do Progress e ouvir o sinalizador Stop se o usuário tiver a oportunidade de interromper a operação.
Aqui está um exemplo esquemático usando QRunnable:
class MyRunnable : public QRunnable { public: MyRunnable(AsyncValue& value) : m_value(value) {} void run() final { m_value.setProgress(...);
O mesmo esquema para trabalhar com std :: thread:
AsyncValue value; std::thread thread([&value] () { value.setProgress(...);
Assim, a primeira versão da nossa classe poderia ser algo como isto:
template <typename ValueType_t, typename ErrorType_t, typename ProgressType_t> class AsyncValue { public: using ValueType = ValueType_t; using ErrorType = ErrorType_t; using ProgressType = ProgressType_t;
Todo mundo que se depara com classes que oferecem suporte a multithreading sabe que a interface dessas classes é diferente dos análogos de thread único. Por exemplo, a função size () é inútil e perigosa em um vetor multiencadeado. Seu resultado pode se tornar imediatamente inválido, pois o vetor pode ser modificado no momento em outro encadeamento.
Os usuários da classe AsyncValue devem poder acessar dados da classe. A emissão de uma cópia dos dados pode ser cara, qualquer um dos tipos ValueType / ErrorType / ProgressType pode ser pesado. A emissão de um link para dados internos é perigosa - a qualquer momento, ela pode se tornar inválida. A seguinte solução é proposta:
1. Conceda acesso aos dados por meio das funções accessValue / accessError / accessProgress, nas quais são recebidas lambdas que recebem os dados correspondentes. Por exemplo:
template <typename Pred> bool accessValue(Pred valuePred) { QReadLocker locker(&m_lock); if (m_value.index() != 0) return false; valuePred(std::get<0>(m_value)); return true; }
Assim, o acesso ao valor interno é realizado por referência e está sob o bloqueio para leitura. Ou seja, o link no momento do acesso não se tornará inválido.
2. O usuário AsyncValue na função accessValue pode lembrar o link para dados internos, desde que ele esteja inscrito no sinal stateChanged e após o processamento o sinal não precise mais usar esse link, porque ela se tornará inválida.
Sob tais condições, o consumidor do AsyncValue sempre garante um acesso a dados válido e conveniente. Esta solução tem várias consequências que afetam a implementação da classe AsyncValue.
Primeiro, nossa classe deve enviar um sinal quando um estado muda, mas ao mesmo tempo é modelo. Teremos que adicionar uma classe Qt básica, onde podemos determinar o sinal pelo qual o widget atualizará seu conteúdo e todos os interessados atualizarão links para dados internos.
lass AsyncValueBase : public QObject { Q_OBJECT Q_DISABLE_COPY(AsyncValueBase) signals: void stateChanged(); };
Em segundo lugar, o momento do envio do sinal deve ser bloqueado para leitura (para que o AsyncValue não possa ser alterado até que todos processem o sinal) e, o
mais importante , nesse momento deve haver links válidos para dados novos e antigos. Como no processo de envio do sinal, alguns consumidores do AsyncValue ainda usam os links antigos e aqueles que processaram o sinal usam os novos.
Acontece que std :: variant não é adequado para nós e teremos que armazenar dados na memória dinâmica para que os endereços de dados novos e antigos não sejam alterados.
Uma pequena digressão.
Você pode considerar outras implementações da classe AsyncValue que não exigem alocações dinâmicas:
- Dê aos consumidores apenas cópias dos dados internos do AsyncValue. Como escrevi anteriormente, essa solução pode ser mais abaixo do ideal se os dados forem grandes.
- Defina dois sinais em vez de um: stateWillChange / stateDidChange. Obrigar os consumidores a se livrar de links antigos no primeiro sinal e receber novos links no segundo sinal. Parece-me que esse esquema complica excessivamente os consumidores do AsyncValue, porque eles têm intervalos de tempo quando o acesso ao AsyncValue é negado.
A seguinte implementação esquemática da função setValue é obtida:
void AsyncValue::setValue(...) { m_lock { m_lock m_lock } stateChanged m_lock };
Como você pode ver, precisamos aumentar o bloqueio m_lock para escrever e devolvê-lo para leitura. Infelizmente, não existe esse suporte na classe QReadWriteLock. Você pode obter a funcionalidade desejada com um par de QMutex / QReadWriteLock. Aqui está uma implementação da classe AsyncValue, quase real:
Para quem não está cansado e não está perdido, continuamos.
Como você pode ver, temos funções accessXXX que não esperam até que o AsyncValue entre no estado correspondente, mas simplesmente retornam false. Às vezes, é útil aguardar de forma síncrona até que um valor ou erro apareça no AsyncValue. Essencialmente, precisamos de um análogo de std :: future :: get. Aqui está a assinatura da função:
template <typename ValuePred, typename ErrorPred> void wait(ValuePred valuePred, ErrorPred errorPred);
Para que essa função funcione, precisamos de uma variável de condição - um objeto de sincronização que possa ser esperado em um thread e ativado em outro. Na função de espera, devemos esperar e, ao alterar o estado de AsyncValue de Progresso para Valor ou Erro, devemos notificar os garçons.
Adicionar outro campo à classe AsyncValue, necessário em casos raros quando a função de espera é usada, me levou a pensar - esse campo pode ser opcional? A resposta é óbvia, é claro que é possível se você armazenar std :: unique_ptr e criá-lo, se necessário. A segunda questão surgiu - é possível tornar esse campo opcional e não fazer alocações dinâmicas. Quem se importa, veja o código a seguir. A idéia principal é a seguinte: a primeira chamada de espera cria uma estrutura QWaitCondition na pilha e grava seu ponteiro em AsyncValue; as chamadas de espera subsequentes simplesmente verificam se o ponteiro não está vazio; use a estrutura desse ponteiro; se o ponteiro estiver vazio, veja acima a primeira chamada de espera .
class AsyncValueBase : public QObject { ... struct Waiter {
Como já mencionado, o AsyncValue não possui um método para computação assíncrona para não ser vinculado a uma biblioteca específica. Em vez disso, são usadas funções livres que implementam a assincronia de uma maneira ou de outra. A seguir, é apresentado um exemplo de computação do AsyncValue em um pool de threads:
template <typename AsyncValueType, typename Func, typename... ProgressArgs> bool asyncValueRunThreadPool(QThreadPool *pool, AsyncValueType& value, Func&& func, ProgressArgs&& ...progressArgs) {
A biblioteca implementa mais duas funções semelhantes: asyncValueRunNetwork para processamento de solicitações de rede e asyncValueRunThread, que executa uma operação em um thread recém-criado. Os usuários da biblioteca podem criar facilmente suas próprias funções e usá-las para usar as ferramentas assíncronas usadas em outros locais.
Para aumentar a segurança, a classe AsyncValue foi estendida com outra classe de modelo AsyncTrackErrorsPolicy, que permite responder ao uso indevido de AsyncValue. Por exemplo, aqui está a implementação padrão da função AsyncTrackErrorsPolicy :: inProgressWhileDestruct, que será chamada se AsyncValue for destruído enquanto a operação assíncrona estiver em execução:
void inProgressWhileDestruct() const { Q_ASSERT(false && "Destructing value while it's in progress"); }
Quanto aos widgets, sua implementação é bastante simples e concisa. O AsyncWidget é um contêiner que contém um widget para exibir um erro, progresso ou valor, dependendo do estado em que o AsyncValue estiver atualmente.
virtual QWidget* createValueWidgetImpl(ValueType& value, QWidget* parent); virtual QWidget* createErrorWidgetImpl(ErrorType& error, QWidget* parent); virtual QWidget* createProgressWidgetImpl(ProgressType& progress, QWidget* parent);
O usuário é obrigado a redefinir apenas a primeira função, para exibir valor, os outros dois têm implementações padrão.
A biblioteca
qt-async acabou sendo compacta, mas ao mesmo tempo bastante útil. O uso do AsyncValue / AsyncWidget, onde anteriormente havia funções síncronas e uma GUI estática, permitirá que seus aplicativos se tornem modernos e mais responsivos.
Para quem leu o bônus até o final - um vídeo do aplicativo de demonstração