Come limitare la quantità di operazioni I / O asincrone simultanee?


115
// let's say there is a list of 1000+ URLs
string[] urls = { "http://google.com", "http://yahoo.com", ... };

// now let's send HTTP requests to each of these URLs in parallel
urls.AsParallel().ForAll(async (url) => {
    var client = new HttpClient();
    var html = await client.GetStringAsync(url);
});

Ecco il problema, avvia più di 1000 richieste web simultanee. Esiste un modo semplice per limitare la quantità simultanea di queste richieste http asincrone? In modo che non vengano scaricate più di 20 pagine web in un dato momento. Come farlo nel modo più efficiente?


2
In che modo è diverso dalla tua domanda precedente ?
svick

1
stackoverflow.com/questions/9290498/… Con un parametro ParallelOptions.
Chris Disley

4
@ChrisDisley, questo parallelizzerà solo il lancio delle richieste.
spender

@svick ha ragione, in cosa è diverso? btw, adoro la risposta lì stackoverflow.com/a/10802883/66372
eglasius

3
Inoltre lo HttpClientè IDisposable, e dovresti smaltirlo, soprattutto quando ne utilizzerai più di 1000. HttpClientpuò essere utilizzato come singleton per più richieste.
Shimmy Weitzhandler

Risposte:


161

Puoi sicuramente farlo nelle ultime versioni di async per .NET, utilizzando .NET 4.5 Beta. Il post precedente di "usr" punta a un buon articolo scritto da Stephen Toub, ma la notizia meno annunciata è che il semaforo asincrono è effettivamente entrato nella versione Beta di .NET 4.5

Se guardi la nostra amata SemaphoreSlimclasse (che dovresti usare poiché è più performante dell'originale Semaphore), ora vanta la WaitAsync(...)serie di sovraccarichi, con tutti gli argomenti previsti: intervalli di timeout, token di cancellazione, tutti i tuoi soliti amici di pianificazione: )

Stephen ha anche scritto un post sul blog più recente sui nuovi gadget .NET 4.5 che sono usciti con la versione beta, vedere Novità per il parallelismo in .NET 4.5 Beta .

Infine, ecco un po 'di codice di esempio su come utilizzare SemaphoreSlim per la limitazione del metodo asincrono:

public async Task MyOuterMethod()
{
    // let's say there is a list of 1000+ URLs
    var urls = { "http://google.com", "http://yahoo.com", ... };

    // now let's send HTTP requests to each of these URLs in parallel
    var allTasks = new List<Task>();
    var throttler = new SemaphoreSlim(initialCount: 20);
    foreach (var url in urls)
    {
        // do an async wait until we can schedule again
        await throttler.WaitAsync();

        // using Task.Run(...) to run the lambda in its own parallel
        // flow on the threadpool
        allTasks.Add(
            Task.Run(async () =>
            {
                try
                {
                    var client = new HttpClient();
                    var html = await client.GetStringAsync(url);
                }
                finally
                {
                    throttler.Release();
                }
            }));
    }

    // won't get here until all urls have been put into tasks
    await Task.WhenAll(allTasks);

    // won't get here until all tasks have completed in some way
    // (either success or exception)
}

Infine, ma probabilmente una menzione degna di nota è una soluzione che utilizza la pianificazione basata su TPL. È possibile creare attività associate a delegato sul TPL che non sono state ancora avviate e consentire a un'utilità di pianificazione delle attività personalizzata per limitare la concorrenza. In effetti, c'è un esempio MSDN qui:

Vedi anche TaskScheduler .


3
un parallelismo per ciascuno con un grado limitato di parallelismo non è un approccio migliore? msdn.microsoft.com/en-us/library/...
GreyCloud

2
Perché non ti HttpClient
sbarazzi

4
@GreyCloud: Parallel.ForEachfunziona con codice sincrono. Ciò consente di chiamare codice asincrono.
Josh Noe

2
@TheMonarch ti sbagli . Inoltre è sempre una buona abitudine racchiudere tutti IDisposablei messaggi usingo try-finallydichiarazioni e assicurarne lo smaltimento.
Shimmy Weitzhandler

29
Data la popolarità di questa risposta, vale la pena sottolineare che HttpClient può e deve essere una singola istanza comune anziché un'istanza per richiesta.
Rupert Rawnsley

15

Se hai un IEnumerable (cioè stringhe di URL) e vuoi eseguire un'operazione di I / O con ciascuno di questi (es. Fare una richiesta http asincrona) contemporaneamente E opzionalmente vuoi anche impostare il numero massimo di simultanei Richieste di I / O in tempo reale, ecco come puoi farlo. In questo modo non si utilizza il pool di thread e altri, il metodo utilizza semaphoreslim per controllare il numero massimo di richieste di I / O simultanee simile a un modello di finestra scorrevole che una richiesta completa, lascia il semaforo e arriva quella successiva.

utilizzo: await ForEachAsync (urlStrings, YourAsyncFunc, optionalMaxDegreeOfConcurrency);

public static Task ForEachAsync<TIn>(
        IEnumerable<TIn> inputEnumerable,
        Func<TIn, Task> asyncProcessor,
        int? maxDegreeOfParallelism = null)
    {
        int maxAsyncThreadCount = maxDegreeOfParallelism ?? DefaultMaxDegreeOfParallelism;
        SemaphoreSlim throttler = new SemaphoreSlim(maxAsyncThreadCount, maxAsyncThreadCount);

        IEnumerable<Task> tasks = inputEnumerable.Select(async input =>
        {
            await throttler.WaitAsync().ConfigureAwait(false);
            try
            {
                await asyncProcessor(input).ConfigureAwait(false);
            }
            finally
            {
                throttler.Release();
            }
        });

        return Task.WhenAll(tasks);
    }


no, non dovresti aver bisogno di disporre esplicitamente SemaphoreSlim in questa implementazione e utilizzo poiché viene utilizzato internamente all'interno del metodo e il metodo non accede alla sua proprietà AvailableWaitHandle, nel qual caso avremmo avuto bisogno di eliminarlo o avvolgerlo in un blocco using.
Dogu Arslan

1
Penso solo alle migliori pratiche e lezioni che insegniamo ad altre persone. Una usingsarebbe bello.
AgentFire

beh, questo esempio posso seguire, ma provando a capire qual è il modo migliore per farlo, fondamentalmente ho un throttler ma il mio Func restituirebbe un elenco, che alla fine voglio in un elenco finale di tutti i completati quando fatto ... che può richiedono bloccato sulla lista, hai suggerimenti.
Seabizkit

puoi aggiornare leggermente il metodo in modo che restituisca l'elenco delle attività effettive e attendi Task.WhenAll dall'interno del codice chiamante. Una volta che Task.WhenAll è completo, è possibile enumerare ciascuna attività nell'elenco e aggiungerne l'elenco all'elenco finale. Cambia la firma del metodo in 'public static IEnumerable <Task <TOut>> ForEachAsync <TIn, TOut> (IEnumerable <TIn> inputEnumerable, Func <TIn, Task <TOut>> asyncProcessor, int? MaxDegreeOfParallelism = null)'
Dogu Arslan

7

Sfortunatamente, in .NET Framework mancano i combinatori più importanti per orchestrare attività asincrone parallele. Non esiste una cosa del genere incorporata.

Guarda la classe AsyncSemaphore costruita dal più rispettabile Stephen Toub. Quello che vuoi è chiamato semaforo e hai bisogno di una versione asincrona di esso.


12
Notare che "Sfortunatamente, in .NET Framework mancano i combinatori più importanti per l'orchestrazione di attività asincrone parallele. Non esiste qualcosa di simile integrato". non è più corretto a partire da .NET 4.5 Beta. SemaphoreSlim ora offre la funzionalità WaitAsync (...) :)
Theo Yaung

SemaphoreSlim (con i suoi nuovi metodi asincroni) dovrebbe essere preferito ad AsyncSemphore, o l'implementazione di Toub ha ancora qualche vantaggio?
Todd Menier

A mio avviso, il tipo da incasso dovrebbe essere preferito perché è probabile che sia ben testato e ben progettato.
usr

4
Stephen ha aggiunto un commento in risposta a una domanda sul suo post sul blog confermando che l'utilizzo di SemaphoreSlim per .NET 4.5 sarebbe generalmente la strada da percorrere.
jdasilva

7

Ci sono molte insidie ​​e l'uso diretto di un semaforo può essere complicato nei casi di errore, quindi suggerirei di utilizzare AsyncEnumerator NuGet Package invece di reinventare la ruota:

// let's say there is a list of 1000+ URLs
string[] urls = { "http://google.com", "http://yahoo.com", ... };

// now let's send HTTP requests to each of these URLs in parallel
await urls.ParallelForEachAsync(async (url) => {
    var client = new HttpClient();
    var html = await client.GetStringAsync(url);
}, maxDegreeOfParalellism: 20);

4

L'esempio di Theo Yaung è carino, ma esiste una variante senza elenco di attività in attesa.

 class SomeChecker
 {
    private const int ThreadCount=20;
    private CountdownEvent _countdownEvent;
    private SemaphoreSlim _throttler;

    public Task Check(IList<string> urls)
    {
        _countdownEvent = new CountdownEvent(urls.Count);
        _throttler = new SemaphoreSlim(ThreadCount); 

        return Task.Run( // prevent UI thread lock
            async  () =>{
                foreach (var url in urls)
                {
                    // do an async wait until we can schedule again
                    await _throttler.WaitAsync();
                    ProccessUrl(url); // NOT await
                }
                //instead of await Task.WhenAll(allTasks);
                _countdownEvent.Wait();
            });
    }

    private async Task ProccessUrl(string url)
    {
        try
        {
            var page = await new WebClient()
                       .DownloadStringTaskAsync(new Uri(url)); 
            ProccessResult(page);
        }
        finally
        {
            _throttler.Release();
            _countdownEvent.Signal();
        }
    }

    private void ProccessResult(string page){/*....*/}
}

4
Nota, c'è un pericolo nell'usare questo approccio: tutte le eccezioni che si verificano in ProccessUrlo le sue sottofunzioni verranno effettivamente ignorate. Verranno acquisiti in Tasks, ma non ritrasmessi al chiamante originale di Check(...). Personalmente, ecco perché uso ancora Tasks e le loro funzioni combinatrici come WhenAlle WhenAny- per ottenere una migliore propagazione degli errori. :)
Theo Yaung

3

SemaphoreSlim può essere molto utile qui. Ecco il metodo di estensione che ho creato.

    /// <summary>
    /// Concurrently Executes async actions for each item of <see cref="IEnumerable<typeparamref name="T"/>
    /// </summary>
    /// <typeparam name="T">Type of IEnumerable</typeparam>
    /// <param name="enumerable">instance of <see cref="IEnumerable<typeparamref name="T"/>"/></param>
    /// <param name="action">an async <see cref="Action" /> to execute</param>
    /// <param name="maxActionsToRunInParallel">Optional, max numbers of the actions to run in parallel,
    /// Must be grater than 0</param>
    /// <returns>A Task representing an async operation</returns>
    /// <exception cref="ArgumentOutOfRangeException">If the maxActionsToRunInParallel is less than 1</exception>
    public static async Task ForEachAsyncConcurrent<T>(
        this IEnumerable<T> enumerable,
        Func<T, Task> action,
        int? maxActionsToRunInParallel = null)
    {
        if (maxActionsToRunInParallel.HasValue)
        {
            using (var semaphoreSlim = new SemaphoreSlim(
                maxActionsToRunInParallel.Value, maxActionsToRunInParallel.Value))
            {
                var tasksWithThrottler = new List<Task>();

                foreach (var item in enumerable)
                {
                    // Increment the number of currently running tasks and wait if they are more than limit.
                    await semaphoreSlim.WaitAsync();

                    tasksWithThrottler.Add(Task.Run(async () =>
                    {
                        await action(item).ContinueWith(res =>
                        {
                            // action is completed, so decrement the number of currently running tasks
                            semaphoreSlim.Release();
                        });
                    }));
                }

                // Wait for all of the provided tasks to complete.
                await Task.WhenAll(tasksWithThrottler.ToArray());
            }
        }
        else
        {
            await Task.WhenAll(enumerable.Select(item => action(item)));
        }
    }

Utilizzo di esempio:

await enumerable.ForEachAsyncConcurrent(
    async item =>
    {
        await SomeAsyncMethod(item);
    },
    5);

0

Vecchia domanda, nuova risposta. @vitidev aveva un blocco di codice che è stato riutilizzato quasi intatto in un progetto che ho recensito. Dopo aver discusso con alcuni colleghi, uno ha chiesto "Perché non usi semplicemente i metodi TPL incorporati?" ActionBlock sembra il vincitore lì. https://msdn.microsoft.com/en-us/library/hh194773(v=vs.110).aspx . Probabilmente non finirà per modificare alcun codice esistente, ma cercherà sicuramente di adottare questo nuget e riutilizzare le migliori pratiche di Mr. Softy per il parallelismo limitato.


0

Ecco una soluzione che sfrutta la natura pigra di LINQ. È funzionalmente equivalente alla risposta accettata ), ma utilizza attività di lavoro invece di a SemaphoreSlim, riducendo in questo modo l'impronta di memoria dell'intera operazione. Inizialmente lascia che funzioni senza strozzature. Il primo passo è convertire i nostri URL in un elenco di attività.

string[] urls =
{
    "https://stackoverflow.com",
    "https://superuser.com",
    "https://serverfault.com",
    "https://meta.stackexchange.com",
    // ...
};
var httpClient = new HttpClient();
var tasks = urls.Select(async (url) =>
{
    return (Url: url, Html: await httpClient.GetStringAsync(url));
});

Il secondo passaggio è per awaittutte le attività contemporaneamente utilizzando il Task.WhenAllmetodo:

var results = await Task.WhenAll(tasks);
foreach (var result in results)
{
    Console.WriteLine($"Url: {result.Url}, {result.Html.Length:#,0} chars");
}

Produzione:

Url: https://stackoverflow.com , 105.574 chars
Url: https://superuser.com , 126.953 chars
Url: https://serverfault.com , 125.963 chars
Url: https://meta.stackexchange.com , 185.276 chars
...

L'implementazione di MicrosoftTask.WhenAll materializza istantaneamente l'enumerabile fornito in un array, facendo in modo che tutte le attività vengano avviate contemporaneamente. Non lo vogliamo, perché vogliamo limitare il numero di operazioni asincrone simultanee. Quindi avremo bisogno di implementare un'alternativa WhenAllche enumererà il nostro enumerabile dolcemente e lentamente. Lo faremo creando un numero di attività di lavoro (pari al livello di concorrenza desiderato), e ciascuna attività di lavoro enumererà la nostra attività enumerabile alla volta, utilizzando un blocco per garantire che ogni attività url venga elaborata da una sola attività lavorativa. Quindi dobbiamo completare awaittutte le attività dei lavoratori e infine restituiamo i risultati. Ecco l'implementazione:

public static async Task<T[]> WhenAll<T>(IEnumerable<Task<T>> tasks,
    int concurrencyLevel)
{
    if (tasks is ICollection<Task<T>>) throw new ArgumentException(
        "The enumerable should not be materialized.", nameof(tasks));
    var locker = new object();
    var results = new List<T>();
    var failed = false;
    using (var enumerator = tasks.GetEnumerator())
    {
        var workerTasks = Enumerable.Range(0, concurrencyLevel)
        .Select(async _ =>
        {
            try
            {
                while (true)
                {
                    Task<T> task;
                    int index;
                    lock (locker)
                    {
                        if (failed) break;
                        if (!enumerator.MoveNext()) break;
                        task = enumerator.Current;
                        index = results.Count;
                        results.Add(default); // Reserve space in the list
                    }
                    var result = await task.ConfigureAwait(false);
                    lock (locker) results[index] = result;
                }
            }
            catch (Exception)
            {
                lock (locker) failed = true;
                throw;
            }
        }).ToArray();
        await Task.WhenAll(workerTasks).ConfigureAwait(false);
    }
    lock (locker) return results.ToArray();
}

... ed ecco cosa dobbiamo cambiare nel nostro codice iniziale, per ottenere il throttling desiderato:

var results = await WhenAll(tasks, concurrencyLevel: 2);

C'è una differenza per quanto riguarda la gestione delle eccezioni. Il nativo Task.WhenAllattende il completamento di tutte le attività e aggrega tutte le eccezioni. L'implementazione di cui sopra termina immediatamente dopo il completamento della prima attività con errore.


L'implementazione di AC # 8 che restituisce un IAsyncEnumerable<T>può essere trovata qui .
Theodor Zoulias,

-1

Sebbene 1000 attività possano essere accodate molto rapidamente, la libreria Attività parallele può gestire solo attività simultanee pari alla quantità di core della CPU nella macchina. Ciò significa che se si dispone di una macchina a quattro core, verranno eseguite solo 4 attività in un dato momento (a meno che non si abbassi MaxDegreeOfParallelism).


8
Sì, ma questo non si riferisce alle operazioni di I / O asincrone. Il codice sopra attiverà più di 1000 download simultanei anche se è in esecuzione su un singolo thread.
Grief Coder

Non ho visto la awaitparola chiave qui. Rimuoverlo dovrebbe risolvere il problema, giusto?
scottm

2
La libreria può certamente gestire più attività in esecuzione (con lo Runningstato) contemporaneamente rispetto alla quantità di core. Ciò sarà particolarmente il caso di attività associate a I / O.
svick

@svick: yep. Sai come controllare in modo efficiente il numero massimo di attività TPL simultanee (non thread)?
Grief Coder

-1

I calcoli paralleli dovrebbero essere usati per velocizzare le operazioni legate alla CPU. Qui stiamo parlando di operazioni di I / O vincolate. La tua implementazione dovrebbe essere puramente asincrona , a meno che tu non stia sovraccaricando il singolo core occupato sulla tua CPU multi-core.

MODIFICA Mi piace il suggerimento fatto da usr di usare un "semaforo asincrono" qui.


Buon punto! Sebbene ogni attività qui conterrà codice asincrono e di sincronizzazione (pagina scaricata in modo asincrono e quindi elaborata in modo sincronizzato). Sto cercando di distribuire la parte di sincronizzazione del codice tra le CPU e allo stesso tempo limitare la quantità di operazioni I / O asincrone simultanee.
Grief Coder

Perché? Perché l'avvio simultaneo di più di 1000 richieste http potrebbe non essere un'attività adatta alla capacità di rete dell'utente.
spender

Le estensioni parallele possono anche essere utilizzate come metodo per multiplexare le operazioni di I / O senza dover implementare manualmente una soluzione asincrona pura. Il che sono d'accordo potrebbe essere considerato sciatto, ma fintanto che mantieni un limite stretto al numero di operazioni simultanee probabilmente non metterà a dura prova il threadpool.
Sean U

3
Non credo che questa risposta fornisca una risposta. Essere puramente asincroni non è sufficiente qui: vogliamo davvero limitare gli IO fisici in modo non bloccante.
usr

1
Hmm .. non sono sicuro di essere d'accordo ... quando si lavora su un progetto di grandi dimensioni, se uno troppi sviluppatori ha questa visione, si verrà a morire di fame anche se il contributo di ogni sviluppatore da solo non è sufficiente per ribaltare le cose al limite. Dato che c'è un solo ThreadPool, anche se lo stai trattando in modo semi-rispettoso ... se tutti gli altri stanno facendo lo stesso, i problemi possono seguire. In quanto tale, sconsiglio sempre di eseguire cose lunghe nel ThreadPool.
spender

-1

Usa MaxDegreeOfParallelism, che è un'opzione che puoi specificare in Parallel.ForEach():

var options = new ParallelOptions { MaxDegreeOfParallelism = 20 };

Parallel.ForEach(urls, options,
    url =>
        {
            var client = new HttpClient();
            var html = client.GetStringAsync(url);
            // do stuff with html
        });

4
Non credo che funzioni. GetStringAsync(url)è pensato per essere chiamato con await. Se controlli il tipo di var html, è un Task<string>, non il risultato string.
Neal Ehardt

2
@NealEhardt è corretto. Parallel.ForEach(...)è inteso per eseguire blocchi di codice sincrono in parallelo (ad esempio su thread diversi).
Theo Yaung

-1

In sostanza, vorrai creare un'azione o un'attività per ogni URL che desideri raggiungere, inserirli in un elenco e quindi elaborare tale elenco, limitando il numero che può essere elaborato in parallelo.

Il mio post sul blog mostra come farlo sia con Attività che con Azioni e fornisce un progetto di esempio che puoi scaricare ed eseguire per vedere entrambi in azione.

Con azioni

Se si utilizza Actions, è possibile utilizzare la funzione .Net Parallel.Invoke incorporata. Qui lo limitiamo a eseguire al massimo 20 thread in parallelo.

var listOfActions = new List<Action>();
foreach (var url in urls)
{
    var localUrl = url;
    // Note that we create the Task here, but do not start it.
    listOfTasks.Add(new Task(() => CallUrl(localUrl)));
}

var options = new ParallelOptions {MaxDegreeOfParallelism = 20};
Parallel.Invoke(options, listOfActions.ToArray());

Con attività

Con Tasks non esiste una funzione integrata. Tuttavia, puoi utilizzare quello che fornisco sul mio blog.

    /// <summary>
    /// Starts the given tasks and waits for them to complete. This will run, at most, the specified number of tasks in parallel.
    /// <para>NOTE: If one of the given tasks has already been started, an exception will be thrown.</para>
    /// </summary>
    /// <param name="tasksToRun">The tasks to run.</param>
    /// <param name="maxTasksToRunInParallel">The maximum number of tasks to run in parallel.</param>
    /// <param name="cancellationToken">The cancellation token.</param>
    public static async Task StartAndWaitAllThrottledAsync(IEnumerable<Task> tasksToRun, int maxTasksToRunInParallel, CancellationToken cancellationToken = new CancellationToken())
    {
        await StartAndWaitAllThrottledAsync(tasksToRun, maxTasksToRunInParallel, -1, cancellationToken);
    }

    /// <summary>
    /// Starts the given tasks and waits for them to complete. This will run the specified number of tasks in parallel.
    /// <para>NOTE: If a timeout is reached before the Task completes, another Task may be started, potentially running more than the specified maximum allowed.</para>
    /// <para>NOTE: If one of the given tasks has already been started, an exception will be thrown.</para>
    /// </summary>
    /// <param name="tasksToRun">The tasks to run.</param>
    /// <param name="maxTasksToRunInParallel">The maximum number of tasks to run in parallel.</param>
    /// <param name="timeoutInMilliseconds">The maximum milliseconds we should allow the max tasks to run in parallel before allowing another task to start. Specify -1 to wait indefinitely.</param>
    /// <param name="cancellationToken">The cancellation token.</param>
    public static async Task StartAndWaitAllThrottledAsync(IEnumerable<Task> tasksToRun, int maxTasksToRunInParallel, int timeoutInMilliseconds, CancellationToken cancellationToken = new CancellationToken())
    {
        // Convert to a list of tasks so that we don't enumerate over it multiple times needlessly.
        var tasks = tasksToRun.ToList();

        using (var throttler = new SemaphoreSlim(maxTasksToRunInParallel))
        {
            var postTaskTasks = new List<Task>();

            // Have each task notify the throttler when it completes so that it decrements the number of tasks currently running.
            tasks.ForEach(t => postTaskTasks.Add(t.ContinueWith(tsk => throttler.Release())));

            // Start running each task.
            foreach (var task in tasks)
            {
                // Increment the number of tasks currently running and wait if too many are running.
                await throttler.WaitAsync(timeoutInMilliseconds, cancellationToken);

                cancellationToken.ThrowIfCancellationRequested();
                task.Start();
            }

            // Wait for all of the provided tasks to complete.
            // We wait on the list of "post" tasks instead of the original tasks, otherwise there is a potential race condition where the throttler's using block is exited before some Tasks have had their "post" action completed, which references the throttler, resulting in an exception due to accessing a disposed object.
            await Task.WhenAll(postTaskTasks.ToArray());
        }
    }

E quindi creando il tuo elenco di attività e chiamando la funzione per farle funzionare, diciamo un massimo di 20 simultanee alla volta, potresti fare questo:

var listOfTasks = new List<Task>();
foreach (var url in urls)
{
    var localUrl = url;
    // Note that we create the Task here, but do not start it.
    listOfTasks.Add(new Task(async () => await CallUrl(localUrl)));
}
await Tasks.StartAndWaitAllThrottledAsync(listOfTasks, 20);

Penso che tu stia solo specificando initialCount per SemaphoreSlim e devi specificare il secondo parametro cioè maxCount nel costruttore di SemaphoreSlim.
Jay Shah

Voglio che ogni risposta da ogni attività venga elaborata in un elenco. Come posso ottenere il risultato o la risposta del
reso

-1

questa non è una buona pratica in quanto cambia una variabile globale. inoltre non è una soluzione generale per async. ma è facile per tutte le istanze di HttpClient, se questo è tutto ciò che cerchi. puoi semplicemente provare:

System.Net.ServicePointManager.DefaultConnectionLimit = 20;
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.