在我的文章网站中,我有一些需要数据库更新的统计操作,我想用线程异步执行此操作。
问题在于我试图扼杀这里建议的内容:Simplest way to do a fire and forget method in C#?
因为FireAway()包含数据库更新,我得到"数据库连接已打开"错误。
我的问题是:编写一个在后台作为原子操作而不会发生冲突的线程的最简单方法是什么。
我的网站:www.mentallica.co.il
添加信息..... FireAway()包含对存储所有数据库更新的dll的调用。
我正在使用名为bll的共享类 MyBuisnessLogic bll = new MyBuisnessLogic()
在FireAway()内部
bll.RunStatistics();
bll.RunStatistics()函数打开并关闭sql连接。
我猜这个问题在一个线程打开sql连接而另一个线程试图打开已经打开的连接时会出现问题。
也许我应该为新线程为MyBuisnessLogic创建一个单独的实例? 也许我需要在使用()里面这样做? 有点像 使用(MyBuisnessLogic bll = new MyBuisnessLogic) ?
----检查后我发现MyBuisnessLogic需要Idisposible才能工作......我应该这样做吗?
答案 0 :(得分:1)
您希望进行非阻塞Db处理的常见原因包括:
正如您的帖子建议的那样,管理资源SqlConnections
需要考虑,因为在线程isn't a good idea之间共享SqlConnection
或SqlCommand
。不希望同步对SqlConnection
的访问,因为它将撤消任何并行化的好处。
问题1的简单解决方案是强制每个线程建立自己的SqlConnection
,尽管这不利于高数据库吞吐量:
Task.Factory.StartNew(() =>
{
using (var conn = new SqlConnection(connectionString))
using (var cmd = conn.CreateCommand())
{
conn.Open();
SetupCmd(cmd);
SaveStat(cmd, statToSave);
}
});
存在背景写作的替代方案(案例1),例如通过拥有一个或多个长期写入线程,监听队列,例如ConcurrentQueue
或更好的是,由ASP.Net页面线程提供的BlockingCollection
iterated by a GetConsumingEnumerable
。作者线程将始终保持一个SqlConnection
开放。
在像2.这样的大量情况下,重用SqlConnection
和SqlCommands
对性能至关重要。然后需要在多个线程(或任务,如果使用TPL)之间划分数据。 Parallel.ForEach为我们完成了大部分艰苦工作 - 下面,localInit
的重载用于建立SqlConnection
和SqlCommand
,然后传递给每个正文,以及在任务结束时调用localFinally
(在Body的0..N次迭代之后 - 使用默认分区器,因此TPL决定需要多少个任务,以及传递给每个任务的项目数量身体)。 localInit
允许类似的范例使用线程本地存储。
一个警告 - 如果处理仅适用于大批量插入操作,SqlBulkCopy可能是一个更好的方法。
以下是使用TPL的几个选项: 鉴于表格:
create table StatsData
(
x int ,
y decimal(20,5),
name nvarchar(50)
)
一个模特:
public class StatsData
{
public int X { get; private set; }
public double Y { get; private set; }
public string Name { get; private set; }
public StatsData(int x, double y, string name)
{
X = x;
Y = y;
Name = name;
}
}
以下类提供了2个异步选项(对于第1点和第2点):
public class Dispatcher
{
// Helpers - refactoring
private static void SetupCmd(SqlCommand cmd)
{
cmd.CommandText = "insert into dbo.statsdata(x, y, Name) values (@x, @y, @Name);";
cmd.CommandType = CommandType.Text;
cmd.Parameters.Add("@x", SqlDbType.Int);
cmd.Parameters.Add("@y", SqlDbType.Decimal);
cmd.Parameters.Add("@Name", SqlDbType.NVarChar, 30);
}
private static void SaveStat(SqlCommand cmd, StatsData statToSave)
{
cmd.Parameters["@x"].Value = statToSave.X;
cmd.Parameters["@y"].Value = statToSave.Y;
cmd.Parameters["@Name"].Value = statToSave.Name;
cmd.ExecuteNonQuery();
}
// 1. Save 1 stat at a time on a background task. Use for low / intermittent volumes
public void SaveStatAsynch(string connectionString, StatsData statToSave)
{
Task.Factory.StartNew(() =>
{
using (var conn = new SqlConnection(connectionString))
using (var cmd = conn.CreateCommand())
{
conn.Open();
SetupCmd(cmd);
SaveStat(cmd, statToSave);
}
});
}
// 2. For background writing of large volumes of stats. Uses the default partitioner in parallel foreach
public void SaveStatsParallel(string connectionString, IEnumerable<StatsData> statsToSave)
{
Parallel.ForEach(
statsToSave,
// localInit. Return value is passed to each body invocation
() =>
{
var conn = new SqlConnection(connectionString);
var cmd = conn.CreateCommand();
SetupCmd(cmd);
conn.Open();
return new
{
Conn = conn,
Cmd = cmd
};
},
// Body, 0..N per Task decided by TPL
(stat, loopState, initData) =>
{
SaveStat(initData.Cmd, stat);
return initData;
},
// Disposables
(initData) =>
{
initData.Cmd.Dispose();
initData.Conn.Dispose();
}
);
}
使用示例:
const string connString = @"Server=.\SqlExpress;DataBase=StatsDb;Integrated Security=true";
// Create some dummy data
var statsToSave =
Enumerable
.Range(0, 10000)
.Select(i => new StatsData(i, i*Math.PI, string.Format("Stat #{0}", i)));
// Insert this in parallel on background tasks / threads as determined by the TPL
new Dispatcher().SaveStatsParallel(connString, statsToSave);