У меня есть рабочий код С#, который использует SqlConnection для создания временных таблиц (например, #Foo), вызывает хранимые procs для заполнения этих временных таблиц и возврата результатов клиенту С#, используйте С# для выполнения сложных вычислений по этим результатам, и использовать результаты расчета для обновления одной из созданных ранее временных таблиц.
Из-за временных таблиц, используемых во всем процессе, мы должны иметь только одно SqlConnection.
Я определил узкое место производительности при обновлении таблицы temp с результатами расчета. Этот код уже загружал обновления для предотвращения нехватки памяти у С# -клиента. Каждая партия вычисленных данных была отправлена в сохраненный процесс через SqlCommand.ExecuteNonQuery, а sproc по очереди обновляет временную таблицу. Этот код тратил большую часть времени на этот вызов ExecuteNonQuery.
Итак, я поменял его на BeginExecuteNonQuery, а также на код, чтобы ждать по потокам и вызывать EndExecuteNonQuery. Это улучшило производительность примерно на треть, но меня беспокоит наличие нескольких одновременных вызовов в SqlCommand.BeginExecuteNonQuery с использованием того же SqlConnection.
Это нормально, или я буду сталкиваться с проблемами с потоками?
Извините за подробные объяснения.
В документах MSDN указано:
Метод BeginExecuteNonQuery возвращается немедленно, но пока код не выполнит соответствующий вызов метода EndExecuteNonQuery, он не должен выполнять никаких других вызовов, которые запускают синхронное или асинхронное выполнение с тем же объектом SqlCommand.
Это означает, что разные объекты SqlCommand могут вызывать BeginExecuteNonQuery до завершения первого SqlCommand.
Вот какой код, который иллюстрирует проблему:
private class SqlCommandData
{
public SqlCommand Command { get; set; }
public IAsyncResult AsyncResult { get; set; }
}
public static void TestMultipleConcurrentBeginExecuteNonQueryCalls(string baseConnectionString)
{
var connectionStringBuilder = new SqlConnectionStringBuilder(baseConnectionString)
{
MultipleActiveResultSets = true,
AsynchronousProcessing = true
};
using (var connection = new SqlConnection(connectionStringBuilder.ConnectionString))
{
connection.Open();
// ELIDED - code that uses connection to do various Sql work
SqlDataReader dataReader = null;
// in real code, this would be initialized from calls to SqlCommand.ExecuteReader, using same connection
var commandDatas = new List<SqlCommandData>();
var count = 0;
const int maxCountPerJob = 10000;
while (dataReader.Read())
{
count++;
// ELIDED - do some calculations on data, too complex to do in SQL stored proc
if (count >= maxCountPerJob)
{
count = 0;
var commandData = new SqlCommandData
{
Command = new SqlCommand {Connection = connection}
};
// ELIDED - other initialization of command - used to send the results of calculation back to DB
commandData.AsyncResult = commandData.Command.BeginExecuteNonQuery();
commandDatas.Add(commandData);
}
}
dataReader.Close();
WaitHandle.WaitAll(commandDatas.Select(c => c.AsyncResult.AsyncWaitHandle).ToArray());
foreach (var commandData in commandDatas)
{
commandData.Command.EndExecuteNonQuery(commandData.AsyncResult);
commandData.Command.Dispose();
}
// ELIDED - more code using same SqlConnection to do final work
connection.Close();
}
}