Uso e progettazione di API asincrone con parti naturalmente sincrone

3

Ho programmato per molto tempo, ma molto raramente con qualcosa di asincrono (e spesso non ha nulla a che fare con il multithreading).

Principalmente per il gusto di farlo, sto scrivendo un programma per scaricare i testi delle canzoni e salvarli su file musicali (ad esempio tag ID3v2). Ho già installato la maggior parte di questo: ho due classi che gestiscono il download (una per sito supportato dal programma), oltre al codice necessario per scrivere i tag sui file.
Il design che volevo fin dall'inizio era abbastanza semplice e sincrono; Avevo programmato di creare un'API sincrona / bloccante per scaricare testi e avviare tali attività in background per mantenere viva l'interfaccia utente.

L'idea era che la classe che desidera recuperare i testi creerebbe un'istanza LyricFetcher e chiamerà string LyricFetcher::fetchLyrics(string artist, string title) . Questo, a sua volta, cerca di recuperare i testi di ciascun sito in ordine, usando qualcosa come

for (Site *site : sites) {
    lyrics = site->fetchLyrics(artist, title);
    if (successfully downloaded lyrics)
        return lyrics;
}
return (didn't find anything);

I problemi si sono manifestati quando ho capito che le uniche API che potevo utilizzare per recuperare i dati da Internet erano asincrone; avresti impostato una richiesta e dirla (tramite segnali e slot) per dirti quando è finita.
Con quello, non potevo semplicemente restituire i testi come una stringa.
Come soluzione alternativa, ho progettato l'API per utilizzare un callback, che il codice specifico del sito chiama quando ha terminato il download o non è riuscito, ad esempio qualcosa come void LyricFetcher::fetchLyrics(string artist, string title, function<void(string)> callback) .
Il callback è stato quindi inoltrato alle classi specifiche del sito, che hanno lo stesso tipo di firma del fetchLyrics sopra.

Questo ha funzionato bene per un singolo sito, ma mi sono imbattuto in nuovi problemi cercando di implementare lo pseudocodice multi-sito sopra.
Non riesco a utilizzare lo pseudocodice, poiché site->fetchLyrics restituirebbe immediatamente.
L'unica "soluzione" che ho trovato era usare callback annidati, che è orribile.

void LyricFetcher::fetchLyrics(string artist, string title, function<void(string)> callback) {
    site1->fetchLyrics(artist, title, [=](string lyrics) {
        if (did match)
            callback(lyrics);
        else {
            site2->fetchLyrics(artist, title, [=](string lyrics) {
               ...

Non solo è brutto, ma significa anche che non puoi scorrere i siti in un ciclo semplice. (Non sono nemmeno sicuro che funzionerebbe affatto.)

Quale è un buon modo di progettare in merito a problemi come questi?
L'API "user facing" di LyricFetcher::fetchLyrics dovrebbe preferibilmente essere sincrona (con i testi come un semplice valore di ritorno), o usare segnali / slot per segnalare il completamento (e trasferire i risultati).

FWIW, questo è in C ++ con Qt (C ++ 14, Qt 5.6), ma ho cercato di rimanere con uno pseudocodice C ++ y nel post.

    
posta exscape 31.05.2016 - 22:32
fonte

1 risposta

1

The problem here isn't about starting many tasks and waiting for them, but about running callback/signal-based in a sequential manner.

Usa una funzione concorrente che prende la funzione che crea la struttura ordinata e la funzione che itera attraverso la struttura ordinata come argomenti. Ad esempio, nello spazio dei nomi simultaneo, Qt ha un metodo mapReduce con la seguente firma

QtConcurrent::mappedReduced(const Sequence &sequence, MapFunction mapFunction, ReduceFunction reduceFunction, QtConcurrent::ReduceOptions reduceOptions = UnorderedReduce | SequentialReduce)

Che può essere usato come segue:

void addToCollage(QImage &collage, const QImage &thumbnail)
   {
   QPainter p(&collage);
   static QPoint offset = QPoint(0, 0);
   p.drawImage(offset, thumbnail);
   offset += ...;
   }

QImage scaled(const QImage &image)
  {
  return image.scaled(100, 100);
  }

QList<QImage> images = ...;
QFuture<QImage> thumbnails = QtConcurrent::mapped(images, scaled);

QList<QImage> images = ...;
QFuture<QImage> collage = QtConcurrent::mappedReduced(images, scaled, addToCollage);

The reduce function will be called once for each result returned by the map function, and should merge the intermediate into the result variable. QtConcurrent::mappedReduced() guarantees that only one thread will call reduce at a time, so using a mutex to lock the result variable is not necessary. The QtConcurrent::ReduceOptions enum provides a way to control the order in which the reduction is done. If QtConcurrent::UnorderedReduce is used (the default), the order is undefined, while QtConcurrent::OrderedReduce ensures that the reduction is done in the order of the original sequence.

Riferimenti

risposta data 22.03.2018 - 05:43
fonte

Leggi altre domande sui tag