大家好,又见面了,我是你们的朋友全栈君。
Thread类构造函数可以传入一个委托,作为线程调用的方法。
1 using System;
2 using System.Threading;
3
4 namespace Test
5 {
6 public class Thread1
7 {
8 public static void ThreadFunc1()
9 {
10 while (true)
11 {
12 Console.WriteLine("Thread 1!");
13 Thread.Sleep(1000);
14 }
15 }
16
17 private int num = 5;
18
19 public Thread1()
20 {
21 // 使用静态方法作为线程调用的方法,不带参数
22 Thread thread1 = new Thread(ThreadFunc1);
23 thread1.Start();
24
25 // 使用成员方法作为线程调用的方法,可带一个 object 类型的参数
26 Thread thread2 = new Thread(ThreadFunc2);
27 thread2.Start(2);
28 }
29
30 private void ThreadFunc2(object obj)
31 {
32 int count = num * (int)obj;
33 while (count > 0)
34 {
35 Console.WriteLine("Thread 2!");
36 Thread.Sleep(1000);
37 --count;
38 }
39 }
40 }
41 }
View Code
Thread类的第二个参数可以控制堆栈大小,堆栈大小的简介如下:
每个线程独立拥有一个可配置大小的堆栈,一个线程内所有函数使用到的堆栈都依赖于这个栈,如果太多的变量、参数需要使用栈,则可能导致栈溢出。目前基础平台子系统通过配置环境变量,将默认堆栈大小设置为128K,可以减少这个问题的出现,但业务系统在编码时仍然 需要注意栈的使用,避免出现问题。 包括: 1、不要在函数内部定义过大的局部变量,如过大的结构体变量,联合变量,过大的字符串,数组等; 2、函数调用的深度也需要注意,如果函数 A 调用 B, B 再调用 C,而A/B/C每个函数定义了 10 K的局部变量,则总的栈空间需求将超过 30K; 3、不要直接将大的结构变量通过函数参数传递,这样也会消耗栈空间,可以通过指针或者引用的方式传递; 4、建议每个函数内部定义的变量大小控制在4-8K以下; 5、如果在运行中 COREDUMP,并且通过 GDB 的 WHERE 命令时看到刚进入某个函数就报错,连函数内的第一条调试语句都无法指向,则基本可以认为是栈空间不够导致的,可以尝试将栈空间配置大一点,如果问题不再出现,则可以确定问题。这时需要按照前面几点的要求修改代码,减少栈的使用。
所有前台线程关闭后,还有后台线程在运行的话,后台线程会全部关闭。
主线程和通过Thread构造函数创建的线程默认都是前台线程,线程池获取的则默认是后台线程,通过 IsBackground 属性可以设置和获取当前线程是前台线程还是后台线程。
Priority属性(ThreadPriority枚举)可以控制线程执行的优先级,高优先级的线程会优先执行。
当多个线程同时对一个数据进行修改时,就会因为无法控制其访问顺序导致的无法预知的错误,我们看看下面的代码:
1 using System.Collections.Generic;
2 using System;
3 using System.Threading;
4
5 namespace Test
6 {
7 public class Thread2
8 {
9 private List<int> _nums;
10
11 public Thread2()
12 {
13 _nums = new List<int>();
14
15 Thread thread1 = new Thread(ThreadFunc1);
16 thread1.Start();
17
18 Thread thread2 = new Thread(ThreadFunc2);
19 thread2.Start();
20
21 Console.WriteLine("线程已启动");
22
23 Thread.Sleep(3000);
24
25 string str = "";
26 foreach (var item in _nums)
27 {
28 str += item + ", ";
29 }
30 Console.WriteLine(str);
31 }
32
33 private void ThreadFunc1()
34 {
35 for (int i = 0; i < 10; i++)
36 {
37 AddNum(i);
38 }
39 }
40
41 private void ThreadFunc2()
42 {
43 for (int i = 10; i < 20; i++)
44 {
45 AddNum(i);
46 }
47 }
48
49 private void AddNum(int num)
50 {
51 _nums.Add(num);
52 }
53 }
54 }
View Code
输出如下:
0, 1, 2, 3, 14, 5, 6, 7, 8, 16, 17, 18, 19,
我们发现,由于可能同时调用AddNum方法,会导致_nums中的顺序和数量都出现问题。
可以通过给AddNum方法加锁来解决两个线程同时访问同一块数据,加了lock代码块的代码,可以保证同一时刻只有一个线程会对其进行访问,如下:
1 using System.Collections.Generic;
2 using System;
3 using System.Threading;
4
5 namespace Test
6 {
7 public class Thread2
8 {
9 private List<int> _nums;
10
11 public Thread2()
12 {
13 _nums = new List<int>();
14
15 Thread thread1 = new Thread(ThreadFunc1);
16 thread1.Start();
17
18 Thread thread2 = new Thread(ThreadFunc2);
19 thread2.Start();
20
21 Console.WriteLine("线程已启动");
22
23 Thread.Sleep(3000);
24
25 string str = "";
26 foreach (var item in _nums)
27 {
28 str += item + ", ";
29 }
30 Console.WriteLine(str);
31 }
32
33 private void ThreadFunc1()
34 {
35 for (int i = 0; i < 10; i++)
36 {
37 AddNum(i);
38 }
39 }
40
41 private void ThreadFunc2()
42 {
43 for (int i = 10; i < 20; i++)
44 {
45 AddNum(i);
46 }
47 }
48
49 private void AddNum(int num)
50 {
51 lock (this)
52 {
53 _nums.Add(num);
54 }
55 }
56 }
57 }
View Code
看起来输出正常了,但是其实是每个线程的代码执行速度很快,所以看不出来线程的切换,如下:
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19,
下面我们在添加数字的地方加入一段比较耗时的方法,就会触发线程的切换了:
1 using System.Collections.Generic;
2 using System;
3 using System.Threading;
4
5 namespace Test
6 {
7 public class Thread2
8 {
9 private List<int> _nums;
10
11 public Thread2()
12 {
13 _nums = new List<int>();
14
15 Thread thread1 = new Thread(ThreadFunc1);
16 thread1.Start();
17
18 Thread thread2 = new Thread(ThreadFunc2);
19 thread2.Start();
20
21 Console.WriteLine("线程已启动");
22
23 Thread.Sleep(3000);
24
25 string str = "";
26 foreach (var item in _nums)
27 {
28 str += item + ", ";
29 }
30 Console.WriteLine(str);
31 }
32
33 private void ThreadFunc1()
34 {
35 for (int i = 0; i < 10; i++)
36 {
37 AddNum(i);
38 }
39 }
40
41 private void ThreadFunc2()
42 {
43 for (int i = 10; i < 20; i++)
44 {
45 AddNum(i);
46 }
47 }
48
49 private void AddNum(int num)
50 {
51 lock (this)
52 {
53 _nums.Add(num);
54 largeComputationalCost();
55 }
56 }
57
58 private void largeComputationalCost()
59 {
60 for (int i = 0; i < 10000000; i++)
61 {
62 }
63 }
64 }
65 }
View Code
可以从结果看出来,数字的插入顺序是乱序的:
0, 1, 10, 2, 3, 11, 12, 4, 5, 13, 14, 15, 6, 7, 8, 16, 9, 17, 18, 19,
被lock标记的代码块,会被加锁,指定同一时刻,只有一个线程可以执行代码块中的代码,需要注意的是,lock可以带一个参数,该参数用于标记代码块的加锁状态。
下面我们简单的理解一下加锁参数的用法:
1 lock (this)
2 {
3 // ...
4 }
1. lock参数只能是引用类型,如果是值类型会怎样,我们看下面的例子:
1 int a = 1;
2 lock (a)
3 {
4 // ...
5 }
因为每次运行到这里,都会是一个新的值类型a,所以其它的线程给这个值类型a打了加锁标记后,下一个线程运行到这里会发现值类型a仍然是没有加锁的,lock代码块就变得毫无意义,多个线程仍然可以同时访问。
2. 大部分的情况下,lock参数都是使用的this:
当然这是因为,大部分情况下,我们多线程操作的都是当前对象实例的成员变量,多个对象的实例相互之间不需要加锁。
我们也可以传递其它的引用实例来打加锁标记,但是需要注意只有相同的引用,才会保证只有一个线程访问lock代码块,我们看看下面比较极端的情况:
1 MyClass a = new MyClass();
2 lock (a)
3 {
4 // ...
5 }
这种情况和使用值类型一样,因为每次执行都会产生一个新对象,所以加lock代码是没有意义的,多个线程仍然可以同时访问。
lock代码块可以看做是Monitor的语法糖,在IL代码中lock会被翻译成Monitor,也就是Monitor.Enter(obj)和Monitor.Exit(obj),如下:
1 lock (this)
2 {
3 // ...
4 }
5 // 等同于下面这样
6 try
7 {
8 Monitor.Enter(this);
9 // ...
10 }
11 finally
12 {
13 Monitor.Exit(this);
14 }
Monitor还额外提供了一些功能:
1. Monitor.TryEnter(obj, timespan),超过timespan的时间之后,就不执行这段代码了,而lock会一直等待从而出现死锁。
2. Monitor.Wait()、Monitor.Pulse()和Monitor.PulseAll(),要弄清楚这3个方法的含义,需要先理解lock的下面的流程:
对于同一个被lock的对象,会有下面3个属性:
明白了上面的3个属性后,就可以具体看这3个方法了:
下面说的3种同步方式都属于内核对象,利用内核对象进行进程或线程之间的同步,线程必须要在用户模式和内核模式间切换,所以一般效率较lock会低一些。
不同于Monitor,这3种同步方法都可以在任意的地方对线程进行等待或者运行的控制。
EventWaitHandle 类允许线程通过发信号互相通信。通常,一个或多个线程在 EventWaitHandle 上阻止,直到一个未阻止的线程调用 Set 方法,以释放一个或多个被阻止的线程。
类似互斥锁,但它可以允许多个线程同时访问一个共享资源,通过使用一个计数器来控制对共享资源的访问,如果计数器大于0,就允许访问,如果等于0,就拒绝访问。计数器累计的是“许可证”的数目,为了访问某个资源。线程必须从信号量获取一个许可证。
Mutex类似于一个接力棒,拿到接力棒的线程才可以开始跑,当然接力棒一次只属于一个线程(Thread Affinity),如果这个线程不释放接力棒(Mutex.ReleaseMutex),那么没办法,其他所有需要接力棒运行的线程都知道能等着看热闹。
当一个或多个进程等待系统资源,而资源又被进程本身或其它进程占用时,就形成了死锁。总的来说,就是两个线程,都需要获取对方锁占有的锁,才能够接着往下执行,但是这两个线程互不相让,你等我先释放,我也等你先释放,但谁都不肯先放,就一直在这僵持住了。
我们看一个简单的示例:
1 using System;
2 using System.Threading;
3
4 namespace Test
5 {
6 public class Thread3
7 {
8 private Object obj1 = new object();
9 private Object obj2 = new object();
10
11 public Thread3()
12 {
13 Thread thread1 = new Thread(ThreadFunc1);
14 thread1.Start();
15
16 Thread thread2 = new Thread(ThreadFunc2);
17 thread2.Start();
18 }
19
20 private void ThreadFunc1()
21 {
22 lock (obj1)
23 {
24 Console.WriteLine("开始执行方法一");
25 Thread.Sleep(1000);
26 lock (obj2)
27 {
28 Console.WriteLine("方法一执行完毕");
29 }
30 }
31 }
32
33 private void ThreadFunc2()
34 {
35 lock (obj2)
36 {
37 Console.WriteLine("开始执行方法二");
38 Thread.Sleep(1000);
39 lock (obj1)
40 {
41 Console.WriteLine("方法二执行完毕");
42 }
43 }
44 }
45 }
46 }
View Code
输出如下:
开始执行方法一
开始执行方法二
避免死锁可以有下面几个方法:
线程池(ThreadPool)有下面几个特点:
不适合使用线程池的情形包括:
线程池的优势:
我们看一个简单的示例:
1 using System;
2 using System.Threading;
3
4 namespace Test
5 {
6 public class Thread4
7 {
8 public Thread4()
9 {
10 ThreadPool.QueueUserWorkItem(new WaitCallback(ThreadFunc), 10);
11 ThreadPool.QueueUserWorkItem(new WaitCallback(ThreadFunc), 15);
12
13 // 避免程序退出
14 Thread.Sleep(5000);
15 }
16
17 private void ThreadFunc(object o)
18 {
19 for (int i = 0; i < (int)o; i++)
20 {
21 Thread.Sleep(100);
22 }
23 Console.WriteLine("线程已执行完毕 " + o);
24 }
25 }
26 }
View Code
输出如下:
线程已执行完毕 10
线程已执行完毕 15
ThreadPool存在一些使用上的不便,比如:
而Task在线程池的基础上进行了优化,并提供了更多的API。
我们看一个简单的例子:
1 using System;
2 using System.Threading;
3 using System.Threading.Tasks;
4
5 namespace Test
6 {
7 public class Thread5
8 {
9 public Thread5()
10 {
11 Task t = new Task(() =>
12 {
13 Console.WriteLine("任务开始工作……");
14 // 模拟工作过程
15 Thread.Sleep(5000);
16 });
17 t.Start();
18 t.ContinueWith((task) =>
19 {
20 Console.WriteLine("任务完成,完成时候的状态为:");
21 Console.WriteLine("IsCanceled={0}\tIsCompleted={1}\tIsFaulted={2}", task.IsCanceled, task.IsCompleted, task.IsFaulted);
22 });
23
24 // 避免程序退出
25 Thread.Sleep(6000);
26 }
27 }
28 }
View Code
输出如下:
任务开始工作……
任务完成,完成时候的状态为:
IsCanceled=False IsCompleted=True IsFaulted=False
Parallel类提供了数据和任务的并行性;
我们主要看下其For方法的使用,类似于C#的for循环语句,也是多次执行一个任务。使用Paraller.For()方法,可以并行运行迭代,迭代的顺序是乱序的。
我们直接看一个例子:
1 using System;
2 using System.Threading;
3 using System.Threading.Tasks;
4
5 namespace Test
6 {
7 public class Thread6
8 {
9 public Thread6()
10 {
11 ParallelLoopResult result = Parallel.For(0, 10, new ParallelOptions() { MaxDegreeOfParallelism = 10 }, i =>
12 {
13 Console.WriteLine("迭代次数:{0}, 任务ID:{1}, 线程ID:{2}", i, Task.CurrentId, Thread.CurrentThread.ManagedThreadId);
14 Thread.Sleep(10);
15 });
16 Console.WriteLine("是否完成:{0}", result.IsCompleted);
17 }
18 }
19 }
View Code
输出如下:
迭代次数:2, 任务ID:2, 线程ID:6
迭代次数:0, 任务ID:5, 线程ID:1
迭代次数:1, 任务ID:1, 线程ID:4
迭代次数:3, 任务ID:3, 线程ID:5
迭代次数:4, 任务ID:4, 线程ID:7
迭代次数:7, 任务ID:5, 线程ID:1
迭代次数:6, 任务ID:4, 线程ID:7
迭代次数:5, 任务ID:1, 线程ID:4
迭代次数:8, 任务ID:2, 线程ID:6
迭代次数:9, 任务ID:3, 线程ID:5
是否完成:True
和C#中使用完全一致,需要注意的是,子线程不能操作和访问Unity的任何对象,需要通过发送消息到主线程来实现控制。
发布者:全栈程序员栈长,转载请注明出处:https://javaforall.cn/155264.html原文链接:https://javaforall.cn