【问题标题】:is this shared DbCommand object thread safe?这个共享的 DbCommand 对象线程安全吗?
【发布时间】:2011-08-02 17:33:51
【问题描述】:

我不明白为什么每次需要调用存储过程时都必须创建一个 DbCommand 对象。所以我试图想出一种方法来做到这一点。我已经测试了我的代码(见下文)。但我想与社区核实一下,以防我遗漏了什么。我将在 ASP.NET 应用程序中使用它。这段代码线程安全吗?

SharedDbCommand - 封装了 DbCommand 对象的创建和存储

Db - 数据库的包装器,通过静态字段和 ThreadStatic 属性使用 SharedDbCommand

程序 - 启动线程并使用 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 && 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);
    }

    }
}

【问题讨论】:

  • 你打算为所有线程/用户/会话使用一个连接吗?
  • 不,我希望每个线程上的命令对象都有自己的连接对象。我希望能够创建尽可能多的 Db 对象,但只创建一次命令对象。我不介意为每个线程创建一个,因为我不想锁定对命令对象的访问

标签: c# asp.net multithreading thread-safety data-access-layer


【解决方案1】:

出于多种原因,这是个坏主意。其他人已经提到了其中的一些,但我会给你一个特定于你的实现的:在 ASP.NET 中使用 ThreadStatic 最终会咬你(参见here)。您不控制管道,因此可能多个线程最终为一个请求提供服务(想想页面上的事件处理程序之间,等等 - 有多少不是你的代码正在运行?) .由于未处理的异常,在您不拥有的请求线程上孤立数据也很容易。可能不是一个引人注目的问题,但最好的情况是您正在查看内存泄漏和增加的服务器资源使用量,而您的连接就在那里......

我建议您让 ConnectionPool 完成它的工作 - 它有一些缺陷,但与 ASP.NET 管道中的其他情况相比,性能并不是其中之一。如果你真的想这样做,至少考虑将连接对象存储在 HttpContext.Current.Items 中。您可能可以在有限的情况下完成这项工作,但总会有陷阱,尤其是当您开始编写并行代码时。

从一个去过那里的人那里只需 0.02 美元。 :)

【讨论】:

  • 为了确保我理解您的意思,例如,在我在命令对象上设置了所有参数值之后,ASP.NET 可以回收该线程,并选择其他一些执行页面启动该线程并在同一点开始使用它。然后加载一个堆栈并开始在该线程上执行。但是,因为我有命令对象 ThreadStatic,所以新执行的代码将使用错误的命令对象?
  • 理论上可行,是的。可能在当前的 ASP.NET 上使用非异国情调的页面实现?可能不是......不过,谷歌周围 - 在您不拥有线程的环境中使用 ThreadStatic 通常被认为是一个坏主意。另外,你只是让你的生活变得困难——我敢打赌,数据库连接开销不会接近你应用程序中的最大瓶颈。数以百万计的开发人员在 ASP.NET 上编写代码,如果需要这样的东西,您不认为它已经存在了吗?
  • 如果我傻到跟你打赌,你会赢得那盒饼干。由于您所说的原因,我希望这个想法会被践踏(不是每个人都这样做吗?)。然而,在我的辩护中,没有人通过随从做出任何突破性的事情。
  • 诅咒!又被坑了!这次你赢了 ASP.NET,但我们会再见面的。
【解决方案2】:

这不是保持创建 DbCommand 的好习惯。此外,由于线程处理逻辑,这会使您的应用程序非常复杂。

正如 Microsoft 建议的那样,您应该在执行查询后立即创建和处置连接和命令对象。它们的创建非常轻巧。无需保留与它们一起使用的内存 - 当您的查询执行完成并且您已获取所有结果时释放它们。

【讨论】:

  • 我的示例代码对于调用代码应该是线程安全的。如果我遗漏了任何特定的线程问题,请您指出来吗?我完全了解清理非托管资源的概念。如果您有一个链接,其中详细说明了为什么我应该出于基本的非托管资源清理以外的原因立即创建和处置,您可以发布吗?据我所知,对连接对象调用 Close() 会释放与调用数据库有关的唯一非托管资源。
  • 请查看有关连接池的 MSDN 文章。建议您在使用完连接后始终关闭连接,以便将连接返回到池中。这可以使用 Connection 对象的 Close 或 Dispose 方法来完成。未明确关闭的连接可能不会添加或返回到池中。例如,超出范围但未显式关闭的连接只有在达到最大池大小且连接仍然有效时才会返回到连接池。
  • 您的评论确实让我检查了我的代码并在连接上找到了错过的 Close(),谢谢。我完全意识到这个示例代码是非常规的。其目的是去除时钟周期以换取更少的内存。我不是在问调用存储过程的正确方法是什么。我已经这样做了十多年了。我在询问代码的线程安全性。两个进程是否有可能在这里某个地方互相踩踏,如果是这样,为什么?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-08-19
  • 1970-01-01
相关资源
最近更新 更多