C#多线程

C#多线程C#多线程简单示例Thread类构造函数可以传入一个委托,作为线程调用的方法。1usingSystem;2usingSystem.Threading;34namespaceTes

大家好,又见面了,我是你们的朋友全栈君。

C#多线程简单示例

Thread类构造函数可以传入一个委托,作为线程调用的方法。

<span role="heading" aria-level="2">C#多线程
<span role="heading" aria-level="2">C#多线程

 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枚举)可以控制线程执行的优先级,高优先级的线程会优先执行。

同步

当多个线程同时对一个数据进行修改时,就会因为无法控制其访问顺序导致的无法预知的错误,我们看看下面的代码:

<span role="heading" aria-level="2">C#多线程
<span role="heading" aria-level="2">C#多线程

 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代码块的代码,可以保证同一时刻只有一个线程会对其进行访问,如下:

<span role="heading" aria-level="2">C#多线程
<span role="heading" aria-level="2">C#多线程

 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,

下面我们在添加数字的地方加入一段比较耗时的方法,就会触发线程的切换了:

<span role="heading" aria-level="2">C#多线程
<span role="heading" aria-level="2">C#多线程

 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标记的代码块,会被加锁,指定同一时刻,只有一个线程可以执行代码块中的代码,需要注意的是,lock可以带一个参数,该参数用于标记代码块的加锁状态。

下面我们简单的理解一下加锁参数的用法:

1 lock (this) 2 { 3 // ... 4 }
  • 代码运行到lock时,会先判断下this对象是否已经被标记已经被某个线程运行(注意只针对lock当前的代码块);
  • 如果没有任何线程在执行该代码块,则当前线程开始运行,并且标记this对象已经被当前线程运行;
  • 如果正在某线程运行中,则阻塞等待那个线程执行完毕,其它线程执行完毕后,则当前线程开始运行,并且标记this对象已经被当前线程运行;
  • 当前线程代码块执行完毕,标记this对象没有被任何线程运行;

对lock的参数加深理解

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代码是没有意义的,多个线程仍然可以同时访问。

Monitor

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个属性:

  • 拥有锁的线程:当前正在执行的线程;
  • 就绪队列(ready queue):执行了lock、Monitor.Enter或Monitor.TryEnter的线程会放入该队列中,当拥有锁的线程释放锁之后,会让该队列中的下一个线程拥有锁并执行;
  • 等待队列(wait queue):放入该队列中的线程,不会在当拥有锁的线程释放锁之后让下一个执行,也不会加入到就绪队列中,会等待明确的指令来确定怎么处理队列中的线程;

明白了上面的3个属性后,就可以具体看这3个方法了:

  • Monitor.Wait:将当前拥有锁的线程释放锁且阻塞,并将当前的线程添加到等待队列中;
  • Monitor.Pulse:将等待队列中一个线程移到就绪队列中;
  • Monitor.PulseAll:将等待队列中的所有线程都移到就绪队列中;

其它3种同步方式

下面说的3种同步方式都属于内核对象,利用内核对象进行进程或线程之间的同步,线程必须要在用户模式和内核模式间切换,所以一般效率较lock会低一些。

不同于Monitor,这3种同步方法都可以在任意的地方对线程进行等待或者运行的控制。

EventWaitHandler

EventWaitHandle 类允许线程通过发信号互相通信。通常,一个或多个线程在 EventWaitHandle 上阻止,直到一个未阻止的线程调用 Set 方法,以释放一个或多个被阻止的线程。

Semaphore

类似互斥锁,但它可以允许多个线程同时访问一个共享资源,通过使用一个计数器来控制对共享资源的访问,如果计数器大于0,就允许访问,如果等于0,就拒绝访问。计数器累计的是“许可证”的数目,为了访问某个资源。线程必须从信号量获取一个许可证。

Mutex

Mutex类似于一个接力棒,拿到接力棒的线程才可以开始跑,当然接力棒一次只属于一个线程(Thread Affinity),如果这个线程不释放接力棒(Mutex.ReleaseMutex),那么没办法,其他所有需要接力棒运行的线程都知道能等着看热闹。

死锁

当一个或多个进程等待系统资源,而资源又被进程本身或其它进程占用时,就形成了死锁。总的来说,就是两个线程,都需要获取对方锁占有的锁,才能够接着往下执行,但是这两个线程互不相让,你等我先释放,我也等你先释放,但谁都不肯先放,就一直在这僵持住了。

我们看一个简单的示例:

<span role="heading" aria-level="2">C#多线程
<span role="heading" aria-level="2">C#多线程

 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

输出如下:

开始执行方法一 开始执行方法二

避免死锁可以有下面几个方法:

  1. 应该尽量避免大量嵌套的锁的使用;
  2. 可以使用锁的超时机制来避免对资源的长时间占用;
  3. 通过逻辑上的检查来避免死锁;

线程池

线程池(ThreadPool)有下面几个特点:

  • 线程池中所有线程都是后台线程,如果进程的所有前台线程都结束了,所有的后台线程就会停止。不能把入池的线程改为前台线程。
  • 不能给入池的线程设置优先级或名称。
  • 入池的线程只能用于时间较短的任务。如果线程要一直运行(如Word的拼写检查器线程),就应使用Thread类创建一个线程。
  • 一个进程有且只能管理一个线程池。
  • 当进程启动时,线程池并不会自动创建。当第一次将回调方法排入队列(比如调用ThreadPool.QueueUserWorkItem方法)时才会创建线程池。
  • 在对一个工作项进行排队之后将无法取消它。
  • 线程池中线程在完成任务后并不会自动销毁,它会以挂起的状态返回线程池,如果应用程序再次向线程池发出请求,那么这个挂起的线程将激活并执行任务,而不会创建新线程,这将节约了很多开销。
  • 只有线程达到最大线程数量,系统才会以一定的算法销毁回收线程。

不适合使用线程池的情形包括:

  • 如果需要使一个任务具有特定的优先级。
  • 如果具有可能会长时间运行(并因此阻塞其他任务)的任务。
  • 如果需要将线程放置到单线程单元中(线程池中的线程均处于多线程单元中)。
  • 如果需要用永久标识来标识和控制线程,比如想使用专用线程来中止该线程,将其挂起或按名称发现它。
  • 如果您需要运行与用户界面交互的后台线程,.NET Framework 2.0 版提供了 BackgroundWorker 组件,该组件可以使用事件与用户界面线程的跨线程封送进行通信。

线程池的优势:

  • 可以避免创建和销毁消除的开支,从而可以实现更好的性能和系统稳定性。
  • 把线程交给系统进行管理,程序员不需要费力于线程管理,可以集中精力处理应用程序任务。

我们看一个简单的示例:

<span role="heading" aria-level="2">C#多线程
<span role="heading" aria-level="2">C#多线程

 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

Task

ThreadPool存在一些使用上的不便,比如:

  • ThreadPool不支持线程的取消、完成、失败通知等交互性操作;
  • ThreadPool不支持线程执行的先后次序;

而Task在线程池的基础上进行了优化,并提供了更多的API。

我们看一个简单的例子:

<span role="heading" aria-level="2">C#多线程
<span role="heading" aria-level="2">C#多线程

 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

Parallel类提供了数据和任务的并行性;

我们主要看下其For方法的使用,类似于C#的for循环语句,也是多次执行一个任务。使用Paraller.For()方法,可以并行运行迭代,迭代的顺序是乱序的。

我们直接看一个例子:

<span role="heading" aria-level="2">C#多线程
<span role="heading" aria-level="2">C#多线程

 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

Unity中使用多线程

和C#中使用完全一致,需要注意的是,子线程不能操作和访问Unity的任何对象,需要通过发送消息到主线程来实现控制。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/155264.html原文链接:https://javaforall.cn

【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛

【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...

(0)
blank

相关推荐

  • RDIFramework.NET ━ .NET快速信息化系统开发框架 V3.2->WinForm版本新增新的用户权限设置界面效率更高、更规范…

    RDIFramework.NET ━ .NET快速信息化系统开发框架 V3.2->WinForm版本新增新的用户权限设置界面效率更高、更规范…

  • vue-router实现路由懒加载( 动态加载路由 )_前端懒加载原理

    vue-router实现路由懒加载( 动态加载路由 )_前端懒加载原理为什么需要懒加载?    像vue这种单页面应用,如果没有应用懒加载,运用webpack打包后的文件将会异常的大,造成进入首页时,需要加载的内容过多,时间过长,会出啊先长时间的白屏,即使做了loading也是不利于用户体验,而运用懒加载则可以将页面进行划分,需要的时候加载页面,可以有效的分担首页所承担的加载压力,减少首页加载用时vue异步组件 es提案的import() webpack…

  • php json字符串转json对象_PHP字符串函数

    php json字符串转json对象_PHP字符串函数怎么把php字符串转为json发布时间:2020-07-2214:05:08来源:亿速云阅读:162作者:Leah这期内容当中小编将会给大家带来有关怎么把php字符串转为json,文章内容丰富且以专业的角度为大家分析和叙述,阅读完这篇文章希望大家可以有所收获。php把字符串转为json的方法:首先定义一个数组,调用json_encode方法将数组编码为json格式的字符串;然后添加参数“true…

    2022年10月30日
  • kali最新版安装教程_kali linux安卓版安装

    kali最新版安装教程_kali linux安卓版安装百度搜索kali,就是它了顺着箭头安装,建议使用网盘或者IDM下载,浏览器等待时间太长。下载后解压文件夹,然后打开VMware输入默认的虚拟机账号密码,均为kali选择第一个,然后我们的kali就安装好了。之后就是获取root最高权限方便我们使用kaili所有的工具点击openterminalhere输入sudopasswdroot回车,输入原来密码:kali设置新密码.之前的用户会被注销,登录新的账号回到桌面再次点击openterminalhere,可以看到我们的权限已

  • 免费域名和空间搭建个人网站——服务器篇

    免费域名和空间搭建个人网站——服务器篇免费域名和空间搭建个人网站服务器篇网上有很多免费的服务器,但是免费的都不好用,只能凑合一下啦~~当然你也可以购买一些像腾讯,阿里云或者国外的虚拟主机。我用的是国内的主机屋点击免费空间,选择立即开通,然后登陆,注册成功后,点击立即开通,就可以了开通之后,进入控制台,点击一键初始化网站然后初始化FTP密码,初始化Mysql数据库密码,接下来需要解析域名,选择常规功能,点击域

  • SqlServer定时备份数据库和定时杀死数据库死锁解决

    SqlServer定时备份数据库和定时杀死数据库死锁解决

发表回复

您的电子邮箱地址不会被公开。

关注全栈程序员社区公众号