计时器在很多应用场景中广泛应用,System.Threading命名空间下,有个Timer类可以完成计时器的操作。
下面来讨论下Timer类的使用及多个CPU内核同时运行线程时如何共享高速缓存(cache)的。
主要内容:
- 计时器的使用
- CPU高速缓存的伪共享
1. 计时器的使用
1.1 计时器的创建及改变设置
计时器创建的API都类似,有以下5种:
1
2
3
4
5
|
public
Timer(TimerCallback callback);
public
Timer(TimerCallback callback,
object
state,
int
dueTime,
int
period);
public
Timer(TimerCallback callback,
object
state,
long
dueTime,
long
period);
public
Timer(TimerCallback callback,
object
state, TimeSpan dueTime, TimeSpan period);
public
Timer(TimerCallback callback,
object
state,
uint
dueTime,
uint
period);
|
其中各个参数的含义如下:
- callback:定时执行的操作
- state:传递给callback的参数
- dueTime:首次调用callback时,需要等待多少毫秒
- period:每隔多少毫秒执行一次callback
下面例子演示如何创建一个计时器,并且在运行10秒后如何改变计时器的dueTime和period
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
using
System;
using
System.Threading;
public
class
CLRviaCSharp_20
{
static
void
Main(
string
[] args)
{
// 定义一个定时器,每2秒执行一次
Timer t =
new
Timer(PrintTime,
"Timer Test"
, 0, 2000);
// 主线程等待10秒(让计时器先执行6次)
Thread.Sleep(10000);
// 改变计时器的设置,停3秒后,每2秒执行一次
t.Change(3000, 1000);
Console.ReadKey(
true
);
}
private
static
void
PrintTime(
object
state)
{
Thread.Sleep(3000);
Console.WriteLine(
"{0}: Now is {1}"
, state, DateTime.Now);
}
}
|
运行上面的例子,我们发现有以下现象:
- 刚开始每隔2秒打印一次当前时间
- 10秒后,每隔1秒打印一次当前时间
- callback(即PrintTime函数)中虽然有Sleep(3000);但计时器的下一次callback并没有等待本次callback完成就开始运行了。
对于上述的第三点,是由于Timer在每个period到期时,如果上个callback没完成,会在线程池中再启动一个线程来完成本次的callback。
1.2 耗时操作(操作消耗的时间 > 计时器的时间间隔)
但是,对于某些应用场景,可能需要callback一个一个顺序执行。
如果callback的耗时较长,希望下次callback能在本次callback完成后,再过period后执行。这时,就需要我们使用Timer的Change方法实现。
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
|
using
System;
using
System.Threading;
public
class
CLRviaCSharp_20
{
static
Timer t;
static
void
Main(
string
[] args)
{
// 定义一个计时器,
// period参数设置为Timeout.Infinite的意思是计时器执行一次就结束
t =
new
Timer(PrintTime,
"Timer Test"
, 0, Timeout.Infinite);
Console.ReadKey(
true
);
}
private
static
void
PrintTime(
object
state)
{
Thread.Sleep(3000);
Console.WriteLine(
"{0}: Now is {1}"
, state, DateTime.Now);
// 因为计时器执行一次就结束,所以在callback(即PrintTime)执行完后再启动一个计时器
// 此计时器仍然设置period参数设置为Timeout.Infinite
// 并且设置dueTime参数为2000毫秒,从而保证下次callback在本次callback完成2秒后执行
// 这样不管每个callback耗用多少时间,两个callback之间的间隔时间始终是2秒
t =
new
Timer(PrintTime,
"Timer Test"
, 2000, Timeout.Infinite);
}
}
|
这样运行结果就是每隔5秒打印一次时间了。
其中2秒表示callback之间的间隔。
另外3秒表示callback的执行时间。
2. CPU高速缓存的伪共享
为了提高CPU反复访问内存的能力,在CPU芯片上集成了高速缓存,它的访问速度非常快。
CPU从内存读取数据时,会把读取到的数据存在高速缓存中。下次直接从高速缓存中读取,从而提升性能。
CPU每次读取数据时填充的缓存大小与本机的CPU有关,我的机器是64byte。
可用CPU-Z来查看。
在多核的情况下,如何2个内核使用的数据在一个缓存线(即上面的64byte)中,那么反而会影响性能。
因为内核A如果改变了数据后,内核B需要把内核A使用的缓存线(即上面的64byte)拷入到B的缓存线中。
反之,内核A再次操作数据时,也要把内核B使用的缓存线(即上面的64byte)拷回来。
但是,如果内核A和内核B使用的数据不在一个缓存线中,那么性能就会提高,因为少了上面的不同内核之间缓存线的拷贝操作。
下面用例子来演示两种情况下的性能。
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
|
using
System;
using
System.Diagnostics;
using
System.Runtime.InteropServices;
using
System.Threading.Tasks;
public
class
CLRviaCSharp_20
{
// 因为我的机器的缓存线为64byte,
// 所以两个int型字段应该在同一个缓存线中
private
class
ShareData1
{
public
int
field1;
public
int
field2;
}
// 通过设置使得两个int型字段不在一个缓存线中
[StructLayout(LayoutKind.Explicit)]
private
class
ShareData2
{
[FieldOffset(0)]
public
int
field1;
[FieldOffset(64)]
public
int
field2;
}
static
void
Main(
string
[] args)
{
int
iterations = 100000000;
ShareData1 data1 =
new
ShareData1();
// 情况一:2个数据在一个缓存行中
long
start = Stopwatch.GetTimestamp();
Task t1 =
new
Task(() => {
for
(
int
i = 0; i < iterations; i++)
data1.field1++;
});
Task t2 =
new
Task(() => {
for
(
int
i = 0; i < iterations; i++)
data1.field2++;
});
t1.Start();
t2.Start();
Task.WaitAll();
Console.WriteLine(
"Totle time is : {0:N0}"
, (Stopwatch.GetTimestamp() - start));
// 情况二:2个数据不在一个缓存行中
ShareData2 data2 =
new
ShareData2();
start = Stopwatch.GetTimestamp();
Task t3 =
new
Task(() =>
{
for
(
int
i = 0; i < iterations; i++)
data2.field1++;
});
Task t4 =
new
Task(() =>
{
for
(
int
i = 0; i < iterations; i++)
data2.field2++;
});
t3.Start();
t4.Start();
Task.WaitAll();
Console.WriteLine(
"Totle time is : {0:N0}"
, (Stopwatch.GetTimestamp() - start));
Console.ReadKey(
true
);
}
}
|
在我的双核机器上,明显第二种情况性能要优于第一种情况。
本文转自wang_yb博客园博客,原文链接:http://www.cnblogs.com/wang_yb/archive/2011/11/11/2245712.html,如需转载请自行联系原作者