Utilizzo di ThreadPool.QueueUserWorkItem in ASP.NET in uno scenario di traffico elevato


111

Ho sempre avuto l'impressione che l'utilizzo di ThreadPool per (diciamo non critici) attività in background di breve durata fosse considerata una best practice, anche in ASP.NET, ma poi mi sono imbattuto in questo articolo che sembra suggerire il contrario: il l'argomento è che dovresti lasciare ThreadPool per gestire le richieste relative ad ASP.NET.

Quindi ecco come ho svolto finora piccole attività asincrone:

ThreadPool.QueueUserWorkItem(s => PostLog(logEvent))

E l'articolo suggerisce invece di creare un thread in modo esplicito, simile a:

new Thread(() => PostLog(logEvent)){ IsBackground = true }.Start()

Il primo metodo ha il vantaggio di essere gestito e limitato, ma c'è la possibilità (se l'articolo è corretto) che le attività in background siano quindi in competizione per i thread con gestori di richieste ASP.NET. Il secondo metodo libera il ThreadPool, ma a costo di essere illimitato e quindi potenzialmente utilizzare troppe risorse.

Quindi la mia domanda è: il consiglio nell'articolo è corretto?

Se il tuo sito riceveva così tanto traffico che il tuo ThreadPool si stava riempiendo, allora è meglio andare fuori banda, o un ThreadPool completo implicherebbe che stai comunque arrivando al limite delle tue risorse, nel qual caso tu non dovresti provare ad avviare i tuoi thread?

Chiarimento: sto solo chiedendo nell'ambito di piccole attività asincrone non critiche (ad esempio, registrazione remota), elementi di lavoro non costosi che richiederebbero un processo separato (in questi casi sono d'accordo che avrai bisogno di una soluzione più robusta).


La trama si infittisce: ho trovato questo articolo ( blogs.msdn.com/nicd/archive/2007/04/16/… ), che non riesco a decodificare del tutto. Da un lato, sembra che IIS 6.0+ elabori sempre le richieste sui thread di lavoro del pool di thread (e che le versioni precedenti potrebbero farlo), ma poi c'è questo: "Tuttavia, se stai usando nuove pagine asincrone .NET 2.0 ( Async = "true") o ThreadPool.QueueUserWorkItem (), quindi la parte asincrona dell'elaborazione verrà eseguita all'interno di [un thread della porta di completamento]. " La parte asincrona dell'elaborazione ?
Jeff Sternal,

Un'altra cosa: dovrebbe essere abbastanza facile da testare su un'installazione di IIS 6.0+ (che non ho in questo momento) controllando se i thread di lavoro disponibili del pool di thread sono inferiori ai suoi thread di lavoro massimi, quindi facendo lo stesso all'interno della coda oggetti da lavoro.
Jeff Sternal,

Risposte:


104

Altre risposte qui sembrano tralasciare il punto più importante:

A meno che tu non stia cercando di parallelizzare un'operazione che richiede molta CPU per farla più velocemente su un sito a basso carico, non ha alcun senso usare un thread di lavoro.

Ciò vale sia per i thread gratuiti, creati da new Thread(...), sia per i thread di lavoro in ThreadPoolche rispondono alle QueueUserWorkItemrichieste.

Sì, è vero, puoi morire di fame ThreadPoolin un processo ASP.NET accodando troppi elementi di lavoro. Impedirà ad ASP.NET di elaborare ulteriori richieste. Le informazioni contenute nell'articolo sono accurate a tale riguardo; lo stesso pool di thread utilizzato per QueueUserWorkItemviene utilizzato anche per servire le richieste.

Ma se stai effettivamente mettendo in coda abbastanza elementi di lavoro da causare questa fame, allora dovresti morire di fame il pool di thread! Se stai eseguendo letteralmente centinaia di operazioni che richiedono un uso intensivo della CPU allo stesso tempo, a cosa sarebbe utile avere un altro thread di lavoro per servire una richiesta ASP.NET, quando la macchina è già sovraccarica? Se ti trovi in ​​questa situazione, devi riprogettare completamente!

La maggior parte delle volte vedo o sento parlare di codice multi-thread utilizzato in modo inappropriato in ASP.NET, non è per accodare il lavoro che richiede molta CPU. Serve per accodare il lavoro associato a I / O. E se si desidera eseguire operazioni di I / O, è necessario utilizzare un thread I / O (porta di completamento I / O).

In particolare, dovresti usare i callback asincroni supportati da qualunque classe di libreria stai usando. Questi metodi sono sempre etichettati molto chiaramente; iniziano con le parole Begine End. Come in Stream.BeginRead, Socket.BeginConnect, WebRequest.BeginGetResponse, e così via.

Questi metodi fanno utilizzare la ThreadPool, ma usano IOCPs, che non non interferiscono con le richieste ASP.NET. Sono un tipo speciale di thread leggero che può essere "svegliato" da un segnale di interruzione dal sistema I / O. E in un'applicazione ASP.NET, normalmente hai un thread I / O per ogni thread di lavoro, quindi ogni singola richiesta può avere un'operazione asincrona accodata. Sono letteralmente centinaia di operazioni asincrone senza alcun calo significativo delle prestazioni (supponendo che il sottosistema di I / O possa tenere il passo). È molto più di quanto avrai mai bisogno.

Tieni presente che i delegati asincroni non funzionano in questo modo: finiranno per utilizzare un thread di lavoro, proprio come ThreadPool.QueueUserWorkItem. Sono solo i metodi asincroni incorporati delle classi della libreria .NET Framework che sono in grado di farlo. Puoi farlo da solo, ma è complicato e un po 'pericoloso e probabilmente oltre lo scopo di questa discussione.

La migliore risposta a questa domanda, a mio parere, è non utilizzare ThreadPool oThread un'istanza in background in ASP.NET . Non è affatto come avviare un thread in un'applicazione Windows Form, dove lo fai per mantenere reattiva l'interfaccia utente e non ti interessa quanto sia efficiente. In ASP.NET, la tua preoccupazione è la velocità effettiva e tutto quel contesto che passa su tutti quei thread di lavoro ucciderà assolutamente la velocità effettiva, indipendentemente dal fatto che utilizzi ThreadPoolo meno.

Per favore, se ti ritrovi a scrivere codice di threading in ASP.NET, considera se potrebbe essere riscritto o meno per utilizzare metodi asincroni preesistenti e, se non è possibile, considera se hai davvero bisogno del codice per essere eseguito in un thread in background. Nella maggior parte dei casi, probabilmente aggiungerai complessità senza alcun vantaggio netto.


Grazie per la risposta dettagliata e hai ragione, cerco di utilizzare metodi asincroni quando possibile (accoppiati con controller asincroni in ASP.NET MVC). Nel caso del mio esempio, con il logger remoto, questo è esattamente quello che posso fare. È un problema di progettazione interessante perché spinge la gestione asincrona fino al livello più basso del codice (ovvero, l'implementazione del logger), invece di poterlo decidere da, diciamo, il livello del controller (in quest'ultimo caso , avresti bisogno, ad esempio, di due implementazioni di logger per poter scegliere).
Michael Hart

@Michael: i callback asincroni sono generalmente abbastanza facili da avvolgere se vuoi aumentarli di più livelli; potresti creare una facciata attorno ai metodi asincroni e avvolgerli con un unico metodo che usa Action<T>come callback, per esempio. Se intendi che la scelta se utilizzare un thread di lavoro o un thread I / O avviene al livello più basso, è intenzionale; solo quel livello può decidere se necessita o meno di un IOCP.
Aaronaught

Anche se, come punto di interesse, è solo .NET ThreadPoolche ti limita in questo modo, probabilmente perché non si fidavano degli sviluppatori per farlo bene. Il pool di thread di Windows non gestito ha un'API molto simile ma in realtà consente di scegliere il tipo di thread.
Aaronaught

2
Porte di completamento I / O (IOCP). la descrizione di IOCP non è del tutto corretta. in IOCP, hai un numero statico di thread di lavoro che, a turno, lavorano su TUTTE le attività in sospeso. da non confondere con i pool di thread che possono essere fissi o dinamici nelle dimensioni MA hanno un thread per attività - scala terribilmente. a differenza di ASYNC, non hai un thread per attività. un thread IOCP potrebbe funzionare un po 'sull'attività 1, quindi passare all'attività 3, all'attività 2 e poi di nuovo all'attività 1. gli stati della sessione di attività vengono salvati e vengono passati tra i thread.
MickyD

1
E gli inserimenti nel database? Esiste un comando SQL ASYNC (come Execute)? Gli inserimenti del database sono circa l'operazione di I / O più lenta in circolazione (a causa del blocco) e avere il thread principale in attesa che le righe vengano inserite è solo uno spreco di cicli della CPU.
Ian Thompson,

45

Secondo Thomas Marquadt del team ASP.NET di Microsoft, è sicuro utilizzare ASP.NET ThreadPool (QueueUserWorkItem).

Dall'articolo :

D) Se la mia applicazione ASP.NET utilizza thread CLR ThreadPool, non morirò di fame ASP.NET, che utilizza anche CLR ThreadPool per eseguire le richieste? ..

A) Per riassumere, non preoccuparti di far morire di fame ASP.NET di thread, e se pensi che ci sia un problema qui fammelo sapere e ci penseremo noi.

D) Devo creare i miei thread (nuovo thread)? Non sarà migliore per ASP.NET, poiché utilizza CLR ThreadPool.

A) Per favore non farlo. O per dirla in modo diverso, no !!! Se sei davvero intelligente, molto più intelligente di me, puoi creare i tuoi fili; altrimenti, non pensarci nemmeno. Ecco alcuni motivi per cui non dovresti creare frequentemente nuovi thread:

  1. È molto costoso, rispetto a QueueUserWorkItem ... A proposito, se riesci a scrivere un ThreadPool migliore di CLR, ti incoraggio a candidarti per un lavoro in Microsoft, perché stiamo sicuramente cercando persone come te !.

4

I siti web non dovrebbero andare in giro a generare thread.

Di solito sposti questa funzionalità in un servizio Windows con cui comunichi (io uso MSMQ per parlare con loro).

-- Modificare

Ho descritto un'implementazione qui: Elaborazione in background basata su code nell'applicazione Web ASP.NET MVC

-- Modificare

Per espandere il motivo per cui questo è anche meglio dei semplici thread:

Utilizzando MSMQ, puoi comunicare con un altro server. È possibile scrivere in una coda su più macchine, quindi se si determina, per qualche motivo, che l'attività in background sta utilizzando troppo le risorse del server principale, è possibile spostarla in modo abbastanza banale.

Ti consente anche di elaborare in batch qualsiasi attività stavi cercando di fare (inviare e-mail / qualsiasi cosa).


4
Non sono d'accordo sul fatto che questa affermazione generale sia sempre vera, specialmente per le attività non critiche. La creazione di un servizio Windows, solo ai fini della registrazione asincrona, sembra decisamente eccessiva. Inoltre, tale opzione non è sempre disponibile (essendo in grado di distribuire MSMQ e / o un servizio Windows).
Michael Hart,

Certo, ma è il modo "standard" per implementare attività asincrone da un sito Web (tema della coda rispetto a qualche altro processo).
Noon Silk

2
Non tutte le attività asincrone vengono create uguali, motivo per cui, ad esempio, le pagine asincrone esistono in ASP.NET. Se desidero recuperare un risultato da visualizzare da un servizio Web remoto, non lo farò tramite MSMQ. In questo caso, sto scrivendo su un log utilizzando un post remoto. Non si adatta al problema scrivere un servizio Windows, né collegare MSMQ per quello (e nemmeno posso dato che questa particolare app è su Azure).
Michael Hart,

1
Considera: stai scrivendo a un host remoto? Cosa succede se l'host è inattivo o non raggiungibile? Vuoi riprovare a scrivere? Forse lo farai, forse no. Con la tua implementazione, è difficile riprovare. Con il servizio diventa abbastanza banale. Apprezzo che tu possa non essere in grado di farlo e lascerò che qualcun altro risponda ai problemi specifici con la creazione di thread da siti Web [ad esempio se il tuo thread non era in background, ecc.], Ma sto delineando il "corretto" modo per farlo. Non ho familiarità con l'azzurro, anche se ho usato ec2 (puoi installare un sistema operativo su quello, quindi tutto va bene).
Noon Silk

@silky, grazie per i commenti. Avevo detto "non critico" per evitare questa soluzione più pesante (ma durevole). Ho chiarito la domanda in modo che sia chiaro che non sto chiedendo best practice per gli elementi di lavoro in coda. Azure supporta questo tipo di scenario (ha il proprio archivio di coda), ma l'operazione di accodamento è troppo costosa per la registrazione sincrona, quindi avrei comunque bisogno di una soluzione asincrona. Nel mio caso sono consapevole delle insidie ​​del fallimento, ma non aggiungerò altra infrastruttura solo nel caso in cui questo particolare provider di registrazione fallisca: ho anche altri fornitori di registrazione.
Michael Hart,

4

Sono assolutamente convinto che la pratica generale per un lavoro asincrono rapido e a bassa priorità in ASP.NET sarebbe quella di utilizzare il pool di thread .NET, in particolare per scenari ad alto traffico poiché si desidera che le risorse siano limitate.

Inoltre, l'implementazione del threading è nascosta: se inizi a generare i tuoi thread, devi gestirli correttamente. Non sto dicendo che non potresti farlo, ma perché reinventare quella ruota?

Se le prestazioni diventano un problema ed è possibile stabilire che il pool di thread è il fattore limitante (e non le connessioni al database, le connessioni di rete in uscita, la memoria, i timeout di pagina ecc.), Si modifica la configurazione del pool di thread per consentire più thread di lavoro, richieste in coda più elevate , eccetera.

Se non si hanno problemi di prestazioni, la scelta di generare nuovi thread per ridurre i conflitti con la coda di richieste ASP.NET è un'ottimizzazione prematura classica.

Idealmente non sarebbe necessario utilizzare un thread separato per eseguire un'operazione di registrazione, ma è sufficiente abilitare il thread originale per completare l'operazione il più rapidamente possibile, ed è qui che entrano in gioco MSMQ e un thread / processo consumer separato. Sono d'accordo che questo è più pesante e richiede più lavoro da implementare, ma qui hai davvero bisogno della durabilità: la volatilità di una coda condivisa in memoria esaurirà rapidamente il suo benvenuto.


2

Dovresti usare QueueUserWorkItem ed evitare di creare nuovi thread come faresti per evitare la piaga. Per un'immagine che spiega perché non morirai di fame ASP.NET, poiché utilizza lo stesso ThreadPool, immagina un giocoliere molto abile che usa due mani per tenere in volo una mezza dozzina di birilli, spade o qualsiasi altra cosa. Per una visuale del motivo per cui creare i propri thread non è corretto, immagina cosa succede a Seattle nelle ore di punta quando le rampe di accesso all'autostrada molto utilizzate consentono ai veicoli di entrare immediatamente nel traffico invece di usare una luce e limitare il numero di ingressi a uno ogni pochi secondi . Infine, per una spiegazione dettagliata, consultare questo collegamento:

http://blogs.msdn.com/tmarq/archive/2010/04/14/performing-asynchronous-work-or-tasks-in-asp-net-applications.aspx

Grazie, Thomas


Quel collegamento è molto utile, grazie per questo Thomas. Sarei interessato a sapere cosa ne pensate anche della risposta di @ Aaronaught.
Michael Hart

Sono d'accordo con Aaronaught e ho detto lo stesso nel mio post sul blog. Detto in questo modo, "Nel tentativo di semplificare questa decisione, dovresti passare [a un altro thread] solo se altrimenti blocchi il thread di richiesta ASP.NET mentre non fai nulla. Questa è una semplificazione eccessiva, ma ci sto provando per rendere semplice la decisione. " In altre parole, non farlo per il lavoro di calcolo non bloccante, ma fallo se stai effettuando richieste di servizio web asincrono a un server remoto. Ascolta Aaronaught! :)
Thomas

1

L'articolo non è corretto. ASP.NET dispone del proprio pool di thread, thread di lavoro gestiti, per servire le richieste ASP.NET. Questo pool è in genere composto da poche centinaia di thread ed è separato dal pool ThreadPool, che è un multiplo più piccolo di processori.

L'utilizzo di ThreadPool in ASP.NET non interferirà con i thread di lavoro ASP.NET. Usare ThreadPool va bene.

Sarebbe anche accettabile impostare un singolo thread che è solo per la registrazione dei messaggi e l'utilizzo del modello produttore / consumatore per passare i messaggi di log a quel thread. In tal caso, poiché il thread è di lunga durata, è necessario creare un unico nuovo thread per eseguire la registrazione.

Usare un nuovo thread per ogni messaggio è decisamente eccessivo.

Un'altra alternativa, se stai parlando solo di logging, è usare una libreria come log4net. Gestisce la registrazione in un thread separato e si prende cura di tutti i problemi di contesto che potrebbero sorgere in quello scenario.


1
@ Sam, sto effettivamente utilizzando log4net e non vedo i log scritti in un thread separato: c'è qualche tipo di opzione che devo abilitare?
Michael Hart,

1

Direi che l'articolo è sbagliato. Se gestisci un grande negozio .NET, puoi utilizzare in sicurezza il pool su più app e più siti Web (utilizzando pool di app separati), semplicemente in base a un'istruzione nella documentazione di ThreadPool :

C'è un pool di thread per processo. Il pool di thread ha una dimensione predefinita di 250 thread di lavoro per processore disponibile e 1000 thread di completamento I / O. Il numero di thread nel pool di thread può essere modificato utilizzando il metodo SetMaxThreads. Ogni thread utilizza la dimensione dello stack predefinita e viene eseguito con la priorità predefinita.


Un'applicazione in esecuzione in un unico processo è completamente in grado di arrestarsi! (O almeno degradare le proprie prestazioni abbastanza da rendere il pool di thread una proposta perdente.)
Jeff Sternal,

Quindi immagino che le richieste ASP.NET utilizzino i thread di completamento I / O (al contrario dei thread di lavoro): è corretto?
Michael Hart,

Dall'articolo di Fritz Onion ho collegato la mia risposta: "Questo paradigma cambia [da IIS 5.0 a IIS 6.0] il modo in cui le richieste vengono gestite in ASP.NET. Invece di inviare richieste da inetinfo.exe al processo di lavoro ASP.NET, http. sys accoda direttamente ogni richiesta nel processo appropriato. Pertanto, tutte le richieste sono ora servite da thread di lavoro estratti dal pool di thread CLR e mai sui thread di I / O. " (la mia enfasi)
Jeff Sternal,

Hmmm, non sono ancora del tutto sicuro ... Questo articolo è del giugno 2003. Se leggi questo del maggio 2004 (certamente ancora piuttosto vecchio), dice "La pagina di test Sleep.aspx può essere usata per mantenere un ASP Thread I / O .NET occupato ", dove Sleep.aspx causa solo la sospensione del thread in esecuzione: msdn.microsoft.com/en-us/library/ms979194.aspx - Quando ho la possibilità, vedrò se riesco a codificare quell'esempio e testare su IIS 7 e .NET 3.5
Michael Hart

Sì, il testo di quel paragrafo è confuso. Più avanti in quella sezione si collega a un argomento di supporto ( support.microsoft.com/default.aspx?scid=kb;EN-US;816829 ) che chiarisce le cose: l'esecuzione di richieste sui thread di completamento I / O era un .NET Framework 1.0 problema risolto in ASP.NET 1.1 giugno 2003 Hotfix Rollup Package (dopo di che "TUTTE le richieste ora vengono eseguite sui thread di lavoro"). Ancora più importante, questo esempio mostra abbastanza chiaramente che il pool di thread ASP.NET è lo stesso pool di thread esposto da System.Threading.ThreadPool.
Jeff Sternal,

1

Mi è stata posta una domanda simile al lavoro la scorsa settimana e ti darò la stessa risposta. Perché utilizzi applicazioni web multi-threading per richiesta? Un server web è un sistema fantastico ottimizzato pesantemente per fornire molte richieste in modo tempestivo (cioè multi threading). Pensa a cosa succede quando richiedi quasi tutte le pagine del Web.

  1. Viene richiesta per qualche pagina
  2. Html viene restituito
  3. L'Html dice al cliente di fare ulteriori richieste (js, css, immagini, ecc ..)
  4. Ulteriori informazioni vengono fornite

Fornisci l'esempio della registrazione remota, ma questa dovrebbe essere una preoccupazione del tuo logger. Dovrebbe essere in atto un processo asincrono per ricevere i messaggi in modo tempestivo. Sam sottolinea anche che il tuo logger (log4net) dovrebbe già supportare questo.

Sam ha anche ragione in quanto l'utilizzo del pool di thread su CLR non causerà problemi con il pool di thread in IIS. La cosa di cui preoccuparsi qui, però, è che non stai generando thread da un processo, stai generando nuovi thread dai thread del pool di thread di IIS. C'è una differenza e la distinzione è importante.

Discussioni vs processo

Sia i thread che i processi sono metodi per parallelizzare un'applicazione. Tuttavia, i processi sono unità di esecuzione indipendenti che contengono le proprie informazioni di stato, utilizzano i propri spazi di indirizzi e interagiscono tra loro solo tramite meccanismi di comunicazione tra processi (generalmente gestiti dal sistema operativo). Le applicazioni sono in genere suddivise in processi durante la fase di progettazione e un processo master genera esplicitamente sottoprocessi quando ha senso separare logicamente funzionalità significative dell'applicazione. I processi, in altre parole, sono un costrutto architettonico.

Al contrario, un thread è un costrutto di codifica che non influisce sull'architettura di un'applicazione. Un singolo processo potrebbe contenere più thread; tutti i thread all'interno di un processo condividono lo stesso stato e lo stesso spazio di memoria e possono comunicare tra loro direttamente, perché condividono le stesse variabili.

fonte


3
@Ty, grazie per l'input, ma sono ben consapevole di come funziona un server web e non è realmente rilevante per la domanda - ancora una volta, come ho detto nella domanda, non sto chiedendo indicazioni su questo come architettura problema. Chiedo informazioni tecniche specifiche. Per quanto riguarda il fatto che sia "la preoccupazione del logger" che dovrebbe già avere un processo asincrono in atto, come pensi che il processo asincrono dovrebbe essere scritto dall'implementazione del logger?
Michael Hart,

0

Non sono d'accordo con l'articolo di riferimento (C # feeds.com). È facile creare un nuovo thread ma pericoloso. Il numero ottimale di thread attivi da eseguire su un singolo core è in realtà sorprendentemente basso - meno di 10. È troppo facile far perdere tempo alla macchina nel cambiare thread se vengono creati thread per attività minori. I thread sono una risorsa che RICHIEDE la gestione. L'astrazione WorkItem è lì per gestire questo.

C'è un compromesso tra la riduzione del numero di thread disponibili per le richieste e la creazione di troppi thread per consentire a nessuno di essi di elaborare in modo efficiente. Questa è una situazione molto dinamica, ma penso che dovrebbe essere gestita attivamente (in questo caso dal pool di thread) piuttosto che lasciare che sia il processore a stare al passo con la creazione dei thread.

Infine l'articolo fa alcune affermazioni piuttosto radicali sui pericoli dell'uso di ThreadPool, ma ha davvero bisogno di qualcosa di concreto per sostenerle.


0

Indipendentemente dal fatto che IIS utilizzi o meno lo stesso ThreadPool per gestire le richieste in arrivo, sembra difficile ottenere una risposta definitiva e sembra anche essere cambiato rispetto alle versioni. Quindi sembrerebbe una buona idea non utilizzare i thread ThreadPool in modo eccessivo, in modo che IIS ne abbia molti disponibili. D'altra parte, generare il proprio thread per ogni piccola attività sembra una cattiva idea. Presumibilmente, hai una sorta di blocco nella registrazione, quindi solo un thread potrebbe progredire alla volta, e il resto farebbe a turno per essere pianificato e non programmato (per non parlare del sovraccarico di generare un nuovo thread). In sostanza, ti imbatti nei problemi esatti che ThreadPool è stato progettato per evitare.

Sembra che un compromesso ragionevole sarebbe che la tua app allochi un singolo thread di registrazione a cui potresti passare i messaggi. Dovresti fare attenzione che l'invio di messaggi sia il più veloce possibile in modo da non rallentare la tua app.


0

È possibile utilizzare Parallel.For o Parallel.ForEach e definire il limite di possibili thread che si desidera allocare per eseguire senza problemi e prevenire la fame del pool.

Tuttavia, essendo eseguito in background, sarà necessario utilizzare lo stile TPL puro di seguito nell'applicazione Web ASP.Net.

var ts = new CancellationTokenSource();
CancellationToken ct = ts.Token;

ParallelOptions po = new ParallelOptions();
            po.CancellationToken = ts.Token;
            po.MaxDegreeOfParallelism = 6; //limit here

 Task.Factory.StartNew(()=>
                {                        
                  Parallel.ForEach(collectionList, po, (collectionItem) =>
                  {
                     //Code Here PostLog(logEvent);
                  }
                });
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.