我有两个计时器。其中一个计时器从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
Stack Trace;
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)
答案 0 :(得分: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
,并且您不希望阻止该代码等待第二个事件处理程序,您可以将其放在{{{ 1}}阻止。
<强>更新强>
根据您的意见,以下是我对如何安排整个计划的想法。
主要目标是最小化两个线程可能相互等待的时间。
1)确保使用多线程计时器:if (mut.WaitOne(iMaxWaitMSec))
或System.Timers.Timer
,而不是System.Threading.Timer
。
https://msdn.microsoft.com/en-us/library/system.timers.timer(v=vs.110).aspx
我希望timer事件处理程序在一个单独的线程上运行。
如果Elapsed事件的处理持续时间超过Interval,那么 可能会在另一个ThreadPool线程上再次引发该事件。
因此,有一个标志,指示正在处理事件并检查它。我不认为你想要再次调用你的存储过程,而之前尝试调用它还没有完成。
2)在内存中有一个可以保存数据队列的结构。
第一个定时器将定期从PLC读取数据并将数据附加到队列的末尾。第二个计时器会定期检查队列并从队列的开头挑选待处理的数据。
有一个班级Queue
。理想情况下,它应该能够快速将元素附加到其末尾并从头开始快速删除元素。在.NET 4中有ConcurrentQueue
,这意味着您不需要显式的互斥锁。
如果将数据插入数据库突然变慢(即网络中断),队列将增长并包含多个元素。在这种情况下,您可以自行决定做什么 - 丢弃额外的元素,或者仍尝试插入所有元素。
3)Mutex应该用于防止同时访问这个&#34;队列&#34;最小化等待的对象。
System.Windows.Forms.Timer
...
// 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);
}