دعم تعدد الخيوط في عملاء البريد
عملاء البريد مثل ImapClient و Pop3Client تسمح للمستخدمين باستخدامها في بيئة تعدد الخيوط. يسمح عملاء البريد بوجود اتصال واحد أو أكثر مع الخادم. لإدارة مجموعة الاتصالات داخل العملاء يتم استخدام مجمع الاتصالات (connection pool). عدد الاتصالات التي يمكن إنشاؤها واستخدامها في وقت واحد يحدّه الخاصية CredentialsByHostClient.MaxConnectionsPerServer. يمكن ضبط هذه الخاصية إلى 1 أو قيمة أكبر. افتراضيًا تكون هذه الخاصية مساوية لـ 10. تم تنفيذ طابور الأوامر للاتصال لدعم عمليات تعدد الخيوط. تنفّذ الأوامر أبسط العمليات المعرفة في البروتوكول، مثل Noop و Authenticate وغيرها. قد يبدأ المستخدم تنفيذ عدد أكبر من الأوامر مقارنة بعدد الاتصالات المتاحة. لكن يتم تنفيذها فقط عندما يتمكن العميل من إنشاء اتصال للعملية.
طرق استخدام عملاء البريد في بيئة تعدد الخيوط
عملاء البريد الإلكتروني يتبعون السلوك التالي:
1. في حالة كون MaxConnectionsPerServer = 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 أو أي كائنات أخرى مخصصة لهذا الغرض. كما يمكن للمستخدم استخدام خيوط تم إنشاؤها في شفرة طرف ثالث. خلال ذلك يمتلك العميل نموذجين سلوكيين
أ. إذا لم يقم المستخدم بإنشاء اتصالات إضافية للعمليات في الخيط، فسيتم إرسال جميع عمليات هذا الخيط إلى طابور الأوامر عبر الاتصال الرئيسي. مثال على عمليات في خيط إضافي بدون إنشاء اتصال جديد، جميع المعاملات تتم عبر الاتصال الرئيسي:
/**
* 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()));
}
}
});
ب. عندما يقوم المستخدم بتشغيل طريقة لإنشاء اتصال جديد لخيط إضافي، يُحجب هذا الخيط حتى يتم تعديل قيمة الحصة الخاصة بالاتصالات الجديدة للسماح بالاتصال الجديد. ثم يُنشأ الاتصال الجديد. يُعيّن هذا الاتصال كاتصال افتراضي لجميع العمليات في هذا الخيط. بعد إتمام جميع العمليات في هذا الخيط يجب التخلص من الاتصال. لإنشاء اتصالات جديدة استخدم طريقة 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 فمن الأفضل تنفيذها عبر الاتصال الرئيسي. يرجى ملاحظة أن إنشاء اتصال جديد يُعتبر عملية تستغرق وقتًا كبيرًا.