【问题标题】:ArgumentOutOfRangeException in SqlCommandSqlCommand 中的 ArgumentOutOfRangeException
【发布时间】:2015-03-18 19:01:41
【问题描述】:

我有两个计时器。其中一个计时器从 plc 检索数据并更新数据表中的相关数据行。在另一个计时器中,我将该数据表作为参数发送到存储过程。问题是,有时我的 sqlCommand.ExecuteNonQuery() 会给我一个 ArgumentOutOfRangeException。我的数据表中有 128 行。我从 plc 读取了 512 个字节。一行代表一个浮点值(即4字节)

我无法理解 ArgumentOutOfRange 异常。变量计数适合行数。问题是什么。为什么我有时会出现这个错误?

这是我的代码

        void timer1_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
        {
            timer1.Stop();
            byte[] data = new byte[512];
            int res = dc.readManyBytes(libnodave.daveDB, 19, 0, 512, data);
            if (res == 0)
            {
                for (int i = 0; i < 128; i++)
                {
                    byte[] temp = new byte[] { data[(i * 4 + 3)], data[(i * 4 + 2)], data[(i * 4 + 1)], data[(i * 4)] };
                    double value = Math.Truncate(Convert.ToDouble(BitConverter.ToSingle(temp, 0)) * 100) / 100;
                    DataRow row = dtAddress.Rows[i];
                    switch (row["DataType"].ToString())
                    {
                        case "REAL":
                            DataRow[] rValues = dtValue.Select("AddressID = " + row["ID"]);
                                foreach (DataRow rValue in rValues)
                                {
                                    rValue["Value"] = value;
                                    rValue["LastUpdate"] = DateTime.Now;
                                }
                            break;
                    }
                }
            }
       }

    void timer2_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
    {
        using (SqlCommand crudValues = new SqlCommand("dbo.crudValues", connection))
        {
            crudValues.CommandType = CommandType.StoredProcedure;
            SqlParameter param = crudValues.Parameters.AddWithValue("@tblValue", dtValue);
            param.SqlDbType = SqlDbType.Structured;

            crudValues.ExecuteNonQuery();
        }
    }

--SQL 存储过程

SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO

ALTER PROCEDURE [dbo].[crudValues]
    @tblValue as dbo.tblValue READONLY
AS
BEGIN
    SET NOCOUNT ON;
    UPDATE tblValue SET tblValue.Value = t.Value, tblValue.LastUpdate = t.LastUpdate FROM tblValue INNER JOIN @tblValue t ON tblValue.ID = t.ID
END

堆栈跟踪;

   at System.Data.SqlClient.TdsParser.TdsExecuteRPC(_SqlRPC[] rpcArray, Int32 timeout, Boolean inSchema, SqlNotificationRequest notificationRequest, TdsParserStateObject stateObj, Boolean isCommandProc, Boolean sync, TaskCompletionSource`1 completion, Int32 startRpc, Int32 startParam)
   at System.Data.SqlClient.SqlCommand.RunExecuteReaderTds(CommandBehavior cmdBehavior, RunBehavior runBehavior, Boolean returnStream, Boolean async, Int32 timeout, Task& task, Boolean asyncWrite, SqlDataReader ds)
   at System.Data.SqlClient.SqlCommand.RunExecuteReader(CommandBehavior cmdBehavior, RunBehavior runBehavior, Boolean returnStream, String method, TaskCompletionSource`1 completion, Int32 timeout, Task& task, Boolean asyncWrite)
   at System.Data.SqlClient.SqlCommand.InternalExecuteNonQuery(TaskCompletionSource`1 completion, String methodName, Boolean sendToPipe, Int32 timeout, Boolean asyncWrite)
   at System.Data.SqlClient.SqlCommand.ExecuteNonQuery()
   at GazMotoruPLCScanner.Program.timer2_Elapsed(Object sender, ElapsedEventArgs e) in d:\Projeler\TRES ENERJİ\GazMotoruPLCScanner\Program.cs:line 106
   at System.Timers.Timer.MyTimerCallback(Object state)

【问题讨论】:

  • 你能发布异常的消息吗?
  • @mainvoid 异常消息是index was out of range. must be nonnegative and less than the size of the collection 我也把存储过程放到了我的问题中
  • 原因可能是我填充数据表的方式吗?我通过 SqlDataAdapter 填充它。我将该数据适配器包装到 sqlcommandbuilder 中。
  • 你确定异常是由ExecuteNonQuery方法抛出的吗?
  • 两个定时器是否有可能同时触发?可能是您正在更改 DataTable 的内容,而它正被另一个计时器处理程序发送到数据库?我在您的代码中没有看到任何对这种并发的处理。

标签: c# sql timer sqlcommand


【解决方案1】:

如果问题确实是由两个线程同时处理同一个DataTable对象引起的,那么一种可能的解决方案是使用Mutex同步两个线程。

当两个或多个线程需要同时访问一个共享资源时 时间,系统需要一个同步机制来保证只有 一次一个线程使用该资源。互斥锁是一个同步 仅授予对共享资源的独占访问权限的原语 一根线。如果一个线程获得了一个互斥体,第二个线程想要 获取该互斥体被挂起,直到第一个线程释放 互斥体。

在您的情况下,第一个事件处理程序将元素添加到DataTable,第二个事件处理程序将此DataTable 发送到存储过程。如果在 RunExecuteReader 尝试从中读取行时更改此对象,则任何事情都可能发生。

创建一个可以从timer1_Elapsed()timer2_Elapsed() 访问的 Mutex 类实例。

private static Mutex mut = new Mutex();

您的计时器事件处理程序可能如下所示:

void timer1_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
{
    int iMaxWaitMSec = 10000;
    if (mut.WaitOne(iMaxWaitMSec))
    {
        try
        {
            // Populate DataTable
        }
        catch
        {
        }
        finally
        {
            mut.ReleaseMutex();
        }
    }
    else
    {
        // we waited longer than iMaxWaitMSec milliseconds
        // in an attempt to lock the mutex
        // skip this timer event
        // we'll retry next time
    }
}

.

void timer2_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
{
    int iMaxWaitMSec = 10000;
    if (mut.WaitOne(iMaxWaitMSec))
    {
        try
        {
            // Send DataTable to the database
        }
        catch
        {
        }
        finally
        {
            mut.ReleaseMutex();
        }
    }
    else
    {
        // we waited longer than iMaxWaitMSec milliseconds
        // in an attempt to lock the mutex
        // skip this timer event
        // we'll retry next time
    }
}

检查语法错误。将超时设置为某个适当的值。在获取互斥锁的时间过长时添加适当的情况处理。

这种方法的结果是timer1_Elapsed()timer2_Elapsed() 中的两个代码块在if (mut.WaitOne(iMaxWaitMSec)) 中永远不会同时运行。

如果您有一些额外的代码不涉及共享的DataTable,并且您不希望该代码在等待第二个事件处理程序时被阻塞,您可以将它放在if (mut.WaitOne(iMaxWaitMSec)) 块之外。

更新

根据您的 cmets,这是我对如何安排整个节目的想法。

主要目标是尽量减少两个线程可能相互等待的时间。

1) 确保使用多线程计时器:System.Timers.TimerSystem.Threading.Timer,而不是 System.Windows.Forms.Timerhttps://msdn.microsoft.com/en-us/library/system.timers.timer(v=vs.110).aspx

我希望计时器事件处理程序在单独的线程上运行。

如果 Elapsed 事件的处理持续时间超过 Interval,则 事件可能会在另一个 ThreadPool 线程上再次引发。

所以,有一个标志表明事件正在被处理并检查它。我认为您不会希望在上一次调用它的尝试尚未完成时再次调用您的存储过程。

2) 在内存中有一个结构,可以保存一个包含数据的队列。 第一个计时器将定期从 PLC 读取数据并将数据附加到队列的末尾。第二个计时器将定期检查队列并从队列的开头挑选待处理的数据。 有一个班级Queue。理想情况下,它应该能够快速地将元素附加到其末尾并从头快速删除元素。在 .NET 4 中有ConcurrentQueue,这意味着您不需要显式互斥锁。

如果将数据插入数据库突然变慢(即网络中断),队列将增长并包含多个元素。在这种情况下,您可以决定要做什么 - 丢弃多余的元素,或者仍然尝试插入所有元素。

3) 互斥锁仅用于防止同时访问此“队列”对象以最大程度地减少等待。

// somewhere in the main program
Queue<DataTable> MainQueue = new Queue<DataTable>();
// or in .NET 4
ConcurrentQueue<DataTable> MainConcurrentQueue = new ConcurrentQueue<DataTable>();

...

void timer1_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
{
    // read data from PLC
    // parse, process the data
    // create a **new** instance of the DataTable object
    DataTable dt = new DataTable();
    // and fill it with your data

    // append the new DataTable object to the queue
    mut.WaitOne();
    try
    {
        MainQueue.Enqueue(dt);
    }
    catch { }
    finally
    {
        mut.ReleaseMutex();
    }

    // or in .NET4 simply
    MainConcurrentQueue.Enqueue(dt);
}

...

void timer2_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
{
    DataTable dt = null;

    mut.WaitOne();
    try
    {
        dt = MainQueue.Dequeue();
    }
    catch { }
    finally
    {
        mut.ReleaseMutex();
    }

    // or in .NET4 simply
    dt = MainConcurrentQueue.Dequeue();

    // Send DataTable to the database

    // TODO: add checks for empty queue
    // TODO: add checks for long queue 
    // and send all or some of the accumulated elements to the DB
}

【讨论】:

  • @MOD,实际上,如果两个计时器具有相同的间隔......为什么首先需要两个计时器?如果您可以将所有代码放在一个计时器事件中,则无需同步两个事件处理程序。
  • 我不希望我的更新数据表延迟我对 sql 的存储。而且我可以将更新数据表代码放入不确定触发的事件中
  • 如果我正确理解了您的评论,您需要两个计时器和两个事件,因为您希望将数据保存到数据库中不会干扰从 PLC 读取数据。换句话说,无论如何,您都希望每隔 NN 秒从 PLC 读取数据。将这些数据保存到数据库需要一些时间,并且无法预测需要多长时间,因此您将该步骤放在单独的计时器中。正确的?如果是,那么您最好调整程序的布局。使用我所描述的互斥锁可以使代码像两个事件处理程序合并为一个一样工作 - 这不是您想要的。
  • 是的。它是正确的。那我应该如何调整程序的布局呢?
  • 我已经在答案中添加了如何使用队列的一般想法。
猜你喜欢
  • 1970-01-01
  • 2019-02-23
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-11-30
  • 2023-04-08
  • 1970-01-01
相关资源
最近更新 更多