邮件客户端中的多线程支持
类似以下的邮件客户端 ImapClient 和 Pop3Client 允许用户在多线程环境中使用它们。邮件客户端支持与服务器建立一个或多个连接。客户端内部使用连接池来管理这些连接。可同时创建和使用的连接数量受属性 CredentialsByHostClient.MaxConnectionsPerServer 限制。该属性可以设置为 1 或更大,默认值为 10。为了支持多线程操作,已在连接上实现了命令队列。命令实现了协议中定义的最简单操作,如 Noop、Authenticate 等。用户可以发起的命令数量可能超过可用连接数,但只有在客户端能够为该操作创建连接时才会执行。
在多线程环境中使用邮件客户端的方法
邮件客户端具有以下行为:
1. 当 MaxConnectionsPerServer = 1 时,客户端仅创建 1 个连接并执行身份验证和授权。该连接在客户端被释放之前保持工作状态。来自不同线程的所有操作都会被指向放在主连接中的单一命令队列。
2. 当 MaxConnectionsPerServer > 1 时,客户端会创建所需数量的连接,并对每个连接执行身份验证和授权。保留一个连接作为主连接。该连接在客户端被释放之前一直保持工作状态。其他连接根据需求创建和释放。此类连接的最大数量由属性 MaxConnectionsPerServer 定义,例如,如果 MaxConnectionsPerServer = 2,则保留一个作为主连接,第二个连接作为额外连接用于在其他线程中执行的操作。同理,如果 MaxConnectionsPerServer = 3,则第一个连接为主连接,另外两个连接作为额外连接用于在其他线程中执行的操作。如果来自新线程的下一次连接请求到来,而所有连接均已被占用,客户端将等待,直到已使用的连接数量减少。此关键点阐明了正确释放连接为何如此重要。
示例
用户可以通过多种方式在不同线程中执行操作。它们可以分为两类:
1. 用户使用客户端定义的异步(Begin/End)方法。在这种情况下,邮件客户端在需要时会启动新线程。客户端实现了任务队列(请勿与连接中的命令队列混淆)。如果有可用连接,任务即可执行。当已使用的连接数量少于限制值时,客户端会创建新连接,为当前任务创建线程并执行该任务。以下是使用异步操作的示例:
// Create an imapclient with host, user and password
ImapClient client = new ImapClient();
client.setHost("domain.com");
client.setUsername("username");
client.setPassword("password");
client.selectFolder("InBox");
ImapMessageInfoCollection messages = client.listMessages();
IAsyncResult res1 = client.beginFetchMessage(messages.get_Item(0).getUniqueId());
IAsyncResult res2 = client.beginFetchMessage(messages.get_Item(1).getUniqueId());
MailMessage msg1 = client.endFetchMessage(res1);
MailMessage msg2 = client.endFetchMessage(res2);
2. 用户可以使用 Thread、ExecutorService 或其他任何用于此目的的对象来创建线程。也可以使用第三方代码创建的线程。在此情况下,客户端具有两种行为模型
a. 如果用户没有为该线程的操作创建额外的连接,所有该线程的操作将被发送到主连接的命令队列。以下是未创建新连接的额外线程中操作的示例,所有事务均通过主连接进行:
/**
* Creates an executor service with a fixed pool size, that will time
* out after a certain period of inactivity.
*
* @param poolSize The core- and maximum pool size
* @param keepAliveTime The keep alive time
* @param timeUnit The time unit
* @return The executor service
*/
private static ExecutorService createFixedTimeoutExecutorService(
int poolSize, long keepAliveTime, TimeUnit timeUnit)
{
ThreadPoolExecutor e =
new ThreadPoolExecutor(poolSize, poolSize,
keepAliveTime, timeUnit, new LinkedBlockingQueue<Runnable>());
e.allowCoreThreadTimeOut(true);
return e;
}
final List<MailMessage> list = new ArrayList<MailMessage>();
ExecutorService executor = createFixedTimeoutExecutorService(1, 1000, TimeUnit.MILLISECONDS);
executor.execute(new Runnable() {
public void run() {
client.selectFolder("folderName");
ImapMessageInfoCollection messageInfoCol = client.listMessages();
for (ImapMessageInfo messageInfo : messageInfoCol) {
list.add(client.fetchMessage(messageInfo.getUniqueId()));
}
}
});
b. 当用户调用方法为额外线程创建新连接时,该线程会被阻塞,直到新连接的配额值被更改以允许创建新连接。随后创建新连接,并将其设为该线程中所有操作的默认连接。该线程中的所有操作完成后,必须释放该连接。要创建新连接,请使用 CredentialsByHostClient.CreateConnection 方法。此方法返回实现 IDisposable 接口的对象。要释放连接,必须调用 Dispose 方法。连接的创建和释放必须在执行邮件操作的线程内部进行。在创建邮件客户端的线程中尝试创建新连接会导致错误,因为此时该线程不能用于创建新连接。当 MaxConnectionsPerServer = 1 时也无法创建新连接。以下是创建新连接的额外线程的代码示例:
final List<MailMessage> list = new ArrayList<MailMessage>();
ExecutorService executor = createFixedTimeoutExecutorService(1, 1000, TimeUnit.MILLISECONDS);
executor.execute(new Runnable() {
public void run() {
IConnection connection = client.createConnection();
try {
client.selectFolder(connection, "FolderName");
ImapMessageInfoCollection messageInfoCol = client.listMessages(connection);
for (ImapMessageInfo messageInfo : messageInfoCol)
list.add(client.fetchMessage(connection, messageInfo.getUniqueId()));
} finally {
connection.dispose();
}
}
});
建议
值得注意的是,如果用户将所有命令发送到主连接,可能会出现来自不同线程的命令混在一起的情况。用户应了解哪些命令依赖于其执行顺序,并采取措施同步这些命令。还需考虑在不同会话(IMAP/POP3)中执行命令的可能性。最耗时的操作,如 FetchMessage、AppendMessage 和 Send,可能需要在新线程和新连接中执行。但像 Delete 这样快速的操作则应使用主连接。请注意,初始化新连接本身已经是一个耗时操作。