Tigase组件第三节 – 多线程
本文翻译自 – http://www.tigase.org/content/component-implementation-lesson-3-multi-threading
拥有多个CPU或多核CPU的电脑已经非常普及了。你也许会把像XMPP服务这样的应用部署到多核或多CPU的电脑上。但是现在你的新组件还只能在单一线程里面处理所有的packet。
如果packet的处理是复杂的运算(比如垃圾信息过滤),那么后果可能很严重:一个CPU负载为100%,而另外的几颗CPU却还在闲置。你也许非常希望所有CPU或核心都能够分摊负载,共同参与到工作当中。
Tigase API提供一种非常方便的方式让组件的processPacket(Packet packet)在多个线程中运行。int processingThreads()方法返回组件被分配到几个线程当中。缺省情况下,返回值为“1”,这是因为并不是所有的组件都能够允许多线程并发处理packet。你可以通过覆写(overwrite)processThreads方法来指定processPacket可以在几个线程里面并发执行。
如果packet的处理只是简单的CPU运算,那么你也许会想要尽可能多的线程来压榨CPU的潜力:
1
2
3
4
|
@Override
publicintprocessingThreads() {
returnRuntime.getRuntime().availableProcessors();
}
|
如果处理中需要使用IO(网络或者是数据库),那么在添加线程数的时候可能需要考虑更多。很难准确的定义到底需要多少个线程,只能通过一些测试来获取经验值。
现在新开发的组件已经有足够多的线程了。但还有个小问题,在多数情况下多个packet的处理顺序是不能颠倒的。如果组件的processPacket(…)方法由多个线程并发处理,很有可能会出现一个后发送的消息取代了先前发送的消息首先到达目的地,特别是在第一个packet很大而第二个很小的时候。我们可以通过调整“负责packet在线程中做分发的那个方法”来避免这种情况发生。
包的分发算法很简单:
1
|
intthread_idx = hashCodeForPacket(packet) % threads_total;
|
所以调整的关键在于hashCodeForPacket(…)方法。通过覆写这个方法可以保证所有发往同一个用户的packet都是由同一个线程来进行处理的:
1
2
3
4
5
6
7
8
9
10
11
12
|
@Override
publicinthashCodeForPacket(Packet packet) {
if(packet.getElemTo() !=null) {
returnpacket.getElemTo().hashCode();
}
// 程序不应该运行到这里,所有的packet都必须具有一个目的地地址,但是也许垃圾过滤器也许会过滤一些奇怪的地址
if(packet.getElemFrom() !=null) {
returnpacket.getElemFrom().hashCode();
}
// 如果程序真的运行到这一部,就应该好好检查一下到达组件的packet是否正常,然后找到一个更好的计算hashCode方法。
return1;
}
|
刚刚提到的两个方法一个可以控制组件处理packet的线程数,另一个可以控制packet分发到线程的逻辑。它们还不是Tigase API在多线程处理时提供的全部方法。
有时还会用到周期性任务。你当然可以创建一个Timer实例,并使用TimerTasks来加载它;但当很多类似的工作都需要在各个组件当中执行的时候,你会需要更多额外的资源来支撑这么多个TimerTask。Tigase提供了一些允许你重用Timer对象来执行各种任务的方法。
首先,你有三个可以执行周期性任务的方法:
1
2
3
|
publicsynchronizedvoideverySecond();
publicsynchronizedvoideveryMinute();
publicsynchronizedvoideveryHour();
|
一个“周期性向某些特定地址发送通知”的功能可以这样来实现:
1
2
3
4
5
6
7
8
9
10
11
|
@Override
publicsynchronizedvoideveryMinute() {
super.everyMinute();
if((++delayCounter) >= notificationFrequency) {
addOutPacket(Packet.getMessage(abuseAddress, getComponentId(),
StanzaType.chat,"Detected spam messages: "+ spamCounter,
"Spam counter",null, newPacketId("spam-")));
delayCounter =0;
spamCounter =0;
}
}
|
这个方法每分钟(也就是notificationFrequency)向“abuseAddress”发送一个通知,内容为上个周期垃圾信息过滤器拦截了多少条消息。需要注意的是:你一定需要调用super.everyMinute()方法,确保父类的语句也能够被执行,同时也要让自己添加的语句尽可能高效率快速得完成,尤其是在复写的是everySecond()方法的时候。
还有两个可以安排在特定的时间执行任务的方法,它们非常类似于java.util.Timer API,区别在于Timer可以在类/父类/爷爷类等各个层级被重用。每一个类实例都有一个独立的Timer,这样能够避免组件间的干扰:
1
2
|
addTimerTask(TimerTask task,longdelay, TimeUnit unit);
addTimerTask(TimerTask task,longdelay);
|
还有一个和多线程没有直接联系,但是非常有用,可以在某一个特定时间点执行任务的方法。这个时间点就是在服务器完成初始化的时候,这个时候所有的组件都已经被创建,并接收到它们各自的配置项。这时Tigase服务器会调用每一个已加载组件的void initializationComplete()方法,你可以通过覆写这个方法来执行一些任务。
下面的代码使用到了上面提到的所有API:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
|
importjava.util.Arrays;
importjava.util.Map;
importjava.util.logging.Logger;
importtigase.server.AbstractMessageReceiver;
importtigase.server.Packet;
importtigase.util.JIDUtils;
importtigase.xmpp.StanzaType;
publicclassTestComponentextendsAbstractMessageReceiver {
privatestaticfinalLogger log =
Logger.getLogger(TestComponent.class.getName());
privatestaticfinalString BAD_WORDS_KEY ="bad-words";
privatestaticfinalString WHITELIST_KEY ="white-list";
privatestaticfinalString PREPEND_TEXT_KEY ="log-prepend";
privatestaticfinalString SECURE_LOGGING_KEY ="secure-logging";
privatestaticfinalString ABUSE_ADDRESS_KEY ="abuse-address";
privatestaticfinalString NOTIFICATION_FREQ_KEY ="notification-freq";
privateString[] badWords = {"word1","word2","word3"};
privateString[] whiteList = {"admin@localhost"};
privateString prependText ="Spam detected: ";
privateString abuseAddress ="abuse@locahost";
privateintnotificationFrequency =10;
privateintdelayCounter =0;
privatebooleansecureLogging =false;
privatelongspamCounter =0;
@Override
publicvoidprocessPacket(Packet packet) {
// 这是一个message packet吗?
if("message"== packet.getElemName()) {
String from = JIDUtils.getNodeID(packet.getElemFrom());
// 消息的发送者在白名单内吗?
if(Arrays.binarySearch(whiteList, from) <0) {
// 如果ta不在白名单里面,那么检查消息的内容
String body = packet.getElemCData("/message/body");
if(body !=null&& !body.isEmpty()) {
body = body.toLowerCase();
for(String word : badWords) {
if(body.contains(word)) {
log.finest(prependText + packet.toString(secureLogging));
++spamCounter;
return;
}
}
}
}
}
// 不是垃圾信息,返回以便做下一步处理
Packet result = packet.swapFromTo();
addOutPacket(result);
}
@Override
publicintprocessingThreads() {
returnRuntime.getRuntime().availableProcessors();
}
@Override
publicinthashCodeForPacket(Packet packet) {
if(packet.getElemTo() !=null) {
returnpacket.getElemTo().hashCode();
}
// 程序不应该运行到这里,所有的packet都必须具有一个目的地地址,但是也许垃圾过滤器也许会过滤一些奇怪的地址
if(packet.getElemFrom() !=null) {
returnpacket.getElemFrom().hashCode();
}
// 如果程序真的运行到这一部,就应该好好检查一下到达组件的packet是否正常,然后找到一个更好的计算hashCode方法。
return1;
}
@Override
publicMap<String, Object> getDefaults(Map<String, Object> params) {
Map<String, Object> defs =super.getDefaults(params);
defs.put(BAD_WORDS_KEY, badWords);
defs.put(WHITELIST_KEY, whiteList);
defs.put(PREPEND_TEXT_KEY, prependText);
defs.put(SECURE_LOGGING_KEY, secureLogging);
defs.put(ABUSE_ADDRESS_KEY, abuseAddress);
defs.put(NOTIFICATION_FREQ_KEY, notificationFrequency);
returndefs;
}
@Override
publicvoidsetProperties(Map<String, Object> props) {
super.setProperties(props);
badWords = (String[])props.get(BAD_WORDS_KEY);
whiteList = (String[])props.get(WHITELIST_KEY);
Arrays.sort(whiteList);
prependText = (String)props.get(PREPEND_TEXT_KEY);
secureLogging = (Boolean)props.get(SECURE_LOGGING_KEY);
abuseAddress = (String)props.get(ABUSE_ADDRESS_KEY);
notificationFrequency = (Integer)props.get(NOTIFICATION_FREQ_KEY);
}
@Override
publicsynchronizedvoideveryMinute() {
super.everyMinute();
if((++delayCounter) >= notificationFrequency) {
addOutPacket(Packet.getMessage(abuseAddress, getComponentId(),
StanzaType.chat,"Detected spam messages: "+ spamCounter,
"Spam counter",null, newPacketId("spam-")));
delayCounter =0;
spamCounter =0;
}
}
}
|
来源:oschina
链接:https://my.oschina.net/u/811247/blog/209863