MultiThreading programming #2

Thread

Thread Safety

  • 本地状态与共享状态
    • Local 本地独立:CLR为每个线程分配自己的内存栈,以便使本地变量保持独立
    • Shared 共享:如果多个线程引用到了同一个对象实例,那么他们就共享了数据;被Lambad表达式或匿名委托捕获的本地变量,会被编译器转化为字段(field),所以也会被共享;静态字段也会在线程间共享
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
class Program
{

static void Main(string[] args)
{
/*本地变量不共享*/
//打印六次hello...
Thread thread = new Thread(GO);
thread.Start();
GO();
/*同一实例共享*/
//打印一次Done!
ThreadTest test = new ThreadTest();
new Thread(test.Test).Start();
Thread.Sleep(1000);
test.Test();
/*匿名委托共享*/
//打印一次Done
bool done = false;
ThreadStart action = () =>
{
if (!done)
{
//此处还是会有输出多次的风险
done = true;
System.Console.WriteLine("Done");
}
};
new Thread(action).Start();
Thread.Sleep(new TimeSpan(0, 0, 1));
action.Invoke();
/*静态字段共享*/
//打印一次Done!
ThreadTest test1 = new ThreadTest();
ThreadTest test2 = new ThreadTest();
new Thread(test1.AnotherTest).Start();
Thread.Sleep(10);
test2.AnotherTest();
}

static void GO()
{
//cycle是本地变量,属于本地状态
//在每个线程的内存栈上,都会创建cycle的 独立副本
for (int cycle = 0; cycle< 3; cycle++)
{
System.Console.WriteLine("hello...");
}
}

}

class ThreadTest
{
private bool _done;
private static bool done;
public void AnotherTest()
{
//此处还是会有输出多次的风险
if(!done)
{
System.Console.WriteLine("DONE!");
done = true;
}
}
public void Test()
{
//此处还是会有输出多次的风险
if(!_done)
{
System.Console.WriteLine("Done!");
_done = true;
}
}

}
  • 线程安全
    • 上述后涉及线程共享数据的代码是缺乏线程安全的,其实际输出无法确定,理论上Done有可能会打印两次,因为一个线程可能正在评估if,而另一个语句没来得及调整donetrue
    • 保证线程安全:消除代码执行过程中的不确定性
      1. 尽可能的避免使用共享状态以保证线程安全
      2. 使用lock语句加锁,在读取和写入共享数据的时候,通过使用互斥锁,就可以修复前面代码中的问题,当两个线程同时竞争一个锁的时候(锁可以基于任何引用类型对象),一个线程会等待或阻塞,直到锁重新变成可用状态
    • 然而,lock也并非线程安全的银弹,lock也会引起一些其他问题(如死锁)
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
class Program
{
static readonly object locker=new object();
static bool done;
static void Main(string[] args)
{
new Thread(GO).Start();
GO();
}

static void GO()
{
// 锁要基于引用类型的变量
// 线程安全
lock(locker)
{
if (!done)
{
System.Console.WriteLine("Done");
Thread.Sleep(1000);
done = true;
}
}
}
}

Transfer Parameter

  • 如果想往线程启动方法里传递参数,最简单的方式就是使用lambda表达式,在里面使用参数调用方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Program
{

static void Main(string[] args)
{
new Thread(() => { Print("hello world"); }).Start();

}

static void Print(string s)
{
System.Console.WriteLine(s);
}
}
  • 还可以使用Thread.Start方法来传递参数,类似委托中Invoke时的传参
    • Thread的重载构造函数可以接受下列两个委托之一作为参数:
      1. public delegate void ThreadStart();
      2. public delegate void ParameterizedThreadStart(object obj);
    • 第二种委托接收带参数的方法名
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Program
{

static void Main(string[] args)
{
Thread thread = new Thread(Print);
thread.Start("hello world");
}

static void Print(object s)
{
s = s as string;
System.Console.WriteLine(s);
}
}
  • 需要注意的是lambda表达式(匿名委托)传的参数会当作字段,即就算传的是值类型的变量也会得到其地址并被线程共享;而第二种传参强制要求了引用变量,也会被线程共享
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
class Program
{
static void Main(string[] args)
{
// Thread1();
Thread2();
}

// 线程共享了同一个局部变量i
// 会出现重复的数
static void Thread1()
{
for (int i = 0; i < 5; i++)
{
new Thread(() => { System.Console.WriteLine(i); }).Start();
}
}
// 每个线程获得不同局部变量的地址
// 不会出现重复的数
static void Thread2()
{

for (int i = 0; i < 5; i++)
{
int temp = i;
new Thread(() => { System.Console.WriteLine(temp); }).Start();
}
}

}

Exception

  • 异常处理块种的线程抛出异常时,不会被捕获,解决方案是传入线程的方法中处理异常
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
class Program
{
static void Main(string[] args)
{
// 正常捕获
new Thread(Go2).Start();
// 无法捕获
try
{
new Thread(Go1).Start();
}
catch
{
System.Console.WriteLine("Exceptoin!");
}
}

static void Go1()
{
throw new Exception();
}

static void Go2()
{
try
{
throw new Exception();
}

catch
{
System.Console.WriteLine("catch sucessfully");
}
}

}

Foreground Threads and Background Threads

  • 默认情况下,手动创建的线程就是前台线程
  • 只要有前台线程在运行,那么应用程序就会一直处于活动状态
    • 后台线程运行不会保持应用程序的活动状态
    • 一旦所有前台线程停止,应用程序随即停止,后台线程也会立即终止
  • 线程的前台、后台与它的优先级无关
  • 通过IsBackground属性判断线程是否是后台线程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Program
{
static void Main(string[] args)
{
Thread thread = new Thread(() => { Console.WriteLine(Console.ReadLine());});

// 如果将thread设置为后台线程,当主线程(前台线程)执行完之后程序立即结束
if(args.Length>0)
thread.IsBackground = true;

thread.Start();
}

}
  • 如果在退出前想要等待后台线程执行完毕,可以考虑使用Join
  • 应用程序无法正常退出的一个常见原因就是还有活跃的前台线程

Priority of Threading

  • 线程的优先级(ThreadPriority属性)决定了相对于操作系统中其他活跃线程所占的执行时间
  • 优先级划分:
    • enum ThreadPriority{Lowest,BelowNormal,Normal,AboveNormal,Highest}
  • 提升线程优先级:
    • 提升线程优先级时要特别注意,因为他可能饿死其他线程
    • 如果想让某线程的优先级比其他进程中的线程优先级高,那么就必须提升进程优先级
  • 手动提升进程/线程优先级适用于只做少量工作且需要较低延迟的非UI进程
  • 对于需要大量计算的应用程序(尤其是带UI的),手动提升进程/线程优先级可能会使其他进程/线程饿死,从而降低整个计算机的速度

Signaling

  • 某个线程在收到其他线程发来通知之前一直处于等待状态,发送通知的过程就称为Signaling,不同于信号量机制
  • 最简单的信号结构就是ManualResetEvent
    • 调用其上WaitOne方法会阻塞当前线程,直到另一个线程通过调用Set方法开启信号
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Program
{
static void Main(string[] args)
{
var signal = new ManualResetEvent(false);

new Thread(() =>
{
System.Console.WriteLine("Waiting for signal...");
// 因为在实例化时设置了false主线程打开信号之前处于阻塞状态
// 成功接收到信号时不会将signal状态重置为nonusignaled
signal.WaitOne();
// 获得信号后直接关闭该信号量
System.Console.WriteLine("Got signal!");
}).Start();
for (int i = 0; i < 5; i++)
{
System.Console.WriteLine(i);
}
Thread.Sleep(1000);
signal.Set();
}
}
  • 调用完Set后信号会处于signaled状态,通过调用Reset将信号重新变为nonsignaled状态