Подтвердить что ты не робот

Как сделать два SQL-запроса действительно асинхронными

Моя проблема основана на реальной проблеме проекта, но я никогда не использовал библиотеку System.Threading.Tasks или не выполнял никаких серьезных программ, связанных с потоками, поэтому мой вопрос может быть сочетанием недостатка знаний о конкретной библиотеке и более общего непонимания того, что асинхронный действительно означает с точки зрения программирования.

Итак, это мой реальный случай - мне нужно получить данные о пользователе. В моем текущем сценарии это финансовые данные, поэтому позвольте сказать, что мне нужен все Accounts, all Deposits и все Consignations для определенного пользователя. В моем случае это означает запрос миллиона записей для каждого свойства, и каждый запрос относительно медленный сам, однако для извлечения Accounts в несколько раз медленнее, чем выборка Deposits. Поэтому я определил три класса для трех банковских продуктов, которые я собираюсь использовать, и когда я хочу получить данные для всех банковских продуктов определенного пользователя, я делаю что-то вроде этого:

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

Итак, проблема начинается здесь. Мне нужно получить все эти три списка одновременно, потому что я собираюсь передать их в представление, где я показываю все данные пользователей. Но поскольку это прямо сейчас, выполнение является синхронным (если я правильно использую этот термин здесь), общее время сбора данных для всех трех продуктов:

Total_Time = Time_To_Get_Accounts + Time_To_Get_Deposits + Time_To_Get_Consignations

Это не хорошо, потому что каждый запрос относительно медленный, поэтому общее время довольно большое, но также, запрос Accounts занимает гораздо больше времени, чем два других запроса, поэтому идея, которая попала мне в голову сегодня, "Что делать, если я могу выполнять эти запросы одновременно". Может быть, здесь приходит мое самое большое недоразумение в этой теме, но для меня самое близкое к этой идее - сделать их асинхронными, поэтому, возможно, тогда Total_Time не будет временем самого медленного запроса, но будет намного быстрее, чем сумма всех трех запросы.

Поскольку мой код сложный, я создал простой пример использования, который, как я думаю, отражает то, что я пытаюсь сделать очень хорошо. У меня есть два метода:

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;
}

и второй метод:

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;
}

который я называю следующим образом:

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

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

Как вы можете видеть, я сначала вызываю GetAccounts(), и я замедляю исполнение вниз, поэтому я даю возможность исполнению продолжить следующий метод. Однако я не получаю никакого результата в течение определенного периода времени, а затем я все распечатываю на консоли в то же время.

Итак, проблема - как сделать так, чтобы я не дождался завершения первого метода, чтобы перейти к следующему методу. В общем, структура кода не так важна, и я действительно хочу понять, есть ли способ заставить оба запроса выполнить в одно и то же время. Образец здесь является результатом моих исследований, которые, возможно, могут быть расширены до такой степени, что я получу желаемый результат.

P.S Я использую ExecuteScalarAsync(); только потому, что начал с метода, который его использовал. На самом деле я буду использовать Scalar и Reader.

4b9b3361

Ответ 1

Когда вы используете свойство Result для задачи, которая еще не завершена, вызывающий поток блокируется до завершения операции. Это означает, что в вашем случае операция GetAccounts должна завершиться до начала вызова GetDeposits.

Если вы хотите убедиться, что этот метод является параллельным (в том числе и для синхронных частей, требующих интенсивного использования ЦП), вам необходимо разгрузить эту работу в другой поток. Самый простой способ сделать это - использовать 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();
}

Поскольку Main не может быть async и поэтому не может использовать await, вы можете просто вызвать этот метод и синхронно дождаться его завершения с помощью Wait.

Ответ 2

Здесь можно выполнить две задачи асинхронно и параллельно:

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

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

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

Ответ 3

Обычно я предпочитаю использовать Task.WaitAll. Чтобы настроить этот сегмент кода, я изменил сигнатуры GetAccounts/GetDeposits только для возврата int (public static int GetAccounts())

Я поместил Console.WriteLine в тот же поток, что и назначение возврата для проверки того, что один GetDeposits возвращается до GetAccounts, но это не нужно и, вероятно, лучше всего переместить его после 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());



    }

Ответ 4

Если ASP.NET использует AJAX для извлечения после отображения страницы и размещения данных в хранилище. Каждая выборка AJAX является асинхронной. Если вы хотите создать одновременные SQL-запросы на сервере?

Использование:

 // 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);

Источник:

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);
        }

    }
}

Ответ 5

Асинхронность хороша, если процесс занимает много времени. Другой вариант - иметь одну хранимую процедуру, которая возвращает все три набора записей.

        adp = New SqlDataAdapter(cmd)
        dst = New DataSet
        adp.Fill(dst)

В коде за страницей указывайте их как dst.Tables(0), dst.Tables(1) и dst.Tables(2). Таблицы будут в том же порядке, что и операторы выбора в хранимой процедуре.