邮件客户端中的多线程支持和连接池
邮件客户端,例如 ImapClient, Pop3Client,以及 SmtpClient 可在多线程环境中使用。客户端可以与服务器保持一个或多个连接。要管理客户端内部的连接集合,使用 连接池。同时可创建和使用的连接数受 CredentialsByHostClient.MaxConnectionsPerServer 属性。此属性可设置为 1 或更大的值。默认情况下为 10。
为每个连接实现了命令队列,以支持多线程操作。命令实现了协议中定义的最简单操作,例如 Noop, Authenticate,等等。用户可以启动的命令数量超过可用连接数,但这些命令只有在客户端能够为该操作创建连接时才会执行。
邮件客户端在多线程环境中的行为
邮件客户端具有以下行为:
-
当
MaxConnectionsPerServer = 1,客户端创建一个连接并执行身份验证和授权。该连接在客户端被释放前保持工作状态。来自不同线程的所有操作都被放入主连接中的命令队列。 -
当
MaxConnectionsPerServer > 1,客户端会创建所需数量的连接并为每个连接执行身份验证和授权。一个连接被保留为主连接。该连接在客户端被释放前保持工作状态。所有其他连接按需创建和释放。此类连接的最大数量由MaxConnectionsPerServer属性。例如,如果MaxConnectionsPerServer = 2,则一个连接被保留为主连接,第二个连接作为额外连接用于在其他线程中执行的操作。因此,如果MaxConnectionsPerServer = 3,则第一个连接被保留为主连接,其余两个连接作为额外连接用于在其他线程中执行的操作。当来自新线程的连接请求出现且所有连接已被占用时,客户端会等待,直到已使用的连接数量下降。这一点非常重要,阐明了正确释放连接为何如此关键。
在多线程环境中使用邮件客户端的示例
用户可以通过多种方式在不同线程中执行操作。它们可分为两类。
使用异步(Begin/End)方法
用户使用客户端中定义的异步(Begin/End)方法。在这种情况下,邮件客户端在需要时会启动新线程。客户端实现了任务队列(不要将其与连接中的命令队列混淆)。当有可用连接时,任务可能被执行。一旦已使用的连接数少于限制值,客户端会创建新连接,为当前任务创建线程并执行该任务。异步操作的示例:
// Create an imapclient with host, user and password
ImapClient client = new ImapClient();
client.Host = "domain.com";
client.Username = "username";
client.Password = "password";
client.SelectFolder("InBox");
ImapMessageInfoCollection messages = client.ListMessages();
IAsyncResult res1 = client.BeginFetchMessage(messages[0].UniqueId);
IAsyncResult res2 = client.BeginFetchMessage(messages[1].UniqueId);
MailMessage msg1 = client.EndFetchMessage(res1);
MailMessage msg2 = client.EndFetchMessage(res2);
使用用户创建的线程
用户可以使用诸如 Thread, ThreadPool, Task,或任何其他为此目的设计的对象。用户也可以使用第三方代码创建的线程。在这种情况下,客户端有两种行为模型。
a. 如果用户未为线程中的操作创建额外连接,所有该线程的操作将发送到主连接的命令队列。以下是未创建新连接的额外线程中操作的示例——所有事务均通过主连接进行:
List<MailMessage> List = new List<MailMessage>();
ThreadPool.QueueUserWorkItem(delegate(object o)
{
client.SelectFolder("folderName");
ImapMessageInfoCollection messageInfoCol = client.ListMessages();
foreach (ImapMessageInfo messageInfo in messageInfoCol)
{
List.Add(client.FetchMessage(messageInfo.UniqueId));
}
});
b. 当用户运行方法为额外线程创建新连接时,线程会被阻塞,直到新连接配额值变更允许创建新连接。然后创建新连接,并将其设置为该线程中所有操作的默认连接。线程中所有操作完成后,必须释放该连接。要创建新连接,请使用 CredentialsByHostClient.CreateConnection 方法。此方法返回实现了 IDisposable 接口。要释放连接,使用 Dispose 方法必须被调用。创建和释放连接必须在执行邮件操作的线程内部进行。尝试在创建邮件客户端的线程中创建新连接会导致错误,因为该线程此时不能用于创建新连接。如果 MaxConnectionsPerServer = 1。在额外线程中创建新连接的代码示例:
List<MailMessage> List1 = new List<MailMessage>();
ThreadPool.QueueUserWorkItem(delegate(object o)
{
using (IDisposable connection = client.CreateConnection())
{
client.SelectFolder("FolderName");
ImapMessageInfoCollection messageInfoCol = client.ListMessages();
foreach (ImapMessageInfo messageInfo in messageInfoCol)
List1.Add(client.FetchMessage(messageInfo.UniqueId));
}
});
连接池
从 Aspose.Email 19.3 开始,连接池已重构。 EmailClient 类已引入,最终取代了 CredentialsByHostClient 类。该 EmailClient 类提供了一个 ConnectionAsgmtMode 属性,用于定义多线程环境下的连接分配模式。 EmailClient.ConnectionAsgmtMode 使用以下方式设置 ConnectionAsgmtType 枚举。
连接类型
有三种连接类型:
- 主连接。 该连接随邮件客户端一起创建和释放,不能手动创建或释放。
- 默认连接。 用户可以使用 CreateConnection 方法手动创建。如果存在默认连接,线程中执行的所有邮件客户端方法都会隐式使用该连接。每个线程只能存在一个默认连接。它可以手动或自动创建,取决于
EmailClient.ConnectionAsgmtMode属性。可以使用EmailClient.CreateConnection(createAsDefaultConnection = true)方法手动创建。如果未使用默认连接(取决于连接分配模式),则会隐式使用主连接。 - 独立连接。 这些连接不与线程关联。它们需要手动创建,并必须显式作为方法参数使用。这些连接可以使用
EmailClient.CreateConnection()方法或EmailClient.CreateConnection(createAsDefaultConnection = false)方法。
连接分配类型
设置 EmailClient.ConnectionAsgmtMode 属性, ConnectionAsgmtType 枚举。它提供的分配类型列在下方。
-
ConnectionAsgmtType.UseMainOrDefault 该模式是邮件客户端的默认模式。如果未创建默认连接,或未显式将连接作为方法参数传递,邮件客户端将在多个线程的所有操作中使用主连接。主连接与邮件客户端同时创建。用户可以使用
CreateConnection方法。如果为线程创建了默认连接,则在该线程中调用的所有邮件客户端方法都会隐式使用该连接。如果未为线程创建默认连接,则在该线程中调用的所有方法使用主连接。用户也可以使用CreateConnection方法。要使用其他连接(非主连接且非默认连接),用户必须将连接显式作为方法参数传递。用户还可以创建任意数量的连接。每个线程只能存在一个默认连接。请注意,如果用户使用Thread用于多任务编程的对象。如果用户使用连接池或Task用于多任务的对象,在此模式下可能导致行为不正确。为避免此问题,用户必须在代码执行结束时手动释放默认连接(如果使用了)。 -
ConnectionAsgmtType.UseMain 邮件客户端在多个线程的所有操作中使用主连接。主连接与邮件客户端同时创建。用户无法创建默认连接,但可以使用
CreateConnection方法为线程创建默认连接。要使用其他连接,用户必须将其显式作为方法参数传递。 -
ConnectionAsgmtType.UseDefault 邮件客户端在多个线程的所有操作中仅隐式使用默认连接。此模式下不使用主连接。如果线程尚未创建默认连接(在首次调用邮件客户端方法时),邮件客户端会在首次操作执行前为该线程隐式创建默认连接。用户无法使用
CreateConnection方法,因为它们会自动创建。用户还可以使用CreateConnection方法。要使用其他连接,用户必须将其显式作为方法参数传递。用户还可以创建任意数量的连接。每个线程只能使用一个默认连接。请注意,如果用户使用Thread用于多任务编程的对象。如果用户使用连接池或Task用于多任务的对象,在此模式下可能导致行为不正确。为避免此问题,用户必须在代码执行结束时手动释放默认连接。
建议
如果用户将所有命令发送到主连接,可能会出现来自不同线程的命令混合的情况。用户应了解哪些命令依赖于执行顺序,并采取措施同步此类命令。还需考虑在不同会话(IMAP/POP3)中执行命令的可能性。最耗时的操作是 FetchMessage, AppendMessage,以及 Send。这些操作可能更适合在新线程和新连接上执行。快速操作,例如 Delete 在主连接上执行这些操作是有意义的。请注意,初始化新连接是相当耗时的操作。