web-dev-qa-db-fra.com

Comment rendre deux requêtes SQL vraiment asynchrones

Mon problème est basé sur un problème de projet réel, mais je n’ai jamais utilisé la bibliothèque System.Threading.Tasks ni effectué de programmation sérieuse impliquant des threads; ma question peut donc être un mélange de connaissances insuffisantes sur la bibliothèque spécifique et de malentendus plus généraux sur ce que signifie vraiment asynchrone de la programmation.

Mon cas réel est donc le suivant: je dois récupérer des données sur un utilisateur. Dans mon scénario actuel, il s'agit de données financières, alors disons qu'il me faut tous les Accounts, tous les Deposits et tous les Consignations pour un utilisateur donné. Dans mon cas, cela signifie interroger des millions d'enregistrements pour chaque propriété et chaque requête est relativement lente elle-même. Cependant, l'extraction de Accounts est plusieurs fois plus lente que l'extraction de Deposits. J'ai donc défini trois classes pour les trois produits bancaires que je vais utiliser et lorsque je veux extraire les données de tous les produits bancaires de certains utilisateurs, je fais quelque chose comme ceci:

List<Account> accounts = GetAccountsForClient(int clientId);
List<Deposit> deposits = GetDepositsForClient(int clientId);
List<Consignation> consignations = GetConsignationsForClient(int clientId);

Le problème commence donc ici. Je dois obtenir ces trois listes en même temps, car je vais les passer à la vue où j’affiche toutes les données des utilisateurs. Mais comme c'est le cas actuellement, l'exécution est synchrone (si j'utilise correctement le terme ici), le temps total de collecte des données pour les trois produits est donc le suivant:

Total_Time = Time_To_Get_Accounts + Time_To_Get_Deposits + Time_To_Get_Consignations

Ce n’est pas bon, car chaque requête est relativement lente, donc le temps total est assez long, mais la requête accounts prend beaucoup plus de temps que les deux autres requêtes. exécuter cette requête simultanément ". Voici peut-être mon plus grand malentendu sur le sujet, mais pour moi, l'idée la plus proche est de les rendre asynchrones. Alors peut-être que Total_Time ne sera pas le temps de la requête la plus lente, mais sera beaucoup plus rapide que la somme des trois requêtes.

Depuis que mon code est compliqué, j'ai créé un cas d'utilisation simple qui, je pense, reflète bien ce que j'essaie de faire. J'ai deux méthodes:

public static async Task<int> GetAccounts()
{
    int total1 = 0;
    using (SqlConnection connection = new SqlConnection(connString))
    {
        string query1 = "SELECT COUNT(*) FROM [MyDb].[dbo].[Accounts]";
        SqlCommand command = new SqlCommand(query1, connection);
        connection.Open();
        for (int i = 0; i < 19000000; i++)
        {
            string s = i.ToString();
        }
        total1 = (int) await command.ExecuteScalarAsync();
        Console.WriteLine(total1.ToString());
    }
    return total1;
}

et la seconde méthode:

public static async Task<int> GetDeposits()
{
    int total2 = 0;
    using (SqlConnection connection = new SqlConnection(connString))
    {
        string query2 = "SELECT COUNT(*) FROM [MyDb].[dbo].[Deposits]";
        SqlCommand command = new SqlCommand(query2, connection);
        connection.Open();
        total2 = (int) await command.ExecuteScalarAsync();
        Console.WriteLine(total2.ToString());
    }
    return total2;
}

que j'appelle comme ceci: 

static void Main(string[] args)
{
    Console.WriteLine(GetAccounts().Result.ToString());

    Console.WriteLine(GetDeposits().Result.ToString());
}

Comme vous pouvez le voir, j'appelle d'abord GetAccounts() et je ralentis volontairement l'exécution afin que je laisse une chance à l'exécution de continuer avec la méthode suivante. Cependant, je n'obtiens aucun résultat pendant un certain temps, puis tout est imprimé en même temps sur la console.

Donc le problème - comment faire en sorte que je n’attende pas la fin de la première méthode pour passer à la méthode suivante. En général, la structure du code n’est pas très importante. Ce que je veux vraiment savoir, c’est de savoir s’il est possible de faire en sorte que les deux requêtes soient exécutées en même temps. L'échantillon ci-dessous est le résultat de mes recherches qui pourraient peut-être être étendues au point d'obtenir le résultat souhaité.

P.S J'utilise ExecuteScalarAsync(); juste parce que j'ai commencé avec une méthode qui l'utilisait. En réalité, je vais utiliser Scalar et Reader.

16
Leron

Lorsque vous utilisez la propriété Result sur une tâche qui n'est pas encore terminée, le thread appelant bloque jusqu'à la fin de l'opération. Cela signifie que, dans votre cas, l'opération GetAccounts doit être terminée avant que l'appel de GetDeposits ne commence.

Si vous voulez vous assurer que ces méthodes sont parallèles (y compris les parties synchrone gourmandes en ressources processeur), vous devez les décharger sur un autre thread. Le moyen le plus simple de le faire serait d'utiliser Task.Run:

static void Main(string[] args)
{
    var accountTask = Task.Run(async () => Console.WriteLine(await GetAccounts()));
    var depositsTask = Task.Run(async () => Console.WriteLine(await GetDeposits()));

    Task.WhenAll(accountTask, depositsTask).Wait();
}

Étant donné que Main ne peut pas être async et ne peut donc pas utiliser await, vous pouvez simplement appeler cette méthode et attendre sa fin de manière synchrone à l'aide de Wait.

14
i3arnon

Voici un moyen d'effectuer deux tâches de manière asynchrone et en parallèle:

Task<int> accountTask = GetAccounts();
Task<int> depositsTask = GetDeposits();

int[] results = await Task.WhenAll(accountTask, depositsTask);

int accounts = results[0];
int deposits = results[1];
6
abatishchev

Je préfère généralement utiliser Task.WaitAll. Pour configurer ce segment de code, j'ai modifié les signatures GetAccounts/GetDeposits uniquement pour renvoyer int (public static int GetAccounts())

J'ai placé Console.WriteLine dans le même fil que l'attribution du retour pour valider le retour d'un GetDeposits avant GetAccounts, mais cela est inutile et probablement préférable de le déplacer après le Task.WaitAll

     private static void Main(string[] args) {

        int getAccountsTask = 0;
        int getDepositsTask = 0;
        List<Task> tasks = new List<Task>() {
            Task.Factory.StartNew(() => {
                getAccountsTask = GetAccounts();
                Console.WriteLine(getAccountsTask);
            }),
            Task.Factory.StartNew(() => {
                getDepositsTask = GetDeposits();
                Console.WriteLine(getDepositsTask);

            })

        };
        Task.WaitAll(tasks.ToArray());



    }
2
AWinkle

Si c'est ASP.Net Utilisez AJAX pour récupérer après le rendu de la page et placez les données dans un magasin. Chaque extraction AJAX est asynchrone. Si vous souhaitez créer des requêtes SQL simultanées sur le serveur?

Usage:

 // Add some queries ie. ThreadedQuery.NamedQuery([Name], [SQL])
 var namedQueries= new ThreadedQuery.NamedQuery[]{ ... };

 System.Data.DataSet ds = ThreadedQuery.RunThreadedQuery(
 "Server=foo;Database=bar;Trusted_Connection=True;", 
 namedQueries).Result;


 string msg = string.Empty;
 foreach (System.Data.DataTable tt in ds.Tables)
 msg += string.Format("{0}: {1}\r\n", tt.TableName, tt.Rows.Count);

La source:

public class ThreadedQuery
{

    public class NamedQuery
    {
        public NamedQuery(string TableName, string SQL)
        {
            this.TableName = TableName;
            this.SQL = SQL;
        }
        public string TableName { get; set; }
        public string SQL { get; set; }
    }
    public static async System.Threading.Tasks.Task<System.Data.DataSet> RunThreadedQuery(string ConnectionString, params NamedQuery[] queries)
    {

        System.Data.DataSet dss = new System.Data.DataSet();
        List<System.Threading.Tasks.Task<System.Data.DataTable>> asyncQryList = new List<System.Threading.Tasks.Task<System.Data.DataTable>>();

        foreach (var qq in queries)
            asyncQryList.Add(fetchDataTable(qq, ConnectionString));

        foreach (var tsk in asyncQryList)
        {
            System.Data.DataTable tmp = await tsk.ConfigureAwait(false);
            dss.Tables.Add(tmp);
        }

        return dss;

    }

    private static async System.Threading.Tasks.Task<System.Data.DataTable> fetchDataTable(NamedQuery qry, string ConnectionString)
    {
        // Create a connection, open it and create a command on the connection
        try
        {

            System.Data.DataTable dt = new System.Data.DataTable(qry.TableName);
            using (SqlConnection connection = new SqlConnection(ConnectionString))
            {
                await connection.OpenAsync().ConfigureAwait(false);
                System.Diagnostics.Debug.WriteLine("Connection Opened ... " + qry.TableName);
                using (SqlCommand command = new SqlCommand(qry.SQL, connection))
                {
                    using (SqlDataReader reader = command.ExecuteReader())
                    {
                        System.Diagnostics.Debug.WriteLine("Query Executed ... " + qry.TableName);

                        dt.Load(reader);

                        System.Diagnostics.Debug.WriteLine(string.Format("Record Count '{0}' ... {1}", dt.Rows.Count, qry.TableName));

                        return dt;
                    }
                }
            }
        }
        catch(Exception ex)
        {

            System.Diagnostics.Debug.WriteLine("Exception Raised ... " + qry.TableName);
            System.Diagnostics.Debug.WriteLine(ex.Message);

            return new System.Data.DataTable(qry.TableName);
        }

    }
}
0
Juls