#c# #asp.net #multithreading #thread-safety #data-access-layer
#c# #asp.net #многопоточность #потокобезопасность #уровень доступа к данным
Вопрос:
Я не понимаю, почему я должен создавать объект DbCommand каждый раз, когда мне нужно вызвать хранимую процедуру. Итак, я пытаюсь придумать способ сделать это. Я протестировал свой код (см. Ниже). Но я хотел бы уточнить у сообщества, есть ли что-то, что я пропустил. Я бы использовал его с в ASP.NET приложение. Является ли этот код потокобезопасным?
SharedDbCommand — завершает создание и хранение объекта DbCommand
Db — оболочка для базы данных, использует SharedDbCommand через статическое поле и атрибут ThreadStatic
Program — консольное приложение, которое запускает потоки и использует объект Db, который
// SharedDbCommand.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Configuration;
using System.Data.Common;
using System.Data.SqlClient;
using System.Data;
namespace TestCmdPrepare {
public class SharedDbCommand {
[ThreadStatic]
static DbCommand cmd;
public SharedDbCommand(string procedureName, ConnectionStringSettings dbConfig) {
var factory = DbProviderFactories.GetFactory(dbConfig.ProviderName);
cmd = factory.CreateCommand();
cmd.Connection = factory.CreateConnection();
cmd.Connection.ConnectionString = dbConfig.ConnectionString;
cmd.CommandText = procedureName;
cmd.CommandType = System.Data.CommandType.StoredProcedure;
if (cmd is SqlCommand) {
try {
cmd.Connection.Open();
SqlCommandBuilder.DeriveParameters(cmd as SqlCommand);
} finally {
if (cmd != null amp;amp; cmd.Connection != null)
cmd.Connection.Close();
}
}
}
public DbParameter this[string name] {
get {
return cmd.Parameters[name];
}
}
public IDataReader ExecuteReader() {
try {
cmd.Connection.Open();
return cmd.ExecuteReader(CommandBehavior.CloseConnection);
} finally {
cmd.Connection.Close();
}
}
public void ExecuteNonQuery() {
try {
cmd.Connection.Open();
cmd.ExecuteNonQuery();
} finally {
cmd.Connection.Close();
}
}
}
}
// Db.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Configuration;
using System.Data.Common;
using System.Data;
using System.Data.SqlClient;
using System.Threading;
using System.Diagnostics;
namespace TestCmdPrepare {
public class Db {
ConnectionStringSettings dbSettings;
DbProviderFactory factory;
public Db() {
dbSettings = ConfigurationManager.ConnectionStrings["db"];
factory = DbProviderFactories.GetFactory(dbSettings.ProviderName);
}
IDataReader ExecuteReader(DbCommand cmd) {
cmd.Connection.Open();
return cmd.ExecuteReader(CommandBehavior.CloseConnection);
}
private DbConnection CreateConnection() {
var c = factory.CreateConnection();
c.ConnectionString = dbSettings.ConnectionString;
return c;
}
DbCommand CreateCommand(string procedureName) {
var cmd = factory.CreateCommand();
cmd.Connection = CreateConnection();
cmd.CommandText = "get_stuff";
cmd.CommandType = CommandType.StoredProcedure;
if (cmd is SqlCommand) {
try {
cmd.Connection.Open();
SqlCommandBuilder.DeriveParameters(cmd as SqlCommand);
} finally {
cmd.Connection.Close();
}
}
return cmd;
}
[ThreadStatic]
static DbCommand get_stuff;
DbCommand GetStuffCmd {
get {
if (get_stuff == null)
get_stuff = CreateCommand("get_stuff");
return get_stuff;
}
}
public string GetStuff(int id) {
GetStuffCmd.Parameters["@id"].Value = id;
using (var reader = ExecuteReader(GetStuffCmd)) {
if (reader.Read()) {
return reader.GetString(reader.GetOrdinal("bar"));
}
}
return null;
}
[ThreadStatic]
static SharedDbCommand get_stuff2;
public string GetStuff2(int id) {
if (get_stuff2 == null)
get_stuff2 = new SharedDbCommand("get_stuff", dbSettings);
get_stuff2["@id"].Value = id;
using (var reader = get_stuff2.ExecuteReader()) {
if (reader.Read()) {
Thread.Sleep(1000);
return reader.GetString(reader.GetOrdinal("bar"));
}
}
return null;
}
}
}
// Program.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data.Common;
using System.Configuration;
using System.Data.SqlClient;
using System.Threading;
namespace TestCmdPrepare {
class Program {
static void Main(string[] args) {
var db = new Db();
var threads = new List<Thread>();
for (int i = 0; i < 4; i ) {
var one = new Thread(Run2);
var two = new Thread(Run1);
threads.Add(one);
threads.Add(two);
one.Start();
two.Start();
Write(db, 1);
Write(db, 2);
}
Console.WriteLine("Joining");
foreach (var thread in threads) {
thread.Join();
}
Console.WriteLine();
Console.WriteLine("DONE");
Console.ReadLine();
return;
}
static void Write(Db db, int id) {
Console.WriteLine("2:{0}:{1}", Thread.CurrentThread.ManagedThreadId, db.GetStuff2(id));
Console.WriteLine("1:{0}:{1}", Thread.CurrentThread.ManagedThreadId, db.GetStuff(id));
}
static void Run1() {
var db = new Db();
Write(db, 1);
}
static void Run2() {
var db = new Db();
Write(db, 2);
}
}
}
Комментарии:
1. Собираетесь ли вы использовать одно соединение для всех потоков / пользователей / сеансов?
2. нет, я ожидаю, что объект command в каждом потоке будет иметь свой собственный объект connection. Я хочу иметь возможность создавать столько объектов Db, сколько захочу, но объект command создается только один раз. Я не возражаю, что один создается для каждого потока, потому что я не хочу блокировать доступ к объекту command
Ответ №1:
Плохая идея по многим причинам. Другие упоминали некоторые из них, но я приведу вам один, специфичный для вашей реализации: использование ThreadStatic в ASP.NET в конечном итоге это вас укусит (смотрите здесь). Вы не управляете конвейером, поэтому возможно, что несколько потоков в конечном итоге будут обслуживать один запрос (подумайте, между обработчиками событий на странице и т.д. — Сколько выполняется кода, который не является вашим?). Также легко удалить данные из потока запроса, которым вы не владеете, из-за необработанных исключений. Возможно, это не проблема с остановкой показа, но в лучшем случае вы столкнулись с утечкой памяти и повышенным использованием ресурсов сервера, в то время как ваше соединение просто находится там…
Я бы рекомендовал вам просто позволить ConnectionPool выполнять свою работу — у него есть некоторые недостатки, но производительность не является одним из них по сравнению с тем, что еще происходит в ASP.NET конвейер. Если вы действительно хотите это сделать, по крайней мере, рассмотрите возможность сохранения объекта connection в HttpContext.Current .Товары. Вероятно, вы можете заставить это работать в ограниченных обстоятельствах, но всегда будут ошибки, особенно если вы когда-нибудь начнете писать параллельный код.
Всего за 0.02 доллара от парня, который был там. 🙂
Комментарии:
1. Чтобы убедиться, что я вас понял, вы говорите, что, например, после того, как я установлю все значения параметров в моем командном объекте, ASP.NET может ли этот поток быть переработан, и какая-либо другая исполняющая страница подхватит этот поток и начнет использовать его в тот же момент. Затем загрузите стек и начните выполнение в этом потоке. НО, поскольку у меня есть командный объект ThreadStatic, во вновь выполняемом коде будет использоваться неправильный объект command?
2. Теоретически возможно, да. Вероятно, в текущем ASP.NET с использованием неэкзотических вложенных страниц? Вероятно, нет… Погуглите, однако, — использование ThreadStatic в средах, где вы не являетесь владельцем потоков, обычно рассматривается как плохая идея. Кроме того, вы просто усложняете себе жизнь — я готов поспорить на коробку печенья, что накладные расходы на подключение к БД даже близко не соответствуют самому большому узкому месту в вашем приложении. Миллионы разработчиков пишут на ASP.NET если бы что-то подобное было необходимо, не думаете ли вы, что это уже было бы доступно?
3. Ты бы выиграл эту коробку печенья, если бы я был настолько глуп, чтобы поспорить с тобой. Я ожидал, что эта идея будет растоптана по той причине, о которой вы сказали (разве не все будут это делать?). Однако, в мою защиту, никто никогда не делал ничего новаторского, следуя стаду.
4. Проклятия! Снова сбой! На этот раз вы выиграли ASP.NET , но мы встретимся снова.
Ответ №2:
Это не очень хорошая практика для сохранения созданной DbCommand. Кроме того, это делает ваше приложение очень сложным из-за логики обработки потоков.
Как предлагает Microsoft, вы должны создавать и утилизировать объекты connection и command сразу после выполнения вашего запроса. Они очень легкие в создании. Нет необходимости сохранять память, используемую с ними — утилизируйте их, когда выполнение вашего запроса завершено и вы получили все результаты.
Комментарии:
1. Предполагается, что мой пример кода должен быть потокобезопасным для вызывающего кода. Если есть какие-либо конкретные проблемы с потоками, которые я пропустил, не могли бы вы, пожалуйста, указать на них? Я полностью осведомлен о концепции очистки неуправляемых ресурсов. Если у вас есть ссылка, содержащая подробную информацию о том, почему я должен создавать и утилизировать сразу по причинам, отличным от базовой очистки неуправляемых ресурсов, не могли бы вы, пожалуйста, опубликовать это? Насколько мне известно, вызов Close() для объекта connection освобождает единственный неуправляемый ресурс, связанный с вызовом базы данных.
2. Пожалуйста, взгляните на статью MSDN, касающуюся пула соединений. Рекомендуется всегда закрывать соединение после завершения его использования, чтобы соединение было возвращено в пул. Это можно сделать, используя методы Close или Dispose объекта Connection. Соединения, которые явно не закрыты, могут не добавляться или не возвращаться в пул. Например, соединение, которое вышло за пределы области видимости, но которое не было явно закрыто, будет возвращено в пул соединений только в том случае, если достигнут максимальный размер пула и соединение все еще действует.
3. Ваш комментарий заставил меня пересмотреть мой код и найти пропущенную Close() при подключении, спасибо. Я полностью осознаю, что этот пример кода является нетрадиционным. Его цель — удалить тактовые циклы в обмен на меньший объем памяти. Я не спрашиваю, как правильно вызвать хранимую процедуру. Я занимаюсь этим уже более десяти лет. Я спрашиваю о потокобезопасности кода. Возможно ли, чтобы два процесса где-то здесь давили друг на друга, и если да, то почему?