Using Semaphores in Delphi, Part 2: The Connection Pool
Posted on 2023-01-23 11:29 wissly 阅读(359) 评论(0) 编辑 收藏 举报Abstract: Semaphores are used to coordinate multiple threads and processes. That semaphores provide multiple threads with simultaneous access to a shared resource is highlighted by the TFixedConnectionPool class described in this article.
Semaphores are like mutexes on steroids. Not only do they provide for blocking thread synchronization, but they permit two or more threads, up to some specified maximum number, to work concurrently with a shared resource. As a result, they are unlike most other synchronization objects, which typically provide access to one thread at a time. For an introduction to the creation and use of semaphores, read the preceding article in this series on the Borland Developer Network site by clicking here.
This unique feature of a semaphore, the ability to provide two or more threads with simultaneous access to a resource, lends itself very well to the implementation of objects that contain a fixed number of resources that need to be made available in a multithreaded environment. Two common examples of objects of this type are thread pools and database connection pools. This article discusses the use of a semaphore to implement a connection pool.
Connection Pool Overview
A connection pool is an object designed to provide thread-safe access to a limited number of database connections in a multithreaded environment. In most scenarios, a connection pool contains one or more active database connections that can be used by any thread needing to work with the database.
A connection pool provides two primary benefits. The first is related to performance. Making and dropping a connection to a database is a relatively time consuming process. Depending on the environment, if each thread must make and then drop a connection each time data access is needed, a significant amount of processing power will be spent on the connection process. By comparison, a connection pool includes a fixed number of connections that can be shared among many threads. When a thread needs to work with a database, it obtains a handle to an existing connection that it uses temporarily. This reuse can dramatically reduce the number of times a connection is made and dropped, thereby producing an overall increase in performance.
The second benefit is related to connection counts. Many database licensing schemes permit only a limited number of simultaneous connections to the database. A connection pool, therefore, becomes a convenient mechanism for ensuring that no more than some fixed maximum number of connections can be active at a given moment. While a semaphore can be used outside a connection pool to manage the maximum number of simultaneous connections, a connection pool automatically provides this feature with the addition benefit of increased performance.
The FixedConnectionPool
The fixed connection pool unit, available for download from Code Central (by clicking here), provides an implementation of a connection pool. The purpose of this code is to demonstrate the use of a semaphore to provide multithreaded access to a limited resource (the connections in the pool). If you want to use this code in a production application, it is your responsibility to certify that the connection pool meets your application's requirements. Note also, as of this writing, this connection pool has not been tested on a multiprocessor system.
There are four type declarations used in this connection pool. One is an interface and three are classes. The interface, named IConnection, defines the formal declaration of the connection pool interface, and also provides for the lifecycle management of individual connections.
The three classes consist of the connection pool object itself, named TFixedConnectionPool, as well as two helper classes. These helper classes are TConnectionModule, a data module descendant that is created once for each open connection, and TCleanupThread, a TThread descendant that drops connections that have not been used after some specified period of time.
IConnection
Let's begin by considering the IConnection interface. This interface declaration is shown here:
IConnection = Interface(IInterface) //CHANGE //To use a connection of another type, change the //return type of the Connection function function Connection: TSQLConnection; function GetRefCount: Integer; function GetLastAccess: TDateTime; property LastAccess: TDateTime read GetLastAccess; property RefCount: Integer read GetRefCount; end;
The Connection function returns a SQLConnection reference, which when implemented, refers to a SQLConnection object that is actively connected to a database. If you want to modify this connection pool to use a data access technology other than dbExpress, you need to change the return type of this Connection function to some other connection type, such as an TADOConnection (ADO), TIBDatabase (Interbase Express), or TSession (BDE).
The functions GetRefCount and GetLastAccess are the accessor methods for the RefCount and LastAccess properties, respectively. RefCount returns the number of current references to the IConnection implementing object. LastAccess returns the TDateTime of the last release of the IConnection implementing object. This value is used by the cleanup thread object to test whether the IConnection implementing object has gone unused for an extended period of time, and if so, to drop the unused connection.
TConnectionModule
The actual database connection is provided for by the TConnectionModule class. This class is a TDataModule descendant, and the connection component appears on this data module, as shown in the following figure.
TConnectionModule implements the IConnection interface. The SQLConnection that appears on this data module is the reference that is returned when the Connection method is invoked. As mentioned in the preceding discussion of the IConnection interface, if you want to use a connection other than dbExpress, you will replace this SQLConnection component with one that provides a connection using an alternative data access mechanism.
The following is the declaration of the TConnectionModule class. This class is commented extensively, and I am leaving these comments in this code segment for clarity.
//This data module provides the implementation //of the IConnection interface. To use a data access //mechanism other than dbExpress, modify the components //that appear on this data module, and change the class //of the Connection function in the IConnection interface //as well as in this class. TConnectionModule = class(TDataModule, IConnection) SQLConnection1: TSQLConnection; private { Private declarations } protected FRefCount: Integer; FLastAccess: TDateTime; //When the data module is created the //connection pool that creates the data module //will assign its critical section to this field. //The data module will use this critical section //to synchronize access to its reference count. CriticalSection: TCriticalSection; //This semaphore points to the FixedConnectionPool's //semaphore. It will be used to call ReleaseSemaphore //from the _Release method of the TDataModule. Semaphore: THandle; //These two static methods are reintroduced //in order to implement lifecycle management //for the interface of this object. //Normally, unlike normal COM objects, Delphi //TComponent descendants are not lifecycle managed //when used in interface references. function _AddRef: Integer; stdcall; function _Release: Integer; stdcall; {IConnection methods} function GetLastAccess: TDateTime; function GetRefCount: Integer; public { Public declarations } {IConnection method} //CHANGE //To use a connection of another type, change the //return type of the Connection function function Connection: TSQLConnection; end;
The Connection, GetLastAccess, and GetRefCount methods are implemented in this class to satisfy the implementation of the IConnection interface. The _AddRef and _Release static methods are reintroduced in this class to implement lifecycle management on the interface. This is necessary since the _AddRef and _Release methods introduced in TComponent (a TDataModule ancestor), do not implement lifecycle management through reference counting for VCL (visual component library) objects. This lifecycle management plays a critical role in the management of the connection pool.
The TConnectionModule class also contains two member fields, CriticalSection and Semaphore. Both of these members are used to point to a TCriticalSection and a semaphore (a THandle), both of which are maintained by the TFixedConnectionPool class (and are assigned to these members by the TFixedConnectionPool instance when it creates an instance of the connection module).
The critical section class is used to synchronize access to the internal reference count of the TConnectionPool instances, and the semaphore controls access to instances of the TConnectionModule class. This is shown in the _AddRef and _Release method implementations, shown here.
function TConnectionModule._AddRef: Integer; begin //increment the reference count CriticalSection.Enter; try Inc(FRefCount); Result := FRefCount; finally CriticalSection.Leave; end; end; function TConnectionModule._Release: Integer; begin //decrement the reference count CriticalSection.Enter; try Dec(FRefCount); Result := FRefCount; //if not more references, call Destroy if Result = 0 then Destroy else Self.FLastAccess := Now; finally CriticalSection.Leave; if FRefCount = 1 then ReleaseSemaphore(Semaphore, 1, nil); end; end;
Notice that the critical section is locked before the internal reference count can be incremented or decremented. Also, notice that if the reference count of a TConnectionModule instance drops to one that ReleaseSemaphore is called. When the internal reference count on a TConnectionModule instance drops to one, the thread that was using the connection has released it, and the only remaining reference to the connection module is held by the connection pool itself.
TFixedConnectionPool
The connection pool is implemented by the TFixedConnectionPool class, shown in the following code segment. Once again, the extensive comments have been left in the code for clarity.
//This is the class that manages the connection pool TFixedConnectionPool = class(TObject) private FPool: array of IConnection; FPoolSize: Integer; FTimeout: LargeInt; CleanupThread: TCleanupThread; //This semaphore is used to limit the number of //simultaneous connections. When the nth+1 connection //is requested, it will be blocked until a connection //becomes available. Semaphore: THandle; //This is the critical section that synchronizes //access to the connection module reference counts CriticalSection: TCriticalSection; public //This overloaded constructor takes two optional //parameters. These parameters specify the size //of the connection pool, as well as how long idle //connections in the connection pool will be kept. constructor Create(const PoolSize: Integer = 10; const CleanupDelayMinutes: Integer = 5; const Timeoutms: LargeInt = 10000); overload; destructor Destroy; override; //This function returns an object //that implements the IConnection interface. //This object can be a data module, as was //done in this example. function GetConnection: IConnection; end;
One of the more notable elements of this declaration is the FPool member, which is a dynamic array of IConnection references. This array provides the list of pointers to the connections in the connection pool.
The connection pool is configured in the overloaded Create constructor (Create is static in TObject, the TConnectionPool ancestor, which is why it is not overridden). The constructor is shown here.
constructor TFixedConnectionPool.Create(const PoolSize: Integer = 10; const CleanupDelayMinutes: Integer = 5; const Timeoutms: LargeInt = 10000); begin FPoolSize := PoolSize; FTimeout := Timeoutms; Semaphore := CreateSemaphore(nil, PoolSize, PoolSize, ''); CriticalSection := TCriticalSection.Create; //Set the length of the connection pool SetLength(FPool, PoolSize); //Create and start the cleanup thread CleanupThread := TCleanupThread.Create(True, CleanupDelayMinutes); with CleanupThread do begin FreeOnTerminate := True; Priority := tpLower; FixedConnectionPool := Self; Resume; end; end;
As you can see from this code, the constructor stores the cleanup delay and timeout values in member fields of the connection pool object. It then creates the semaphore, setting the number of available locks and the maximum number of locks to the size of the pool. As a result, initially the connection pool is created with all connections available. Also, the critical section, which as described earlier is used to synchronize access to TConnectionModule instance reference counts, is created.
Next, the length of the dynamic array of IConnection references, FPool, is set to the number of available connections. This dynamic array is used to hold internal references to active connections (TConnectionModule instances), thereby preventing them from being released (until the TConnectionPool class is destroyed, or the cleanup thread determines that one or more of the connections have gone unused for an extended period of time).
Finally, an instance of the cleanup thread is created. This class is discussed in detail later in this article.
The most important method of the TConnectionPool class is the GetConnection method. This method returns an IConnection (implemented by TConnectionModule), so long as one is available (as controlled by the semaphore). The implementation of this method is shown here:
function TFixedConnectionPool.GetConnection: IConnection; var i: Integer; DM: TConnectionModule; WaitResult: Integer; begin Result := nil; WaitResult := WaitForSingleObject(Semaphore, FTimeout); if WaitResult <> WAIT_OBJECT_0 then raise EConnPoolException.Create('Connection pool timeout. '+ 'Cannot obtain a connection'); CriticalSection.Enter; try for i := Low(FPool) to High(FPool) do begin //If FPool[i] = nil, the IConnection has //not yet been created. Create it, initialize //it, and return it. If FPool[i] <> nil, then //check to see if its RefCount = 1 (only the pool //is referencing the object). if FPool[i] = nil then begin DM := TConnectionModule.Create(nil); DM.CriticalSection := Self.CriticalSection; DM.Semaphore := Self.Semaphore; FPool[i] := DM; FPool[i].Connection.Connected := True; Result := FPool[i]; Exit; end; //if FPool[i].FRefCount = 1 then //the connection is available. Return it. if FPool[i].RefCount = 1 then begin Result := FPool[i]; Exit; end; end; //for finally CriticalSection.Leave; end; end;
As you can see from this code, when GetConnection is invoked, WaitForSingleObject is called using the connection pool's semaphore. If a connection is available, WaitForSingleObject returns immediately, and the available count on the semaphore is reduced by one. If a connection is not currently available, the thread on which GetConnection is invoked is blocked until either a connection becomes available, or the connection timeout expires. If the connection timeout expires, an exception is raised.
When WaitForSingleObject returns Wait_Object_0, the thread invoking GetConnection is being granted access to the connection pool. This means that either there is one or more connections that have not yet been connected, or a previously connected connection is now available (its reference count is one).
This code then enters the critical section (since it might read the RefCount property of the connection module). It then iterates through the FPool dynamic array. It tests each element of this dynamic array for a value of nil, which signals that this connection has not yet been connected. If the element of FPool is nil, an instance of TConnectionModule is created and assigned to this array element (which increments its reference count to one). It is during this process that the critical section and semaphore reference of the connection pool object is passed to the connection module. Finally, the IConnection implementing object is returned (which increments its reference count to two).
Each time a given element of the dynamic array is found to be not nil, the loop next tests to see if the reference count of the current element is one. If it is one, that means the connection module was previously created and connected, but is not currently in use. In that case, that connection module is returned (which increments its reference count to two).
Once an available connection has returned, the critical section is exited.
Let's consider the semaphore for a moment. When GetConnection is called, one of the available locks on the semaphore is taken when a connection can be obtained. This lock on the semaphore will be released back to the semaphore when the connection is no longer used by an external object (one outside the connection pool). This is signaled when the reference count decrements to one. This code was shown earlier in the TConnectionModule's _Release method.
TCleanupThread
Just about all relevant code concerning this connection pool has been discussed, with the exception of the TCleanupThread class. This class implements a low priority thread that periodically wakes up and inspects the LastAccess property of all instances of the TConnectionModule class. While this operation is not essential for a connection pool, it permits a connection pool to limit the resources it uses on a database server by releasing connections when they are no longer being used.
The following is the declaration of the TClearnupThread class:
//This thread class is used by the connection pool //object to cleanup idle connections after a //configurable period of time. TCleanupThread = class(TThread) private FCleanupDelay: Integer; protected //When the thread is created, this critical section //field will be assigned the connection pool's //critical section. This critical section is //used to synchronize access to data module //reference counts. CriticalSection: TCriticalSection; FixedConnectionPool: TFixedConnectionPool; procedure Execute; override; constructor Create(CreateSuspended: Boolean; const CleanupDelayMinutes: Integer); end;
As you can see, the TCleanupThread class contains TCriticalSection and TFixedConnectionPool member fields. As you saw in the TFixedConnetionPool constructor shown earlier in this article, these members are assigned instances of the fixed connection pool's critical section and a reference to the connection pool itself, respectively.
The following code shows the initialization of the TCleanupThread class in its constructor:
constructor TCleanupThread.Create(CreateSuspended: Boolean; const CleanupDelayMinutes: Integer); begin // always create suspended inherited Create(True); // always create suspended FCleanupDelay := CleanupDelayMinutes; //Resume if not created suspended if not CreateSuspended then Resume; end;
As you can see from this overloaded constructor, it begins by creating an instance of the thread as a suspended thread. It then stores the value of the ClearnupDelayMinutes formal parameter in a member field. Then, if the thread was created with a CreateSuspended formal parameter value of False, it resumes the thread.
The code in a thread is executed in that thread's Execute method. The following is the Execute method of this thread:
procedure TCleanupThread.Execute; var i: Integer; begin while True do begin if Terminated then Exit; //sleep for delay sleep(FCleanupDelay * 1000 * 60); if Terminated then Exit; FixedConnectionPool.CriticalSection.Enter; try for i := low(FixedConnectionPool.FPool) to High(FixedConnectionPool.FPool) do //if the connection exists, has no external reference, //and has not been used lately, release it if (FixedConnectionPool.FPool[i] <> nil) and (FixedConnectionPool.FPool[i].RefCount = 1) and (MinutesBetween(FixedConnectionPool.FPool[i].LastAccess, Now) > FCleanupDelay) then FixedConnectionPool.FPool[i] := nil; finally FixedConnectionPool.CriticalSection.Leave; end;//try end;//while end;
When the thread executes, and after it verifies that it has not been terminate, it calls sleep for some number of minutes defined by the FCleanupDelay value. After the cleanup delay expires, the thread once again checks to see if it has been terminated, after which is enters the critical section and iterates through the array of IConnection references. For each connection that is not nil and whose reference count is not one, it checks to see if the connection has gone unused for greater than the cleanup delay interval. When an unused connection is found, the corresponding value of the IConnection dynamic array is set to nil, which causes the associated TConnectionModule object to be destroyed (through the interface reference count mechanism). This has the effect of closing the connection.
The TFixedConnectionPool Destructor
A number of resources are used by the TFixedConnectionPool class, including the semaphore and critical section. These need to be cleaned up when the fixed connection pool is destroyed. These tasks are performed by the overridden Destroy destructor of the TFixedConnectionPool class, as shown in the following implementation:
destructor TFixedConnectionPool.Destroy; var i: Integer; begin //Free any remaining connections CleanupThread.Terminate; CriticalSection.Enter; try for i := Low(FPool) to High(FPool) do FPool[i] := nil; SetLength(FPool,0); finally CriticalSection.Leave; end; CriticalSection.Free; //Release the semaphore CloseHandle(Semaphore); inherited; end;
Here you see that the destructor begins by terminating the cleanup thread. Next, it enters the critical section and iterates through the connections in the connection pool, setting each to nil (thereby destroying the corresponding TConnectionModule instance). The IConnection dynamic array is then set to a size of zero. Finally, the critical section is exited, the critical section is freed, after which the handle of the semaphore closed.
Using the Fixed Connection Pool
Before you can use the TFixedConnectionPool class, you must invoke its constructor. This is shown in the following pseudo code:
var ConnPool: TFixedConnectionPool; begin ConnPool := TFixedConnectionPool.Create(10, 5, 20000);
Here the connection pool is created with a maximum of 10 connections. Unused connections may be cleaned up after five minutes, and an exception will be raised if a connection cannot be obtained within 20 seconds.
The GetConnection method of the connection pool is used to return a connection module, which you must assign to an IConnection interface reference. You then use the returned interfaced object to obtain the actual connection by reading its connection property. This is shown in the following pseudo code:
var SQLDataSet: TSQLDataSet; Conn: IConnection; begin SQLDataSet := TSQLDataSet.Create(nil); try SQLDataSet.CommandText := 'select * from employee'; Conn := ConnPool.GetConnection; SQLDataSet.SQLConnection := Conn.Connection; SQLDataSet.Open; //do something with the result finally SQLDataSet.Free; end; //The connection is released when Conn goes out of scope end;
This article was adapted in part from Mastering Multithreading and Other Advanced Delphi Topics by Cary Jensen, one of the Delphi Developer Days 2003 Power Workshops, focused Delphi (TM) training. For information on this an other Delphi Developer Days 2003 Power Workshops, visit http://www.DelphiDeveloperDays.com.
About the Author
Cary Jensen is President of Jensen Data Systems, Inc., a Texas-based training and consulting company that won the 2002 Delphi Informant Magazine Readers Choice award for Best Training. He is the author and presenter for Delphi Developer Days (www.DelphiDeveloperDays.com), an information-packed Delphi (TM) seminar series that tours North America and Europe, and Delphi Developer Days Power Workshops, focused Delphi (TM) training. Cary is also an award-winning, best-selling co-author of eighteen books, including Building Kylix Applications (2001, Osborne/McGraw-Hill), Oracle JDeveloper (1999, Oracle Press), JBuilder Essentials (1998, Osborne/McGraw-Hill), and Delphi In Depth (1996, Osborne/McGraw-Hill). For information about onsite training and consulting you can contact Cary at cjensen@jensendatasystems.com, or visit his Web site at www.JensenDataSystems.com.
Click here for the current schedule of Delphi Developer Days Power Workshops, focused Delphi (TM) training.
New!: Stay informed, stay in touch. Register online to receive the free Developer Days ELetter: information, observations, and events for the Delphi developer by Cary Jensen. Each Developer Days ELetter includes Delphi tips and tricks, .NET information, links to recent articles posted to the Borland Developers Network site, and events in the Delphi community. Click here to sign up now.