这几天Winsock服务器成功转型至多线程后测试中遇到的一些问题
本帖最后由 jessezappy 于 2009-11-21 21:29 编辑老马,空间终于搬家了啊。这个速度感觉快多了,只是发现数据库转过来的时候还是有些小BUG或是不兼容问题,不过不影响使用,不错。
‘---------------------------------------
前几天和老马就多线程内winsock服务程序的运用问题探讨和研究了好几天。终于成功在多个线程内启用了winsock控件了,我测试了2000多个客户端的连接,每个线程内启动20~100个winsock控件数组元素,结果发现个不爽的问题:
因为使用了多线程技术后,多个winsock可以实现真正的并发了,问题就是在多个线程内的winsock同时并发时,如果线程过多,会导致winsock.senddata方法消耗系统资源过多或者说是相互卡住,之后的现象就是每个线程内的winsock都能正常工作,但是主界面基本不响应了,这只有在我的线程内有数据需要发送时调用.senddata方法时才会发生这种情况,最终结果是点击主界面后,windows提示程序忙,是否切换到。。。。这个提示出来,仿佛点切换到或者重试按钮都无济于事,除非数据都发送完毕,不再调用 .senddata 方法后,主界面才会活过来。这时我程序的配置是启用2000个客户端连接,每个线程内20个连接,也就是总共开启100个线程,此时必卡住,但是数据还在正常发送。此时我是在线程内的timer过程中循环检测20个连接是否需要发送数据,有要发的就发,全部连接检测完一遍才退出timer过程。
之后我将 .senddata 方法屏蔽后,继续测试程序发现,依旧是卡,只是没有之前的严重了,因为每检测一个连接就要查询一次数据库是否有数据需要发送至该连接,后来我怀疑是使用循环检测20个连接导致在循环过程消耗资源过多,因为根据我跟踪每个线程的循环检测20个连接只检索数据库需要的时间发现耗时居然高达1秒,因为100个线程同时执行循环检索数据库,数据库又是运行在我本机,可想而知。
再后来我将每个线程内的连接数增加到了100,让总的线程数变为20,这下倒是很少出现那个程序忙的windows提示了,只是主界面依旧卡,因为此时线程内timer过程中需要循环检测的连接数变为了100,循环耗时比20的更大了。
再后来没办法,只好放弃使用循环了,使用一个线程内全局变量来在timer过程中计数检测所有的连接,也就是每个timer过程只检索和发送一个连接的信息,检索完了计数值加1,就到下一次timer过程才检索和发送下一个连接的信息,由于需要处理的快速,因此,timer 的触发时间先设置为50毫秒,后来发现50毫秒也有点卡,只好改成100毫秒,这次回复了.senddata方法的调用,按照完整功能来测试。这回倒是没有再出现windows的程序忙的提示框了。主界面在每个连接都发送数据时虽然还有一点点迟钝,但是已经不算卡了。每线程内连接数30.
’---------------------------------------------
目前正在按照这个方案测试程序中,这其中提出一个小问题请教老马,就是winsock在并发工作时如果要不卡,预计同时并发数最大值怎么推算?因为我发现线程多了的话,.senddata 时主界面就会极卡。因此怀疑winsock控件处理并发操作的能力并不太好。
’=============================================
补充一点,线程中数据传递我还是使用的是事件调度的办法,因为我需要实时掌握所有线程内总的客户端连接数量,因此我主线程的timer过程内,每100毫秒检测一个线程内的活动连接数量,所有线程检测完一遍后就加和显示在主界面,这个过程是稍微有点耗资源的,但是在不调用 .senddata 方法时这个过程并没有明显的延迟。
‘----------------------------------------------
另外,公共模块中定义的那些全局变量在线程创建时,仅仅只是将变量的定义空间复制了一份去线程内,而这些变量在线程创建前得到的值,并不能一起复制去线程中。也就是说,不能将数值存储在这些变量中幻想线程创建时会将这些值一起复制去线程内。
因此我得出的方案是,居然这些值在线程创建时不能一起复制去线程的话,那么就不必再在线程外定义这些全局变量了,因为毫无意义,反而还会带来逻辑问题。
我之前还幻想定义几个全局变量,先将ini文件内的值读给它们,然后在线程创建时将这些值一起复制去线程,结果,只复制了一份空的变量定义空间过去,全是初始化时候的值, 0 “” 或 false ,一气之下我全部定义都剪切了,去线程内重新定义不是一样嘛。
看来MSDN中说的复制全局变量的一份副本去线程内的说明简直是多于的,没有值的全局变量要它复制来作甚?浪费内存!
最后我采取的解决办法是在线程内单独定义一次这些变量,线程装载后来个初始化过程,将主线程中已经从ini内读出来的数据传递去线程中即可,简单明了逻辑清晰。
‘’‘’‘’‘’‘’‘’‘’‘’‘’‘’‘’‘’‘’‘’‘’‘’‘’‘’‘’‘’‘’‘’‘’‘’‘’‘’
下附我的一个事件传递数据的多线程例子:
其中使用了从 http://www.cnblogs.com/raylynn/archive/2007/01/29/632978.html 学来的线程数组事件定义方法,主模块是用老马的winsock多线程下载例子改的,其中实现了动态定义多线程数量及增删的方法。
另外简单的说下多线程内实现winsock服务器在多个线程内实现 .accept 的思路:
每个线程内都保留一个sock 等待做 Listen ,然后通过事件调度,由主线程再安排空闲线程进行 Listen,同一时刻,只能有一个线程在 Listen,每个线程内的各个连接只接受本线程内 Listen 来的 requestID 用以 accept, requestID 若 离开了Sock_ConnectionRequest 事件过程之后即变成了一个毫无意义的数值啦!
回复 1# jessezappy
我们遇到的问题类似,我也头痛呢,加我吧 QQ592849561 我的问题是这样的,我要做一个服务端来接收下位机发过来的数据(下位机用的时候GPRS模块),我用了一个winsock数组,winsock1(0)用来侦听,每次接收到一个connectionrequest就load一个新的winsock来连接,然后根据index来识别每个winsock的数据,我想这样来实现并行的数据接收和处理,可是遇到2个客户端同时连上发送数据的时候就会出现问题(有时有会接收不到其中一个客户端的数据)
Private Sub Winsock1_DataArrival(index As Integer, ByVal bytesTotal As Long)
Dim Data() As Byte
Dim i As Integer
Winsock1(index).GetData Data, vbByte
Select Case bytesTotal '根据长度来判断处理的过程
Case 13
Call Recv1(index, Data)
Case 50
Call Query(index, Data)
Case 174
Call Read1(index, Data)
End Select
End Sub
还有我在这些处理的过程中还要根据收到的从数据库中读出每个下位机设置信息和写入新的读数,然后senddata, 其实基础的手法就是使用单元线程,但这种方式的工程结构应该如何设计与规划,又是一个新问题了.
对于VB6来说,网络上基本很少多线程方面的实用资料可参考,只能自己折腾....
回楼上:
关于"有时有会接收不到其中一个客户端的数据",我也遇到过,我的那份代码的问题在于对数据的处理过程里.
由于处理过程是几个循环嵌套,并且其中未使用doevents释放控制权,导致WINSOCK事件的丢失....
而你这代码里,CALL的这三个过程会不会也是如此呢?
打个比方.
如果你在它们里面查询了数据库,由于读写硬盘的速度很慢,当前线程在数据库查询未返回时是挂起状态的,此时要是有数据来了,则极有可能丢失. 回复jessezappy
我们遇到的问题类似,我也头痛呢,加我吧 QQ592849561
zzd1000 发表于 2009-11-23 13:54 http://www.m5home.com/bbs/images/common/back.gif
你是CSDN那位吧,我在CSDN回复过你这个问题的帖子了。在CSDN我也是这个用户名。
你的问题需要解决两个方面:
第一,客户端那边要定义一套单独的通讯协议,即一个字符串格式,比如: ID +内容尺寸+ 内容+结尾命令标识。这样你可以分析收到的字符串内容来判断进行的操作,而不应该使用缓冲区字节数来判断,那样不准。
第二,还是我在CSDN里面留得回复意见,数据处理交给一个Timer过程去处理,不要在 数据接收事件过程中处理,因为如果你客户端多了的话,在数据接收过程内处理数据耗时太长的话,必然会触发 340 号错误:堆栈溢出。
剩下的就是需要在数据接收过程对数据处理过程进行一些检测,因为你新的数据到达后如果老的还未处理完那就不妙了,你要设计一个可行的缓冲处理办法。我的做法是判断发来数据的尾部标识,如果上一个还未处理完,就根据发来的命令决定继续的操作类型,这个要根据你的实际工作情况来判断了。
我的因为只是简单的检索数据库,发送数据库内可以发送的内容给客户端,然后客户端回复一个收到数据并处理完成的指令来服务端,由服务端根据这个指令判断是否设置数据库内的那条发送过的信息标志位为已发。如果客户端收到数据后处理失败或者接收数据缺损,则会回复一条继续检索数据库内信息的指令过来,这时,服务端就不用设置那条发送过的信息的标识。而是继续检索发送。这个要根据你自己的实际工作情况来设计了。 刚才和 2# 的 zzd1000 聊了一下,他遇到的问题主要是由于 Call Recv1(index, Data)这几个过程阻塞,导致 无法触发,最后 winsock 缓冲区阻塞,数据接收事件就没了。
他的客户端数量不多,这个问题容易解决。将 Call Recv1(index, Data)这几个过程 调离 数据接收过程应该就成了。
我的问题呢。。。。。我现在想用老马的 API winsock 自定义控件试试并发效果,如果处理 .senddata 的速度比 winsock 本身快的话,应该也就成了。 其实基础的手法就是使用单元线程,但这种方式的工程结构应该如何设计与规划,又是一个新问题了.
对于VB6来 ...
马大哈 发表于 2009-11-24 01:08 http://www.m5home.com/bbs/images/common/back.gif
关于网上的多线程的资料,老马的单元线程实现多线程的例子已经足够了。剩下的就是优化问题了。我在 VBgood 转了好几天,都没有用单元线程的任何资料与提法来做多线程,大家都是说用 API 的创建线程函数来做,那个。。稳定性头疼呢,我一看到CopyMemory Address proc 就头大,系统崩溃就是由这个方法开始的。 其实单元线程是COM模型中的标准用法之一,并非是VB6独创的东东.....
我看了一些API实现的VB6稳定多线程,有的是模拟了一个类似.NET中委托的东东,有的干脆就是调用COM相关API自己来创建了单元.....
虽然都能达到相对的稳定,可是,这与单元线程有什么区别呢?
还不如直接使用这种方式呢......省事点.
另外对于上面说到的溢出以及数据丢失问题,这个其实可以从协议上解决.
即本次发送给接收方的数据在未处理完之前,不再发送下一次的数据.
也就是说,服务器在处理完数据后,通知客户端继续发送,这样的话可以从流程上保证不出问题.
之前与一个朋友说到了丢包的问题,他说使用的是TCP协议,怎么可能丢包.
没错,包是丢不了的,但那是非常底层的事了,这些数据不是在那一层丢的,而是在上层,我们的应用层丢掉的.
所以需要我们的应用层协议来保证数据的正常收发.
至于崩溃........我在多年前的师父在我刚刚到他门下,还连类模块都不会用时,给我说过一句话:
写程序,能不用API就尽量不用API,能不用第三方控件,就尽量不使用.
现在回想起来,哎,真的啊,一切又回到最初了...... 另外对于上面说到的溢出以及数据丢失问题,这个其实可以从协议上解决.
即本次发送给接收方的数据在未处理完之前,不再发送下一次的数据.
也就是说,服务器在处理完数据后,通知客户端继续发送,这样的话可以从流程上保证不出问题.
之前与一个朋友说到了丢包的问题,他说使用的是TCP协议,怎么可能丢包.
没错,包是丢不了的,但那是非常底层的事了,这些数据不是在那一层丢的,而是在上层,我们的应用层丢掉的.
所以需要我们的应用层协议来保证数据的正常收发.
马大哈 发表于 2009-11-26 00:43 http://www.m5home.com/bbs/images/common/back.gif
这个问题昨晚我仔细和他交流过了。它的数据丢失的不是当前连接,而是另一个winsock的控件数组元素的数据丢失,我仔细询问了他的通讯协议了,他的通讯过程是当前客户端发送数据过来后在服务器返回指令前,这个连接的客户端是不会再发送数据过来的,因此对于当前连接来说不存在缓冲区数据丢失的问题。因此只要将那个耗时的处理过程移出数据接受事件就没事了。而且他的客户端最多不超过10个,处理起来和一个也没多少资源消耗区别。我已经设计了一个用多线程来进行数据处理的简单思路给他了,剩下的他自己应该能搞定了。
至于崩溃........我在多年前的师父在我刚刚到他门下,还连类模块都不会用时,给我说过一句话:
写程序,能不用API就尽量不用API,能不用第三方控件,就尽量不使用.
现在回想起来,哎,真的啊,一切又回到最初了......
马大哈 发表于 2009-11-26 00:43 http://www.m5home.com/bbs/images/common/back.gif
老马的师傅这句话很值得学习。受教了。
自从学会了用API以来,我一般很少用 包括VB6自己的那些OCX了, 大部分功能我都用API去做,省的大家对VB6最大的歧视就是那众多的OCX,比如文件选择、目录选择、调色板、等等这些功能,都用API来实现。现在看来我这个思路也有待修订了。 老马的师傅这句话很值得学习。受教了。
自从学会了用API以来,我一般很少用 包括VB6自己的那些OCX了, 大部分功能我都用API去做,省的大家对VB6最大的歧视就是那众多的OCX,比如文件选择、目录选择、调色板、等等这些功能,都用API来实现。现在看来我这个思路也有待修订了。
jessezappy 发表于 2009-11-26 13:10 http://www.m5home.com/bbs/images/common/back.gif
哎....和我一样,我也是能用API实现就尽量不用OCX,结果尝到了苦头......看这里:
http://www.m5home.com/blog/article.asp?id=294
这就是一个使用了很多API的工程,还有很多内嵌汇编的手法......结果,头痛啊......
现在另一个工程中,我得换换思路了...... 本帖最后由 jessezappy 于 2010-1-15 22:21 编辑
老马,好久不见了。前久论坛一直打不开,再加上单位年底事情多,所以一直没来。
今天来了是想继续汇报一下这个多线程服务器程序运用和测试中发现的一些问题,原先说过,我改出来的这个多线程服务器客户端登录过多之后会出现线程响应迟缓的问题,就是点击主进程窗口任何位置会出现那个“ windows 运用程序忙”然后问我是“切换到”还是“重试”的这个窗口,一直以为是线程间数据传递闹的,这几天得闲来改改程序测试了一下,关闭了所有线程间数据传递,问题依旧。又以为是同一线程内客户端连接数量过多了闹的,因为客户端不登陆不会出现这个情况,于是将线程内客户端允许连接数量从20测试到100,问题依旧。最后静下来仔细看了一下,一旦出现这个情况后,SqlServer的CPU占用就会从50%左右降至接近0,因此怀疑是SQLserver响应慢造成的,于是将执行的最频繁的每客户端登录都查询数据库的那条语句禁止了,将其他代码恢复为原状态,一测试,好了,不再出现这个不响应的提示窗口了。
这下算是找到问题了,但是解决起来就头大了。因为我的设计逻辑是每个客户端心跳信息到达3秒后就查询数据库中是否有该客户端的未发送信息,如果有就发送,如果没有就跳过。
也就是说我以前是单线程的时候,同一时间只可能有一条SQL语句提交到SQLSERVER,现在改成多线程了,那么在同一时刻,就可能会有相应线程数量的SQL语句同时提交到SQLSERVER,造成SQLSERVER响应迟缓最后锁死,半天不能返回结果,追踪导致我的线程内执行查询语句的那行代码卡住,我的查询语句很简单:
"SELECT top 1 TerminalID,KindID,contentnote,SID,SendTime From SendNote where (SendORNot='0') and (TerminalID='0906181402') ORDER BY SendTime"
意为将发送时间标记最早的那一条未发送信息取出。
然后将其中检索出的内容单独取出,重新组合成发送到客户端的信息,发送。我的查询是这样操作的:
RS.CursorLocation = adUseClient
RS.Open SQLstr, DB
之后判断 If RS.RecordCount > 0 Then 是否有返回值,如果有则用:
RS.Fields("KindID").Value
这样的方式取出值。
我找了好多SQL查询优化的资料都没有想到适合我这个查询的解决办法,请老马帮忙出出主意,看看能有什么优化的办法,因为我总感觉我这个SQL查询的思路有点问题。
补充说明:我的每个线程是单独和数据库进行连接的 Private DB As Connection, RS As Recordset
因为线程间不能共享数据库连接对象。 忽然想起看下事件日志,发现这样的字样:
17052:
SQL Server 已为 8 个并发查询而优化。13 个查询超过了此限制,因而性能可能会受到不良影响。 :L查了一下查到:http://www.9enjoy.com/post/142/
我也查了一下我的SQLServer版本,发现也是:Microsoft SQL Server2000 - 8.00.2039 (Intel X86) May3 2005 23:18:38 Copyright (c) 1988-2003 Microsoft CorporationPersonal Edition on Windows NT 5.2 (Build 3790: Service Pack 2)
。这就郁闷了,在办公室下载安装的居然是个人版!。还好家里好像有企业版的安装包,明天拿来安装试试。 捣腾了半天时间,终于成功安装好了 SQLserver2000企业版了。
Microsoft SQL Server2000 - 8.00.2039 (Intel X86) May3 2005 23:18:38 Copyright (c) 1988-2003 Microsoft CorporationEnterprise Edition on Windows NT 5.2 (Build 3790: Service Pack 2)
这回版本应该没错了。再次测试了服务器程序,这回没有再出现那个运用程序忙的提示了。只是主界面依然有点点卡,看来这个是线程间数据传递闹的了。
请教下老马,不知道用注册表来进行线程间数据传递是否会提高点效率? 使用注册表的话,你不如直接使用内存,即在本进程申请一块内存,然后再通知目标线程申请的内存的指针,进而目标线程访问内存,得到内容.
不过........好象也绕远了点.
但我猜测,由于这个动作并未使用VB自己的机制(使用的是API),因此不会有线程间调度产生,则数据传输效率会比直接线程间调度高.
线程间调度性能低,好象主要是因为切换线程时有个恢复"现场"的数据环境复制过程吧.....具体细节没注意,但如果你通讯过程并不频繁,且数据量不大,应该不会引起什么明显的性能问题.
这个得仔细分析. 本帖最后由 jessezappy 于 2010-1-25 12:53 编辑
老马你说的这个方法值得一试啊.
"由于这个动作并未使用VB自己的机制(使用的是API),因此不会有线程间调度产生,则数据传输效率会比直接线程间调度高.
"
只是..还没搞过这种方法的VB代码,我还得查查资料.
现在的情况是每客户端10秒发送一次心跳,每个心跳到达我都必须知道这个客户端ID对应的一些数据,需要从数据库查询,而对这个客户端的操作也有数据需要写进数据库中,我现在是采用1分钟每个线程自己查询一次数据库的办法,然后客户端ID心跳到达后就从已经从数据库中读出的这些数据中自己取数据,这个倒是明显降低了SqlServer 的CPU占用了.现在就是还有个客户端的显示屏开关状态需要交换到数据库中.只剩下这个数据需要在线程间传递了.
我试试你说的办法先.
忘记说了,现在客户端数量大约2500个,我每个线程接待30到50个,GPRS网速慢啊. 用以上方法已基本搞定那个多线程了。现在不需要返回线程间数据,只保留了调试时才返回的控制过程,程序不会卡的不动了。
今天回来收下贴。
页:
[1]