|
|
2007年12月24日
即BCD代码。Binary-Coded Decimal,简称BCD,称BCD码或二-十进制代码,亦称二进码十进数。是一种 二进制的数字编码形式,用二进制编码的 十进制代码。这种编码形式利用了四个位元来储存一个十进制的数码,使二进制和十进制之间的转换得以快捷的进行。这种编码技巧,最常用于会计系统的设计里,因为会计制度经常需要对很长的数字串作准确的计算。相对于一般的浮点式记数法,采用BCD码,既可保存数值的精确度,又可免却使电脑作浮点运算时所耗费的时间。此外,对于其他需要高精确度的计算,BCD编码亦很常用。 由于十进制数共有0、1、2、……、9十个数码,因此,至少需要4位二进制码来表示1位十进制数。4位二进制码共有2^4=16种码组,在这16种代码中,可以任选10种来表示10个十进制数码,共有N=16!/(16-10)!约等于2.9乘以10的10次方种方案。常用的BCD代码列于末。 常用BCD编码方式最常用的BCD编码,就是使用"0"至"9"这十个数值的二进码来表示。这种编码方式,在中国大陆称之为“8421码”。除此以外,对应不同需求,各人亦开发了不同的编码方法,以适应不同的需求。这些编码,大致可以分成有权码和无权码两种: 有权BCD码,如:8421(最常用)、2421、5421… 无权BCD码,如:余3码、格雷码… 常用 BCD编码表
|
|
8421码
|
5421码
|
2421码
|
5211码
|
余3码
|
|
0
|
0000
|
0000
|
0000
|
0000
|
0000
|
|
1
|
0001
|
0001
|
0001
|
0001
|
0100
|
|
2
|
0010
|
0010
|
0010
|
0100
|
0101
|
|
3
|
0011
|
0011
|
0011
|
0101
|
0110
|
|
4
|
0100
|
0100
|
0100
|
0111
|
0111
|
|
5
|
0101
|
1000
|
0101
|
1000
|
1000
|
|
6
|
0110
|
1001
|
0110
|
1001
|
1001
|
|
7
|
0111
|
1010
|
0111
|
1100
|
1010
|
|
8
|
1000
|
1011
|
1110
|
1101
|
1011
|
|
9
|
1001
|
1100
|
1111
|
1111
|
1100
|
|
权
|
8421
|
5421
|
2421
|
5211
|
|
|
非压缩式和压缩式BCD码 BCD又分为两种,非压缩式和压缩式两种。 前面这种81存成 “08,01” 是非压缩式,而压缩式会存成 “81h” (直接以十六进制储存)。非压缩的BCD码只有低四位有效,而压缩的BCD码则将高四位也用上了,就是说一个字节有两个BCD码。BCD是用0和1表示十进制,如0000表示0,0001表示1,0010表示2。而压缩的BCD是用00表示0,01表示1,10表示2,110表示3等。 例: 1234表示成非压缩的BCD码是00000001000000100000001100000100,也就是0x01020304;而压缩BCD码则表示成0001001000110100,也就是0x1234。 但压缩的BCD并不固定,可看情况而定,所要的就是用最少的位数表示尽可能多的数。
2007年12月21日
相关技术:
- 连接池
- 引用记数
- 多线程
- Timer类运行基理
- C#.Net
适宜人群
- 数据库应用程序程序员
- 系统分析员
- 模块设计师
- 有一定功底的程序员
目录
- 引言
- 数据库连接池(Connection Pool)的工作原理
- 连接池关键问题分析
- 并发问题
- 事务处理
- 连接池的分配与释放
- 连接池的配置与维护
-
关键议题
-
引用记数
-
如何实现事务处理
-
-
-
构造方法
-
启动服务StartService
-
停止服务StopService
-
申请 GetConnectionFormPool
-
释放DisposeConnection
-
如何更新属性
-
如何确定连接是否失效
-
使用线程管理连接池
-
threadCreate
-
threadCheck
-
引言
一般的数据库应用程序大致都遵循下面的步骤:
- 初始化程序
- 用户在UI上输入操作
- 由用户操作产生数据库操作
- 将数据库操作递交到数据库服务器
- .... (重复2~4)
- 关闭应用程序
而本文则着重讲解上面第4步骤.在着一步骤中我们经常是,打开数据库连接操作数据库,最后关闭数据库. 在服务器端程序设计上与数据库的操作显得十分重要,因为你要处理的数据操作十分巨大.如果频繁创建数据库连接频繁关闭数据库连接则会引起效率低下甚至引发程序崩溃. 也许我们可以有另一种操作数据库的形式,我们可以在程序运行时打开一个数据库连接,让这个连接永久存在直到程序'死亡',那么这样做也有不安全隐患,我们知道一个对象存在时间越长或被使用次数越多则它表现的越不稳定,着不稳定因素是由于对象内部可能存在的潜在设计问题产生,对于数据库连接对象道理也一样.我们不能保证一个Connection对象里面能一点问题不存在.所以我们也不敢长时间将它长时间占用内存. 既然有这么多的问题由此我们需要一个能帮我们维护数据库连接的东西-它就是连接池,网上有很多的连接池例子,但是多数都是简单的例子,或者介绍比较复杂的连接池原理,没有一个比较完整介绍和实现连接池的例子.这里就介绍你如何自己制作一个连接池. 对于共享资源,有一个很著名的设计模式:资源池(Resource Pool)。该模式正是为了解决资源的频繁分配﹑释放所造成的问题。为解决我们的问题,可以采用数据库连接池技术。数据库连接池的基本思想就是为数据库连接建立一个“缓冲池”。预先在缓冲池中放入一定数量的连接,当需要建立数据库连接时,只需从“缓冲池”中取出一个,使用完毕之后再放回去。我们可以通过设定连接池最大连接数来防止系统无尽的与数据库连接。更为重要的是我们可以通过连接池的管理机制监视数据库的连接的数量﹑使用情况,为系统开发﹑测试及性能调整提供依据。连接池的基本工作原理见下图。
数据库连接池(Connection Pool)的工作原理

连接池关键问题分析
1、并发问题
为了使连接管理服务具有最大的通用性,必须考虑多线程环境,即并发问题。这个问题相对比较好解决,因为各个语言自身提供了对并发管理的支持像java,c#等等,使用synchronized(java)lock(C#)关键字即可确保线程是同步的。使用方法可以参考,相关文献。
2、事务处理
我们知道,事务具有原子性,此时要求对数据库的操作符合“ALL-ALL-NOTHING”原则,即对于一组SQL语句要么全做,要么全不做。 我们知道当2个线程公用一个连接Connection对象,而且各自都有自己的事务要处理时候,对于连接池是一个很头疼的问题,因为即使Connection类提供了相应的事务支持,可是我们仍然不能确定那个数据库操作是对应那个事务的,这是由于我们有2个线程都在进行事务操作而引起的。为此我们可以使用每一个事务独占一个连接来实现,虽然这种方法有点浪费连接池资源但是可以大大降低事务管理的复杂性。
3、连接池的分配与释放
连接池的分配与释放,对系统的性能有很大的影响。合理的分配与释放,可以提高连接的复用度,从而降低建立新连接的开销,同时还可以加快用户的访问速度。 对于连接的管理可使用一个List。即把已经创建的连接都放入List中去统一管理。每当用户请求一个连接时,系统检查这个List中有没有可以分配的连接。如果有就把那个最合适的连接分配给他(如何能找到最合适的连接文章将在关键议题中指出);如果没有就抛出一个异常给用户,List中连接是否可以被分配由一个线程来专门管理捎后我会介绍这个线程的具体实现。
4、连接池的配置与维护
连接池中到底应该放置多少连接,才能使系统的性能最佳?系统可采取设置最小连接数(minConnection)和最大连接数(maxConnection)等参数来控制连接池中的连接。比方说,最小连接数是系统启动时连接池所创建的连接数。如果创建过多,则系统启动就慢,但创建后系统的响应速度会很快;如果创建过少,则系统启动的很快,响应起来却慢。这样,可以在开发时,设置较小的最小连接数,开发起来会快,而在系统实际使用时设置较大的,因为这样对访问客户来说速度会快些。最大连接数是连接池中允许连接的最大数目,具体设置多少,要看系统的访问量,可通过软件需求上得到。 如何确保连接池中的最小连接数呢?有动态和静态两种策略。动态即每隔一定时间就对连接池进行检测,如果发现连接数量小于最小连接数,则补充相应数量的新连接,以保证连接池的正常运转。静态是发现空闲连接不够时再去检查。
使用连接池
连接到数据库服务器通常由几个需要很长时间的步骤组成。必须建立物理通道(例如套接字或命名管道),必须与服务器进行初次握手,必须分析连接字符串信息,必须由服务器对连接进行身份验证,必须运行检查以便在当前事务中登记,等等。
实际上,大多数应用程序仅使用一个或几个不同的连接配置。这意味着在执行应用程序期间,许多相同的连接将反复地打开和关闭。为了使打开的连接成本最低,ADO.NET 使用称为连接池的优化方法。
连接池减少新连接需要打开的次数。池进程保持物理连接的所有权。通过为每个给定的连接配置保留一组活动连接来管理连接。只要用户在连接上调用 Open,池进程就会检查池中是否有可用的连接。如果某个池连接可用,会将该连接返回给调用者,而不是打开新连接。应用程序在该连接上调用 Close 时,池进程会将连接返回到活动连接池集中,而不是真正关闭连接。连接返回到池中之后,即可在下一个 Open 调用中重复使用。
只有配置相同的连接可以建立池连接。ADO.NET 同时保留多个池,每个配置一个池。连接由连接字符串以及 Windows 标识(在使用集成的安全性时)分为多个池。
池连接可以大大提高应用程序的性能和可缩放性。默认情况下,ADO.NET 中启用连接池。除非显式禁用,否则,连接在应用程序中打开和关闭时,池进程将对连接进行优化。还可以提供几个连接字符串修饰符来控制连接池的行为。有关更多信息,请参见本主题后面的“使用连接字符串关键字控制连接池”。
池的创建和分配
在初次打开连接时,将根据完全匹配算法创建连接池,该算法将池与连接中的连接字符串关联。每个连接池与不同的连接字符串关联。打开新连接时,如果连接字符串并非与现有池完全匹配,将创建一个新池。按进程、按应用程序域、按连接字符串以及(在使用集成的安全性时)按 Windows 标识来建立池连接。
在以下 C# 示例中创建了三个新的 SqlConnection 对象,但是管理时只需要两个连接池。注意,根据为 Initial Catalog 分配的值,第一个和第二个连接字符串有所不同。
using (SqlConnection connection = new SqlConnection(
"Integrated Security=SSPI;Initial Catalog=Northwind"))
{
connection.Open();
// Pool A is created.
}
using (SqlConnection connection = new SqlConnection(
"Integrated Security=SSPI;Initial Catalog=pubs"))
{
connection.Open();
// Pool B is created because the connection strings differ.
}
using (SqlConnection connection = new SqlConnection(
"Integrated Security=SSPI;Initial Catalog=Northwind"))
{
connection.Open();
// The connection string matches pool A.
}
如果 MinPoolSize 在连接字符串中未指定或指定为零,池中的连接将在一段时间不活动后关闭。但是,如果指定的 MinPoolSize 大于零,在 AppDomain 被卸载并且进程结束之前,连接池不会被破坏。非活动或空池的维护只需要最少的系统开销。
注意 |
|
如果发生致命错误(例如故障转移或注册表中的别名更改),池将自动清除。
|
添加连接
连接池是为每个唯一的连接字符串创建的。当创建一个池后,将创建多个连接对象并将其添加到该池中,以满足最小池大小的要求。连接根据需要添加到池中,但是不能超过指定的最大池大小(默认值为 100)。连接在关闭或断开时释放回池中。
在请求 SqlConnection 对象时,如果存在可用的连接,将从池中获取该对象。连接要可用,必须未使用,具有匹配的事务上下文或未与任何事务上下文关联,并且具有与服务器的有效链接。
连接池进程通过在连接释放回池中时重新分配连接,来满足这些连接请求。如果已达到最大池大小且不存在可用的连接,则该请求将会排队。然后,池进程尝试重新建立任何连接,直到到达超时时间(默认值为 15 秒)。如果池进程在连接超时之前无法满足请求,将引发异常。
警告 |
|
我们建议您在使用完连接时一定要关闭连接,以便连接可以返回池。要关闭连接,可以使用 Connection 对象的 Close 或 Dispose 方法,也可以通过在 C# 的 using 语句中或在 Visual Basic 的 Using 语句中打开所有连接。不是显式关闭的连接可能不会添加或返回到池中。例如,如果连接已超出范围但没有显式关闭,则仅当达到最大池大小而该连接仍然有效时,该连接才会返回到连接池中。有关更多信息,请参见 Visual Basic 的using 语句(C# 参考)或如何:释放系统资源。
|
注意 |
|
不要在类的 Finalize 方法中对 Connection、DataReader 或任何其他托管对象调用 Close 或 Dispose。在终结器中,仅释放类直接拥有的非托管资源。如果类不拥有任何非托管资源,则不要在类定义中包含 Finalize 方法。有关更多信息,请参见垃圾回收。
|
移除连接
连接池进程定期扫描连接池,查找没有通过 Close 或 Dispose 关闭的未用连接,并重新建立找到的连接。如果应用程序没有显式关闭或断开其连接,连接池进程可能需要很长时间才能重新建立连接,所以,最好确保在连接中显式调用 Close 和 Dispose。
如果连接长时间空闲,或池进程检测到与服务器的连接已断开,连接池进程会将该连接从池中移除。注意,只有在尝试与服务器进行通信之后才能检测到断开的连接。如果发现某连接不再连接到服务器,则会将其标记为无效。无效连接只有在关闭或重新建立后,才会从连接池中移除。
如果存在与已消失的服务器的连接,那么即使连接池管理程序未检测到已断开的连接并将其标记为无效,仍有可能将此连接从池中取出。这种情况是因为检查连接是否仍有效的系统开销将造成与服务器的另一次往返,从而抵消了池进程的优势。发生此情况时,初次尝试使用该连接将检测连接是否曾断开,并引发异常。
清除池
ADO.NET 2.0 引入了两种新的方法来清除池:ClearAllPools 和 ClearPool。ClearAllPools 清除给定提供程序的连接池,ClearPool 清除与特定连接关联的连接池。如果在调用时连接正在使用,将进行相应的标记。连接关闭时,将被丢弃,而不是返回池中。
事务支持
连接是根据事务上下文来从池中取出并进行分配的。除非在连接字符串中指定了 Enlist=false,否则,连接池将确保连接在 Current 上下文中登记。如果连接使用登记的 System.Transactions 事务关闭并返回池中,连接将保留在池中,以便使用相同 System.Transactions 事务对该连接池的下一次请求将返回相同的连接。如果该事务没有可用连接,在该连接打开时,将自动注册该连接。
当连接关闭时,它将被释放回池中,并根据其事务上下文放入相应的子部分。因此,即使分布式事务仍然挂起,仍可以关闭该连接而不会生成错误。这样,您就可以在随后提交或中止分布式事务。
使用连接字符串关键字控制连接池
SqlConnection 对象的 ConnectionString 属性支持连接字符串键/值对,可以用于调整连接池逻辑的行为。有关更多信息,请参见 ConnectionString。
池碎片
池碎片是许多 Web 应用程序中的一个常见问题,应用程序可能会创建大量在进程退出后才会释放的池。这样,将打开大量的连接,占用许多内存,从而影响性能。
因为集成安全性产生的池碎片
连接根据连接字符串以及用户标识来建立池连接。因此,如果使用网站上的基本身份验证或 Windows 身份验证以及集成的安全登录,每个用户将获得一个池。尽管这样可以提高单个用户的后续数据库请求的性能,但是该用户无法利用其他用户建立的连接。这样还使每个用户至少产生一个与数据库服务器的连接。这对特定 Web 应用程序结构会产生副作用,因为开发人员需要衡量安全性和审计要求。
因为许多数据库产生的池碎片
许多 Internet 服务提供商在一台服务器上托管多个网站。他们可能使用单个数据库确认窗体身份验证登录,然后为该用户或用户组打开与特定数据库的连接。与身份验证数据库的连接将建立池连接,供每个用户使用。但是,每个数据库的连接存在一个独立的池,因此增加了与服务器的连接数。
这也会对应用程序设计产生副作用。但是,可以通过一个相对简单的方式避免此副作用,而又不会影响连接 SQL Server 时的安全性。不是为每个用户或组连接独立的数据库,而是连接到服务器上的相同数据库,然后执行 Transact-SQL USE 语句来切换为所需的数据库。以下代码段演示入如何创建与 master 数据库的初始连接,然后切换到 databaseName 字符串变量中指定的所需数据库。
C#: // Assumes that command is a SqlCommand object. using (SqlConnection connection = new SqlConnection( "Server=MSSQL1;uid=xxx;pwd=xxx;database=master")) { connection.Open(); command.ExecuteNonQuery("USE " + databaseName); }
应用程序角色和连接池
2007年12月4日
中国科学院光电技术研究所 游志宇在工业生产控制系统中,有许多需要定时完成的操作,如定时显示当前时间,定时刷新屏幕上的进度条,上位 机定时向下位机发送命令和传送数据等。特别是在对控制性能要求较高的实时控制系统和数据采集系统中,就更需要精确定时操作。 众所周知,Windows 是基于消息机制的系统,任何事件的执行都是通过发送和接收消息来完成的。 这样就带来了一些问题,如一旦计算机的CPU被某个进程占用,或系统资源紧张时,发送到消息队列 中的消息就暂时被挂起,得不到实时处理。因此,不能简单地通过Windows消息引发一个对定时要求 严格的事件。另外,由于在Windows中已经封装了计算机底层硬件的访问,所以,要想通过直接利用 访问硬件来完成精确定时,也比较困难。所以在实际应用时,应针对具体定时精度的要求,采取相适 应的定时方法。 VC中提供了很多关于时间操作的函数,利用它们控制程序能够精确地完成定时和计时操作。本文详细介绍了 VC中基于Windows的精确定时的七种方式,如下图所示:
 图一 图像描述
方式一:VC中的WM_TIMER消息映射能进行简单的时间控制。首先调用函数SetTimer()设置定时 间隔,如SetTimer(0,200,NULL)即为设置200ms的时间间隔。然后在应用程序中增加定时响应函数 OnTimer(),并在该函数中添加响应的处理语句,用来完成到达定时时间的操作。这种定时方法非常 简单,可以实现一定的定时功能,但其定时功能如同Sleep()函数的延时功能一样,精度非常低,最小 计时精度仅为30ms,CPU占用低,且定时器消息在多任务操作系统中的优先级很低,不能得到及时响 应,往往不能满足实时控制环境下的应用。只可以用来实现诸如位图的动态显示等对定时精度要求不高的情况。如示例工程中的Timer1。 方式二:VC中使用sleep()函数实现延时,它的单位是ms,如延时2秒,用sleep(2000)。精度非常 低,最小计时精度仅为30ms,用sleep函数的不利处在于延时期间不能处理其他的消息,如果时间太 长,就好象死机一样,CPU占用率非常高,只能用于要求不高的延时程序中。如示例工程中的Timer2。 方式三:利用COleDateTime类和COleDateTimeSpan类结合WINDOWS的消息处理过程来实现秒级延时。如示例工程中的Timer3和Timer3_1。以下是实现2秒的延时代码: COleDateTime start_time = COleDateTime::GetCurrentTime(); COleDateTimeSpan end_time= COleDateTime::GetCurrentTime()-start_time; while(end_time.GetTotalSeconds()< 2) //实现延时2秒 { MSG msg; GetMessage(&msg,NULL,0,0); TranslateMessage(&msg); DispatchMessage(&msg); //以上四行是实现在延时或定时期间能处理其他的消息, //虽然这样可以降低CPU的占有率, //但降低了延时或定时精度,实际应用中可以去掉。 end_time = COleDateTime::GetCurrentTime()-start_time; }//这样在延时的时候我们也能够处理其他的消息。 方式四:在精度要求较高的情况下,VC中可以利用GetTickCount()函数,该函数的返回值是 DWORD型,表示以ms为单位的计算机启动后经历的时间间隔。精度比WM_TIMER消息映射高,在较 短的定时中其计时误差为15ms,在较长的定时中其计时误差较低,如果定时时间太长,就好象死机一样,CPU占用率非常高,只能用于要求不高的延时程序中。如示例工程中的Timer4和Timer4_1。下列代码可以实现50ms的精确定时:
DWORD dwStart = GetTickCount();
DWORD dwEnd = dwStart;
do
{
dwEnd = GetTickCount()-dwStart;
}while(dwEnd <50);
为使GetTickCount()函数在延时或定时期间能处理其他的消息,可以把代码改为:
DWORD dwStart = GetTickCount();
DWORD dwEnd = dwStart;
do
{
MSG msg;
GetMessage(&msg,NULL,0,0);
TranslateMessage(&msg);
DispatchMessage(&msg);
dwEnd = GetTickCount()-dwStart;
}while(dwEnd <50);
虽然这样可以降低CPU的占有率,并在延时或定时期间也能处理其他的消息,但降低了延时或定时精度。 方式五:与GetTickCount()函数类似的多媒体定时器函数DWORD timeGetTime(void),该函数定时精 度为ms级,返回从Windows启动开始经过的毫秒数。微软公司在其多媒体Windows中提供了精确定时器的底 层API持,利用多媒体定时器可以很精确地读出系统的当前时间,并且能在非常精确的时间间隔内完成一 个事件、函数或过程的调用。不同之处在于调用DWORD timeGetTime(void) 函数之前必须将 Winmm.lib 和 Mmsystem.h 添加到工程中,否则在编译时提示DWORD timeGetTime(void)函数未定义。由于使用该 函数是通过查询的方式进行定时控制的,所以,应该建立定时循环来进行定时事件的控制。如示例工程中的Timer5和Timer5_1。 方式六:使用多媒体定时器timeSetEvent()函数,该函数定时精度为ms级。利用该函数可以实现周期性的函数调用。如示例工程中的Timer6和Timer6_1。函数的原型如下:
MMRESULT timeSetEvent( UINT uDelay,
UINT uResolution,
LPTIMECALLBACK lpTimeProc,
WORD dwUser,
UINT fuEvent )
该函数设置一个定时回调事件,此事件可以是一个一次性事件或周期性事件。事件一旦被激活,便调用指定的回调函数, 成功后返回事件的标识符代码,否则返回NULL。函数的参数说明如下:
uDelay:以毫秒指定事件的周期。
Uresolution:以毫秒指定延时的精度,数值越小定时器事件分辨率越高。缺省值为1ms。
LpTimeProc:指向一个回调函数。
DwUser:存放用户提供的回调数据。
FuEvent:指定定时器事件类型:
TIME_ONESHOT:uDelay毫秒后只产生一次事件
TIME_PERIODIC :每隔uDelay毫秒周期性地产生事件。
具体应用时,可以通过调用timeSetEvent()函数,将需要周期性执行的任务定义在LpTimeProc回调函数 中(如:定时采样、控制等),从而完成所需处理的事件。需要注意的是,任务处理的时间不能大于周期间隔时间。另外,在定时器使用完毕后, 应及时调用timeKillEvent()将之释放。 方式七:对于精确度要求更高的定时操作,则应该使用QueryPerformanceFrequency()和 QueryPerformanceCounter()函数。这两个函数是VC提供的仅供Windows 95及其后续版本使用的精确时间函数,并要求计算机从硬件上支持精确定时器。如示例工程中的Timer7、Timer7_1、Timer7_2、Timer7_3。 QueryPerformanceFrequency()函数和QueryPerformanceCounter()函数的原型如下:
BOOL QueryPerformanceFrequency(LARGE_INTEGER *lpFrequency);
BOOL QueryPerformanceCounter(LARGE_INTEGER *lpCount);
数据类型ARGE_INTEGER既可以是一个8字节长的整型数,也可以是两个4字节长的整型数的联合结构, 其具体用法根据编译器是否支持64位而定。该类型的定义如下:
typedef union _LARGE_INTEGER
{
struct
{
DWORD LowPart ;// 4字节整型数
LONG HighPart;// 4字节整型数
};
LONGLONG QuadPart ;// 8字节整型数
}LARGE_INTEGER ;
在进行定时之前,先调用QueryPerformanceFrequency()函数获得机器内部定时器的时钟频率, 然后在需要严格定时的事件发生之前和发生之后分别调用QueryPerformanceCounter()函数,利用两次获得的计数之差及时钟频率,计算出事件经 历的精确时间。下列代码实现1ms的精确定时:
LARGE_INTEGER litmp;
LONGLONG QPart1,QPart2;
double dfMinus, dfFreq, dfTim;
QueryPerformanceFrequency(&litmp);
dfFreq = (double)litmp.QuadPart;// 获得计数器的时钟频率
QueryPerformanceCounter(&litmp);
QPart1 = litmp.QuadPart;// 获得初始值
do
{
QueryPerformanceCounter(&litmp);
QPart2 = litmp.QuadPart;//获得中止值
dfMinus = (double)(QPart2-QPart1);
dfTim = dfMinus / dfFreq;// 获得对应的时间值,单位为秒
}while(dfTim<0.001);
其定时误差不超过1微秒,精度与CPU等机器配置有关。 下面的程序用来测试函数Sleep(100)的精确持续时间:
LARGE_INTEGER litmp;
LONGLONG QPart1,QPart2;
double dfMinus, dfFreq, dfTim;
QueryPerformanceFrequency(&litmp);
dfFreq = (double)litmp.QuadPart;// 获得计数器的时钟频率
QueryPerformanceCounter(&litmp);
QPart1 = litmp.QuadPart;// 获得初始值
Sleep(100);
QueryPerformanceCounter(&litmp);
QPart2 = litmp.QuadPart;//获得中止值
dfMinus = (double)(QPart2-QPart1);
dfTim = dfMinus / dfFreq;// 获得对应的时间值,单位为秒
由于Sleep()函数自身的误差,上述程序每次执行的结果都会有微小误差。下列代码实现1微秒的精确定时:
LARGE_INTEGER litmp;
LONGLONG QPart1,QPart2;
double dfMinus, dfFreq, dfTim;
QueryPerformanceFrequency(&litmp);
dfFreq = (double)litmp.QuadPart;// 获得计数器的时钟频率
QueryPerformanceCounter(&litmp);
QPart1 = litmp.QuadPart;// 获得初始值
do
{
QueryPerformanceCounter(&litmp);
QPart2 = litmp.QuadPart;//获得中止值
dfMinus = (double)(QPart2-QPart1);
dfTim = dfMinus / dfFreq;// 获得对应的时间值,单位为秒
}while(dfTim<0.000001);
其定时误差一般不超过0.5微秒,精度与CPU等机器配置有关。
一、前言
自从微软推出 16 位的 Windows 操作系统起,此后每种版本的 Windows 操作系统都非常依赖于动态链接库 (DLL) 中的函数和数据,实际上 Windows 操作系统中几乎所有的内容都由 DLL 以一种或另外一种形式代表着,例如显示的字体和图标存储在 GDI DLL 中、显示 Windows 桌面和处理用户的输入所需要的代码被存储在一个 User DLL 中、 Windows 编程所需要的大量的 API 函数也被包含在 Kernel DLL 中。
在 Windows 操作系统中使用 DLL 有很多优点,最主要的一点是多个应用程序、甚至是不同语言编写的应用程序可以共享一个 DLL 文件,真正实现了资源 " 共享 " ,大大缩小了应用程序的执行代码,更加有效的利用了内存;使用 DLL 的另一个优点是 DLL 文件作为一个单独的程序模块,封装性、独立性好,在软件需要升级的时候,开发人员只需要修改相应的 DLL 文件就可以了,而且,当 DLL 中的函数改变后,只要不是参数的改变 , 程序代码并不需要重新编译。这在编程时十分有用,大大提高了软件开发和维护的效率。
既然 DLL 那么重要,所以搞清楚什么是 DLL 、如何在 Windows 操作系统中开发使用 DLL 是程序开发人员不得不解决的一个问题。本文针对这些问题,通过一个简单的例子,即在一个 DLL 中实现比较最大、最小整数这两个简单函数,全面地解析了在 Visual C++ 编译环境下编程实现 DLL 的过程,文章中所用到的程序代码在 Windows98 系统、 Visual C++6.0 编译环境下通过。
二、 DLL 的概念
DLL 是建立在客户 / 服务器通信的概念上,包含若干函数、类或资源的库文件,函数和数据被存储在一个 DLL (服务器)上并由一个或多个客户导出而使用,这些客户可以是应用程序或者是其它的 DLL 。 DLL 库不同于静态库,在静态库情况下,函数和数据被编译进一个二进制文件(通常扩展名为 *.LIB ), Visual C++ 的编译器在处理程序代码时将从静态库中恢复这些函数和数据并把他们和应用程序中的其他模块组合在一起生成可执行文件。这个过程称为 " 静态链接 " ,此时因为应用程序所需的全部内容都是从库中复制了出来,所以静态库本身并不需要与可执行文件一起发行。
在动态库的情况下,有两个文件,一个是引入库( .LIB )文件,一个是 DLL 文件,引入库文件包含被 DLL 导出的函数的名称和位置, DLL 包含实际的函数和数据,应用程序使用 LIB 文件链接到所需要使用的 DLL 文件,库中的函数和数据并不复制到可执行文件中,因此在应用程序的可执行文件中,存放的不是被调用的函数代码,而是 DLL 中所要调用的函数的内存地址,这样当一个或多个应用程序运行是再把程序代码和被调用的函数代码链接起来,从而节省了内存资源。从上面的说明可以看出, DLL 和 .LIB 文件必须随应用程序一起发行,否则应用程序将会产生错误。
微软的 Visual C++ 支持三种 DLL ,它们分别是 Non-MFC Dll (非 MFC 动态库)、 Regular Dll (常规 DLL )、 Extension Dll (扩展 DLL )。 Non-MFC DLL 指的是不用 MFC 的类库结构,直接用 C 语言写的 DLL ,其导出的函数是标准的 C 接口,能被非 MFC 或 MFC 编写的应用程序所调用。 Regular DLL: 和下述的 Extension Dlls 一样,是用 MFC 类库编写的,它的一个明显的特点是在源文件里有一个继承 CWinApp 的类(注意:此类 DLL 虽然从 CWinApp 派生,但没有消息循环) , 被导出的函数是 C 函数、 C++ 类或者 C++ 成员函数(注意不要把术语 C++ 类与 MFC 的微软基础 C++ 类相混淆),调用常规 DLL 的应用程序不必是 MFC 应用程序,只要是能调用类 C 函数的应用程序就可以,它们可以是在 Visual C++ 、 Dephi 、 Visual Basic 、 Borland C 等编译环境下利用 DLL 开发应用程序。
常规 DLL 又可细分成静态链接到 MFC 和动态链接到 MFC 上的,这两种常规 DLL 的区别将在下面介绍。与常规 DLL 相比,使用扩展 DLL 用于导出增强 MFC 基础类的函数或子类,用这种类型的动态链接库,可以用来输出一个从 MFC 所继承下来的类。
扩展 DLL 是使用 MFC 的动态链接版本所创建的,并且它只被用 MFC 类库所编写的应用程序所调用。例如你已经创建了一个从 MFC 的 CtoolBar 类的派生类用于创建一个新的工具栏,为了导出这个类,你必须把它放到一个 MFC 扩展的 DLL 中。扩展 DLL 和常规 DLL 不一样,它没有一个从 CWinApp 继承而来的类的对象,所以,开发人员必须在 DLL 中的 DllMain 函数添加初始化代码和结束代码。
三、动态链接库的创建
在 Visual C++6.0 开发环境下,打开 FileNewProject 选项,可以选择 Win32 Dynamic-Link Library 或 MFC AppWizard[dll] 来以不同的方式来创建 Non-MFC Dll 、 Regular Dll 、 Extension Dll 等不同种类的动态链接库。
1 . Win32 Dynamic-Link Library 方式创建 Non-MFC DLL 动态链接库
每一个 DLL 必须有一个入口点,这就象我们用 C 编写的应用程序一样,必须有一个 WINMAIN 函数一样。在 Non-MFC DLL 中 DllMain 是一个缺省的入口函数,你不需要编写自己的 DLL 入口函数,用这个缺省的入口函数就能使动态链接库被调用时得到正确的初始化。如果应用程序的 DLL 需要分配额外的内存或资源时,或者说需要对每个进程或线程初始化和清除操作时,需要在相应的 DLL 工程的 .CPP 文件中对 DllMain() 函数按照下面的格式书写。
|
BOOL APIENTRY DllMain(HANDLE hModule,DWORD ul_reason_for_call,LPVOID lpReserved) { switch( ul_reason_for_call ) { case DLL_PROCESS_ATTACH: ....... case DLL_THREAD_ATTACH: ....... case DLL_THREAD_DETACH: ....... case DLL_PROCESS_DETACH: ....... } return TRUE; }
|
参数中, hMoudle 是动态库被调用时所传递来的一个指向自己的句柄 ( 实际上,它是指向 _DGROUP 段的一个选择符 ) ; ul_reason_for_call 是一个说明动态库被调原因的标志,当进程或线程装入或卸载动态链接库的时候,操作系统调用入口函数,并说明动态链接库被调用的原因,它所有的可能值为: DLL_PROCESS_ATTACH: 进程被调用、 DLL_THREAD_ATTACH: 线程被调用、 DLL_PROCESS_DETACH: 进程被停止、 DLL_THREAD_DETACH: 线程被停止; lpReserved 为保留参数。到此为止, DLL 的入口函数已经写了,剩下部分的实现也不难,你可以在 DLL 工程中加入你所想要输出的函数或变量了。
我们已经知道 DLL 是包含若干个函数的库文件,应用程序使用 DLL 中的函数之前,应该先导出这些函数,以便供给应用程序使用。要导出这些函数有两种方法,一是在定义函数时使用导出关键字 _declspec(dllexport) ,另外一种方法是在创建 DLL 文件时使用模块定义文件 .Def 。需要读者注意的是在使用第一种方法的时候,不能使用 DEF 文件。下面通过两个例子来说明如何使用这两种方法创建 DLL 文件。
1 )使用导出函数关键字 _declspec(dllexport) 创建 MyDll.dll ,该动态链接库中有两个函数,分别用来实现得到两个数的最大和最小数。在 MyDll.h 和 MyDLL.cpp 文件中分别输入如下原代码:
|
//MyDLL.h extern "C" _declspec(dllexport) int Max(int a, int b); extern "C" _declspec(dllexport) int Min(int a, int b); //MyDll.cpp #i nclude #i nclude"MyDll.h" int Max(int a, int b) { if(a>=b)return a; else return b; } int Min(int a, int b) { if(a>=b)return b; else return a; }
|
该动态链接库编译成功后,打开 MyDll 工程中的 debug 目录,可以看到 MyDll.dll 、 MyDll.lib 两个文件。 LIB 文件中包含 DLL 文件名和 DLL 文件中的函数名等,该 LIB 文件只是对应该 DLL 文件的 " 映像文件 " ,与 DLL 文件中, LIB 文件的长度要小的多,在进行隐式链接 DLL 时要用到它。读者可能已经注意到在 MyDll.h 中有关键字 "extern C" ,它可以使其他编程语言访问你编写的 DLL 中的函数。
2 )用 .def 文件创建工程 MyDll
为了用 .def 文件创建 DLL ,请先删除上个例子创建的工程中的 MyDll.h 文件,保留 MyDll.cpp 并在该文件头删除 #i nclude MyDll.h 语句,同时往该工程中加入一个文本文件,命名为 MyDll.def ,再在该文件中加入如下代码:
LIBRARY MyDll EXPORTS Max Min
其中 LIBRARY 语句说明该 def 文件是属于相应 DLL 的, EXPORTS 语句下列出要导出的函数名称。我们可以在 .def 文件中的导出函数后加 @n ,如 Max@1 , Min@2 ,表示要导出的函数顺序号,在进行显式连时可以用到它。该 DLL 编译成功后,打开工程中的 Debug 目录,同样也会看到 MyDll.dll 和 MyDll.lib 文件。
2 . MFC AppWizard[dll] 方式生成常规 / 扩展 DLL
在 MFC AppWizard[dll] 下生成 DLL 文件又有三种方式,在创建 DLL 是,要根据实际情况选择创建 DLL 的方式。一种是常规 DLL 静态链接到 MFC ,另一种是常规 DLL 动态链接到 MFC 。两者的区别是:前者使用的是 MFC 的静态链接库,生成的 DLL 文件长度大,一般不使用这种方式,后者使用 MFC 的动态链接库,生成的 DLL 文件长度小;动态链接到 MFC 的规则 DLL 所有输出的函数应该以如下语句开始:
|
AFX_MANAGE_STATE(AfxGetStaticModuleState( )) // 此语句用来正确地切换 MFC 模块状态
|
最后一种是 MFC 扩展 DLL ,这种 DLL 特点是用来建立 MFC 的派生类, Dll 只被用 MFC 类库所编写的应用程序所调用。前面我们已经介绍过, Extension DLLs 和 Regular DLLs 不一样,它没有一个从 CWinApp 继承而来的类的对象,编译器默认了一个 DLL 入口函数 DLLMain() 作为对 DLL 的初始化,你可以在此函数中实现初始化 , 代码如下:
|
BOOL WINAPI APIENTRY DLLMain(HINSTANCE hinstDll , DWORD reason , LPVOID flmpload) { switch(reason) { ……………// 初始化代码; } return true; }
|
参数 hinstDll 存放 DLL 的句柄,参数 reason 指明调用函数的原因, lpReserved 是一个被系统所保留的参数。对于隐式链接是一个非零值,对于显式链接值是零。
在 MFC 下建立 DLL 文件,会自动生成 def 文件框架,其它与建立传统的 Non-MFC DLL 没有什么区别,只要在相应的头文件写入关键字 _declspec(dllexport) 函数类型和函数名等,或在生成的 def 文件中 EXPORTS 下输入函数名就可以了。需要注意的是在向其它开发人员分发 MFC 扩展 DLL 时,不要忘记提供描述 DLL 中类的头文件以及相应的 .LIB 文件和 DLL 本身,此后开发人员就能充分利用你开发的扩展 DLL 了。
应用程序使用DLL可以采用两种方式:一种是隐式链接,另一种是显式链接。在使用DLL之前首先要知道DLL中函数的结构信息。Visual C++6.0在VCin目录下提供了一个名为Dumpbin.exe的小程序,用它可以查看DLL文件中的函数结构。另外,Windows系统将遵循下面的搜索顺序来定位DLL: 1.包含EXE文件的目录,2.进程的当前工作目录, 3.Windows系统目录, 4.Windows目录,5.列在Path环境变量中的一系列目录。
1.隐式链接
隐式链接就是在程序开始执行时就将DLL文件加载到应用程序当中。实现隐式链接很容易,只要将导入函数关键字_declspec(dllimport)函数名等写到应用程序相应的头文件中就可以了。下面的例子通过隐式链接调用MyDll.dll库中的Min函数。首先生成一个项目为TestDll,在DllTest.h、DllTest.cpp文件中分别输入如下代码:
|
//Dlltest.h #pragma comment(lib , "MyDll.lib") extern "C"_declspec(dllimport) int Max(int a,int b); extern "C"_declspec(dllimport) int Min(int a,int b); //TestDll.cpp #i nclude #i nclude"Dlltest.h" void main() {int a; a=min(8,10) printf(" 比较的结果为 %d " , a); }
|
在创建 DllTest.exe 文件之前,要先将 MyDll.dll 和 MyDll.lib 拷贝到当前工程所在的目录下面,也可以拷贝到 windows 的 System 目录下。如果 DLL 使用的是 def 文件,要删除 TestDll.h 文件中关键字 extern "C" 。 TestDll.h 文件中的关键字 Progam commit 是要 Visual C+ 的编译器在 link 时,链接到 MyDll.lib 文件,当然,开发人员也可以不使用 #pragma comment(lib , "MyDll.lib") 语句,而直接在工程的 Setting->Link 页的 Object/Moduls 栏填入 MyDll.lib 既可。
2 .显式链接
显式链接是应用程序在执行过程中随时可以加载 DLL 文件,也可以随时卸载 DLL 文件,这是隐式链接所无法作到的,所以显式链接具有更好的灵活性,对于解释性语言更为合适。不过实现显式链接要麻烦一些。在应用程序中用 LoadLibrary 或 MFC 提供的 AfxLoadLibrary 显式的将自己所做的动态链接库调进来,动态链接库的文件名即是上述两个函数的参数,此后再用 GetProcAddress() 获取想要引入的函数。自此,你就可以象使用如同在应用程序自定义的函数一样来调用此引入函数了。在应用程序退出之前,应该用 FreeLibrary 或 MFC 提供的 AfxFreeLibrary 释放动态链接库。下面是通过显式链接调用 DLL 中的 Max 函数的例子。
|
#i nclude #i nclude void main(void) { typedef int(*pMax)(int a,int b); typedef int(*pMin)(int a,int b); HINSTANCE hDLL; PMax Max HDLL=LoadLibrary("MyDll.dll");// 加载动态链接库 MyDll.dll 文件; Max=(pMax)GetProcAddress(hDLL,"Max"); A=Max(5,8); Printf(" 比较的结果为 %d " , a); FreeLibrary(hDLL);// 卸载 MyDll.dll 文件; }
|
在上例中使用类型定义关键字 typedef ,定义指向和 DLL 中相同的函数原型指针,然后通过 LoadLibray() 将 DLL 加载到当前的应用程序中并返回当前 DLL 文件的句柄,然后通过 GetProcAddress() 函数获取导入到应用程序中的函数指针,函数调用完毕后,使用 FreeLibrary() 卸载 DLL 文件。在编译程序之前,首先要将 DLL 文件拷贝到工程所在的目录或 Windows 系统目录下。
使用显式链接应用程序编译时不需要使用相应的 Lib 文件。另外,使用 GetProcAddress() 函数时,可以利用 MAKEINTRESOURCE() 函数直接使用 DLL 中函数出现的顺序号,如将 GetProcAddress(hDLL,"Min") 改为 GetProcAddress(hDLL, MAKEINTRESOURCE(2)) (函数 Min() 在 DLL 中的顺序号是 2 ),这样调用 DLL 中的函数速度很快,但是要记住函数的使用序号,否则会发生错误。
2007年4月6日
按着计划一步一步做的很好,累了就小憩一会儿,日子过的相当的惬意.但万万没有想到这样的日子已经在不知不觉中悄悄变少...... 今天上午正思考比赛的策略问题,突然手机上显示一个固定电话的号码,犹豫片刻还是接了起来.对方传来:"是XXX吗?公司决定你要提前到公司报道,明天去HX医院体检,后天直接到公司办理入职手续......"  .我当然是就9月1日报道的问题在电话里理论了一翻,但结果是"公司的项目太紧,公司人手不够......",一时间真的无语了. 感慨计划啊真的不如变化快哦....... 
2007年3月16日
离开公司也差不多二个星期了,玩也玩够了。。。。。。 昨天晚上不经意想了下,到9月1日(又要到新公司去了)前的这段时间该怎么过。由于最近都在MS 的.NET下做东西,以前学的那些知识都忘的差不多了,正好机会又来了,又可以利用这段闲散的时间来复习和加深一下几乎遗忘的知识了。 在公司是每周都要做计划和总结的,所以学习上也不能没有计划啊,在考虑了近几年的职业发展方向后做出了如下的一个复习方案。 一、紧急重要 1. 《P2P语音通信》必须在3月22日文档与代码全部结束。 2. 3月22日至5月11日全部精力投入到比赛中。 二、重要不紧急 1. 复习遗忘的知识 《TCP/IP详解》Ⅰ、Ⅱ 《算法艺术》 《设计模式》 《ACE架构》 《STL剖析》 三、紧急不重要 四、不紧急不重要 玩不尽的游戏了。。。。
2006年11月15日
|