(20)线程安全问题:Lock,双锁问题,Monitor,死锁
一、Lock
1、用多线程给变量自增,10000个线程自增
List<Task> tasks = new List<Task>();int AsyncNum = 0;for (int i = 0; i < 10000; i++){tasks.Add(Task.Run(() =>{AsyncNum++;}));}Task.WaitAll(tasks.ToArray());Console.WriteLine($"AsyncNum is {AsyncNum}");
最后的结果:AsyncNum is 9980 。不是10000。
为什么?
多个任务并行地对AsyncNum进行递增操作。由于这些任务是并行执行的,可能会出现竞态条件(race condition)。
竞态条件是指多个线程同时访问和修改共享资源,而没有适当的同步措施来保证操作的原子性。在这种情况下,多个任务可能会同时读取和修改AsyncNum的值,导致结果不确定。
2、什么是竞态条件?
竞态条件(Race Condition)是指当多个线程或进程同时访问和操作共享资源时,最终的结果依赖于它们执行的相对时间顺序,而不是预期的顺序。竞态条件可能导致不确定的行为,破坏程序的正确性和可靠性。
竞态条件发生的原因是并发执行的线程或进程之间的交互和竞争。当多个线程同时访问和修改共享资源时,如果没有适当的同步机制来保证操作的原子性,就会出现竞态条件。这些共享资源可以是内存中的变量、文件、网络连接、数据库等。
int count = 0;void Increment(){int temp = count;temp++;count = temp;}
上面,多个线程同时调用Increment方法来递增count变量的值。由于没有适当的同步机制,多个线程可能会同时读取和修改count的值,导致结果不确定。例如,如果两个线程同时读取count的值为10,然后将其递增,并将结果写回count,那么最终的结果可能是11,而不是预期的12。
3、改进,用lock
List<Task> tasks = new List<Task>();int AsyncNum = 0;object lockobject = new object();for (int i = 0; i < 10000; i++){tasks.Add(Task.Run(() =>{lock (lockobject){AsyncNum++;}}));}Task.WaitAll(tasks.ToArray());Console.WriteLine($"AsyncNum is {AsyncNum}");
为了解决竞态条件,可以使用线程同步机制,如互斥锁、信号量、条件变量等,来确保对共享资源的访问是原子的。这些同步机制可以保证只有一个线程能够访问共享资源,并且在修改共享资源之前,其他线程必须等待。
通过避免竞态条件,您可以确保多线程或多进程的程序能够正确地访问和操作共享资源,从而提高程序的正确性和可靠性。
4、什么是lock?
lock是C#中的一个关键字,用于实现线程同步和互斥访问共享资源。它提供了一种简单的方式来确保在同一时间只有一个线程可以访问被保护的代码块。lock关键字的语法:
lock (lockObject){// 被保护的代码块}
上面lockObject是一个用于同步的对象。当一个线程进入lock代码块时,它会尝试获取lockObject的锁。如果锁可用,该线程将进入临界区,执行被保护的代码块。其他线程在这个时候会被阻塞,直到锁被释放。
当线程执行完被保护的代码块后,会自动释放锁,允许其他线程进入临界区。这样就确保了在同一时间只有一个线程可以执行被保护的代码块,从而避免了竞态条件。
lock关键字使用的是独占锁(exclusive lock),也称为互斥锁(mutex)。它保证了在任何给定的时刻,只有一个线程能够持有锁,并且其他线程必须等待锁的释放。
注意,lock关键字只能用于引用类型的对象。通常情况下,可以使用一个专门用于同步的对象作为锁。例如,可以使用一个object类型的变量作为锁:
object lockObject = new object();lock (lockObject){// 被保护的代码块}
在使用lock关键字时,需要注意以下几点:
(1)尽量将锁的范围限制在最小的代码块内,以减少对其他线程的阻塞时间。
(2)锁对象应该是一个私有的、只在同步上下文中使用的对象,以避免外部代码对锁的访问。
(3)避免在锁内部调用可能引发异常的代码,以免锁无法被释放。
lock关键字是一种简单而有效的线程同步机制,可以确保共享资源的安全访问。但在一些复杂的情况下,可能需要使用更高级的同步机制,如Monitor类、互斥体、信号量等。
lock使用静态、只读和私有的引用对象(不能是string,不能是null)是一个常见的做法。
(1)静态:
使用静态对象作为锁可以确保在多个实例之间共享同一个锁,从而实现跨实例的线程同步。如果使用实例级别的对象作为锁,那么每个实例都会有一个独立的锁,无法实现跨实例的同步。
(2)只读:
使用只读对象作为锁可以确保锁对象的引用不会被修改。这是因为lock语句需要一个不可变的锁对象,以便在多个线程之间共享同一个锁。如果锁对象可变,那么在多个线程之间可能会出现竞争条件,导致同步失效。
(3)私有:
将锁对象声明为私有可以限制对锁对象的访问,避免外部代码对锁对象的误用。这可以提高代码的可维护性和安全性。
public static int count = 0;private static readonly object obj = new object();//静态,私有,只读,引用private static async Task Main(string[] args){if (!File.Exists(@"E:\1.txt")){ File.Create(@"E:\1.txt").Close(); }else{try{using (var f = File.Open(@"E:\1.txt", FileMode.Open, FileAccess.Read, FileShare.None)){}File.WriteAllText(@"E:\1.txt", string.Empty);}catch (Exception){ Console.WriteLine("文件正被使用!"); return; }}List<Task> tasks = new List<Task>();for (int i = 0; i < 100; i++){tasks.Add(Task.Run(() =>{//lock (obj){WriteTxt();}}));}await Task.WhenAll(tasks);Console.WriteLine("任务完成");Console.ReadKey();}private static void WriteTxt(){string s = (++count).ToString("000");s = $"[{s}]:{DateTime.Now}\r\n";File.AppendAllText(@"E:\1.txt", s);}
加上锁后,写入txt文件就不会出错,否则多个线程竞争抛异常。
5、理解lock
正如单人厕所一样,多个人(线程)想要上厕所,那么A先来就进去关门锁上,其它人一看,有人!只有等待A上完。A上完后开锁打开门出去,其它人就按一定的规则,又进去一个人,然后关门锁上。。。如此,直到所人的都上完。
上锁与开锁就一个信号灯一样。
这个锁就是一个锚定对象,是一个信号灯,与真正做事(上厕所)无关联。
要控制锁的范围,不能无规则地打开或锁上(上面4(2)),同时上锁后上厕所的时间要控制好(上面4(1)),不能占用太久,只写关键性的小片代码,以免门外的多人(等待线程)等待得太久。当然正在上厕所的人(执行线程)也不要发病(异常),来个脑梗,外面的人(等待线程)就不知道里面细节,大家一直死等。
6、使用lock的注意点
在使用lock关键字时,lockObject并不需要具有特定的有意义的值,它只是一个用于同步的锚定对象。可以将其看作是一个信号灯或互斥锁,用于控制对被保护代码块的访问。
当一个线程进入lock代码块时,它会尝试获取lockObject的锁。如果锁可用,该线程将进入临界区,执行被保护的代码块。其他线程在这个时候会被阻塞,直到锁被释放。
只有当锁被释放,即被保护的代码块执行完毕后,其他线程才能够进入临界区执行被保护的代码块。
lock关键字的作用是确保在同一时间只有一个线程可以进入被保护的代码块,从而避免了多个线程同时访问共享资源导致的竞态条件。
注意,为了确保同步,应该使用一个专门用于同步的对象作为锁,而不是使用共享资源本身作为锁。这是因为锁对象应该是私有的,并且只在同步上下文中使用,以避免外部代码对锁的访问。
举例:不用锁时
public static readonly object obj = new object();private static void Main(string[] args){int count = 0;for (int i = 0; i < 10; i++){Task.Run(() =>{Console.WriteLine(count++);});}Console.WriteLine("任务完成.");Console.ReadKey();}
结果:
任务完成.
1
2
9
7
4
3
5
6
0
8
问:上面并发后是乱序,但并没有竞争,为什么呢?
答:由于 Console.WriteLine 方法内部包含了线程同步机制,每次只能有一个线程执行该方法。因此,当多个线程同时执行 Console.WriteLine(count) 时,会按照顺序进行输出,避免输出结果中的数字重复。这就是为什么在 lock 语句块内部输出的结果中没有重复数字。
修改一下:
int count = 0;for (int i = 0; i < 20; i++){Task.Run(() =>{//lock (obj){Console.WriteLine(count);count++;}});}Console.WriteLine("任务完成.");
结果:
任务完成.
0
0
1
3
4
2
2
6
2
5
将 count++ 放在 lock 语句块外面时,多个线程可以同时执行 count++ 操作,这就产生了竞争条件。多个线程同时增加 count 的值,可能会导致输出结果中出现重复的数字。
因此,为了避免竞争条件,应该将 count++ 操作放在 lock 语句块内部,确保每次只有一个线程能够执行该操作。这样可以保证输出结果中不会出现重复的数字。
用锁后:
public static readonly object obj = new object();private static void Main(string[] args){int count = 0;for (int i = 0; i < 10; i++){Task.Run(() =>{lock (obj){Console.WriteLine(count);count++}});}Console.WriteLine("任务完成.");Console.ReadKey();}
结果:
0
1
2
3
4
5
6
7
任务完成.
8
9
7、上面程序可以用并发:
List<Task> tasks = new List<Task>();int pNum = 0;object lockObject = new object();ParallelOptions po = new ParallelOptions();po.MaxDegreeOfParallelism = 10;Parallel.ForEach(Enumerable.Range(0, 10000), po, i =>{lock (lockObject){pNum++;}});Console.WriteLine(pNum);
上面用Parallel并发执行,但同样也有竞态条件,需要用lock进行锁定,确保同时只有一个线程执行。
上面foreach中第三个参数省略了括号。当委托的参数列表只有一个参数时,可以省略参数的括号。这是一种简化语法的写法。省略参数括号可以使代码更加简洁,但也可能降低代码的可读性,因此需要根据具体情况进行权衡。
8、什么是Parallel ?
Parallel类是.NET Framework中提供的一个用于并行编程的工具类。它提供了一组方法和类型,可以简化并行任务的创建和执行,并充分利用多核处理器的性能。
使用Parallel类可以将一个任务分解为多个子任务,并并行地执行这些子任务。这样可以加快任务的执行速度,提高系统的响应性能。
Parallel类提供了以下常用的方法:
(1)Parallel.For:
(2)Parallel.ForEach:用于并行地遍历一个集合。可以提供一个委托来定义对集合元素的操作。
(3)Parallel.Invoke:用于并行地执行多个操作。可以提供多个委托,每个委托定义一个操作。
这些方法都会自动将任务分配给可用的处理器核心,并根据系统资源的情况进行动态调整。它们还提供了一些选项,可以控制并行执行的行为,如最大并行度、取消操作等。
使用Parallel类编写并行代码时,需要注意以下几点:
(1)**任务的独立性**:
要确保并行执行的任务之间是相互独立的,不会产生竞态条件或数据依赖关系。这样才能确保并行执行的正确性和性能提升。
(2)**共享资源的同步**:
如果多个任务需要访问共享资源(如共享变量),则需要使用线程同步机制来确保对共享资源的访问是线程安全的。可以使用锁、互斥量、信号量等机制来实现线程同步。
(3)**性能评估和调优**:
并行执行的性能往往受到多个因素的影响,如任务的粒度、任务之间的通信开销、系统资源的利用率等。在编写并行代码时,需要进行性能评估和调优,以获得最佳的性能提升。
9、什么是ParallelOptions?
ParallelOptions是Parallel类中的一个类,用于指定并行执行的一些选项和参数。通过创建一个ParallelOptions对象,并将其作为参数传递给Parallel类的方法,可以对并行执行的行为进行定制和控制。
ParallelOptions类提供了以下常用的属性和方法:
(1)MaxDegreeOfParallelism:
用于设置并行执行的最大并行度。可以通过设置此属性的值来限制并行任务所使用的处理器核心数量。默认情况下,MaxDegreeOfParallelism的值为-1,表示使用系统可用的所有处理器核心。
(2)CancellationToken:
用于设置一个取消标记,以便在需要时取消并行执行的操作。可以使用CancellationTokenSource类创建一个取消标记,并将其传递给ParallelOptions的构造函数或CancellationToken属性。
(3)TaskScheduler:
用于设置并行任务的调度器。可以通过TaskScheduler类的静态方法来创建一个自定义的调度器,并将其传递给ParallelOptions的构造函数或TaskScheduler属性。
通过创建一个新的ParallelOptions对象,可以为每个并行操作提供不同的选项和参数,以满足具体的需求。
10、对上面程序再优化一下:
List<Task> tasks = new List<Task>();int pNum = 0;object lockObject = new object();ParallelOptions po = new ParallelOptions();po.MaxDegreeOfParallelism = 10;Parallel.ForEach(Enumerable.Range(0, 10000), po, (i) =>{Interlocked.Increment(ref pNum);});Console.WriteLine(pNum);Console.ReadKey();
ref关键字用于传递参数的引用。在该代码中,Interlocked.Increment(ref pNum)使用了ref关键字来将pNum参数按引用传递给Interlocked.Increment方法。
通过使用ref关键字,可以使参数按引用传递而不是按值传递。这意味着在方法内部对参数的修改会影响到调用方法时传递的变量本身。
在Interlocked.Increment(ref pNum)中,pNum是一个变量(通常是整数类型),Interlocked.Increment方法将该变量的值原子性地增加1,并返回新的值。通过使用ref关键字,可以确保对pNum的操作作用于原始变量,而不仅仅是传递了一个副本。
11、什么是Interlocked?
Interlocked是C#中的一个类,提供了一组原子操作方法,用于对共享变量进行原子操作。这些原子操作是在硬件级别上实现的,可以确保操作的原子性,即在执行期间不会被其他线程中断。
相比于使用锁来实现线程安全,Interlocked的原子操作具有以下优势:
(1)**无需获取锁**:
使用锁需要线程在进入临界区之前获取锁对象,而在离开临界区之后释放锁对象。这个过程涉及到线程的上下文切换和内核模式的开销,可能会影响性能。而使用Interlocked的原子操作,不需要获取锁对象,因此可以避免这些开销。
(2)**原子性保证**:
Interlocked的原子操作是在硬件级别上实现的,可以确保操作的原子性。这意味着在执行原子操作期间,不会有其他线程对共享变量进行修改,从而避免了竞态条件的发生。
(3)**高性能**:
由于Interlocked的原子操作是在硬件级别上实现的,它们通常比使用锁的方式具有更高的性能。这是因为原子操作不涉及线程的上下文切换和内核模式的开销,而且可以在多核处理器上并行执行。
Interlocked.Decrement: 原子地将指定变量递减1,并返回递减后的值。
int count = 10;
Interlocked.Decrement(ref count);
Interlocked.CompareExchange: 原子地比较并交换变量的值。可以用来实现原子的读取-修改-写入操作。
int value = 5;
int newValue = 10;
int oldValue = Interlocked.CompareExchange(ref value, newValue, 5);
注意,Interlocked的原子操作只能应用于特定类型的共享变量,例如int、long、float等。对于复杂的数据结构或需要进行多个操作的情况,可能仍然需要使用锁来保证线程安全。
问:Interlocked能用在类上吗?
答:不能。
Interlocked类提供的原子操作方法主要适用于基本数据类型(如int、long、float等)的操作。它们是在硬件级别上实现的,可以确保操作的原子性。
对于复杂的数据结构或类的操作,Interlocked类的原子操作方法并不直接适用。如果需要在多线程环境下对类进行操作,通常需要使用锁或其他线程同步机制来确保线程安全。
注意,lock关键字会引入一定的开销,因为它涉及到线程的上下文切换和内核模式的开销。因此,在使用锁时需要权衡性能和线程安全性。
总结,Interlocked类的原子操作方法适用于基本数据类型的操作,而对于复杂的数据结构或类的操作,通常需要使用锁或其他线程同步机制来确保线程安全。
二、Lock的死锁
1、多把锁也可能造成相互死锁。
例:可能同时使用刀与叉,有时只用刀或叉。给刀一把锁和叉一把锁。线程A与B分别使用这两把锁。
private static readonly object obj1 = new object();private static readonly object obj2 = new object();private static void Main(string[] args){int n = 0;Task t1 = Task.Run(() =>{for (int i = 0; i < 5; i++){lock (obj1){Console.WriteLine("第A线程上1锁" + n);lock (obj2){n++;Console.WriteLine("第A线程 " + n);}}}});Task t2 = Task.Run(() =>{for (int i = 0; i < 5; i++){lock (obj2){Console.WriteLine("第B线程上2锁" + n);lock (obj1){n++;Console.WriteLine("第B线程----" + n);}}}});Console.ReadKey();}
结果:
第A线程上1锁0
第B线程上2锁0
因为A准务进行等待第2锁,但此时第2锁被B线程占用,而B线程占用2锁又等待1锁,AB两线程相互等待对方的锁,形成死锁。
解决办法:
把锁的顺序改为一致即可。
private static readonly object obj1 = new object();private static readonly object obj2 = new object();private static void Main(string[] args){int n = 0;Task t1 = Task.Run(() =>{for (int i = 0; i < 5; i++){lock (obj1){Console.WriteLine("第A线程上1锁" + n);lock (obj2){n++;Console.WriteLine("第A线程 " + n);}}}});Task t2 = Task.Run(() =>{for (int i = 0; i < 5; i++){lock (obj1){Console.WriteLine("第B线程上2锁" + n);lock (obj2){n++;Console.WriteLine("第B线程----" + n);}}}});Console.ReadKey();}
结果:
第A线程上1锁0
第A线程 1
第A线程上1锁1
第A线程 2
第B线程上2锁2
第B线程----3
第B线程上2锁3
第B线程----4
第B线程上2锁4
第B线程----5
第B线程上2锁5
第B线程----6
第B线程上2锁6
第B线程----7
第A线程上1锁7
第A线程 8
第A线程上1锁8
第A线程 9
第A线程上1锁9
第A线程 10
9、问:lock(obj)如果修改了obj,会怎样?
答:如果修改了 obj,那么锁定的对象就会发生变化,此时 lock(obj) 将不再起作用。即起不到锁定的作用。
三、Monitor
1、Monitor和lock也是实现同步与互斥的。
其实lock的底层就是使用Monitor来实现的。
Enter指的是Monitor.Enter(获取指定对象上的排他锁。);
Exit指的是Monitor.Exit(释放指定对象上的排他锁。)
2、实例认识
public static readonly object obj = new object();private static void Main(string[] args){int count = 0;for (int i = 0; i < 10; i++){Task.Run(() =>{Monitor.Enter(obj);try{Console.WriteLine(count);count++;}finally{Monitor.Exit(obj);}});}Console.WriteLine("任务完成.");Console.ReadKey();}
结果:
任务完成.
0
1
2
3
4
5
6
7
8
9
3、问:为什么改为Monitor.TryEnter输出的结果不对?
public static readonly object obj = new object();private static void Main(string[] args){int count = 0;for (int i = 0; i < 10; i++){Task.Run(() =>{if (Monitor.TryEnter(obj)){try{Console.WriteLine(count);count++;}finally{Monitor.Exit(obj);}}});}Console.WriteLine("任务完成.");Console.ReadKey();}
结果:
任务完成.
0
1
2
3
可以看到有些结果不见了。。。。
答:简言之,TryEnter是非阻塞,任务取不到锁,就向下执行了。而Enter是阻塞,取不到锁就阻塞别想走,真正起到锁的作用。
Monitor.TryEnter方法是一个非阻塞的方法。如果锁不可用,它会立即返回false,而不会等待锁可用。因此,如果一个任务尝试获取锁但失败了,它会跳过临界区的代码,继续执行后续的语句。
上面,由于多个任务几乎同时尝试获取锁,只有一个任务能够成功获取锁并进入临界区。其他任务由于锁不可用,会跳过临界区的代码,直接执行后续的语句。因此,你只看到了计数器的值(0-3),其它值的任务已经执行走了,无法再看到其他任务的计数器值。
如果想要确保所有任务都能够进入临界区,可以使用Monitor.Enter方法来获取锁。Monitor.Enter方法是一个阻塞的方法,如果锁不可用,它会等待直到锁可用为止。这样,每个任务都能够按顺序进入临界区,避免了竞态条件的问题。
4、问:什么是临界区?
答:临界区(Critical Section)是指一段代码或一块共享资源,在同一时间只能被一个线程访问的区域。
在临界区内部,线程可以对共享资源进行读取、写入或其他操作。临界区的目的是保护共享资源的一致性,避免多个线程同时访问共享资源导致的竞态条件和数据不一致性。
在多线程编程中,当多个线程同时访问共享资源时,如果没有适当的同步机制来保护临界区,就会发生竞态条件(Race Condition)。竞态条件可能导致不可预测的结果,如数据损坏、数据丢失、死锁等问题。
通过使用同步机制,如锁(Lock)或信号量(Semaphore),可以限制只有一个线程可以进入临界区。这样,当一个线程进入临界区时,其他线程必须等待,直到该线程退出临界区。这种同步机制确保了共享资源的一致性和正确性。
临界区的正确使用对于多线程编程的正确性至关重要。它可以确保线程安全,避免竞态条件和数据不一致性的问题。
5、Monitor类介绍
Monitor类用于实现线程同步和互斥的一个工具类。
它提供了一些方法来控制对共享资源的访问,以确保多个线程能够安全地访问共享资源。
(1) Enter:用于获取锁定对象,如果对象已经被其他线程锁定,则当前线程会被阻塞,直到锁定对象被释放。
(2)Exit:用于释放锁定对象,允许其他线程获取该对象的锁。
(3)TryEnter:尝试获取锁定对象,取得锁则返回true,失败(已经被其它锁定)则为false.
(4)Wait:使当前线程等待,直到其他线程通过调用Monitor.Pulse或Monitor.PulseAll方法唤醒它。
(5)Pulse:唤醒等待在锁定对象上的一个线程。
(6)PulseAll:唤醒等待在锁定对象上的所有线程。
注意,Monitor类是基于内核对象的,因此在使用时需要谨慎,避免出现死锁等问题。同时,应该尽量使用较小的锁定对象,以减少线程等待的时间和提高性能。
6、问:同步互斥时用lock还是Monitor?
答:推荐使用lock。
一般使用lock关键字来实现线程同步和互斥。lock关键字是基于Monitor类实现的,而Monitor类提供了更底层的线程同步功能。
使用lock关键字可以更简单地实现线程同步,它会自动获取和释放锁。在使用lock关键字时,需要传入一个对象作为锁定的对象,多个线程对于同一个锁定对象的lock操作会被互斥执行,保证了线程安全。
而Monitor类提供了更多底层的线程同步方法,可以手动调用Monitor.Enter和Monitor.Exit方法来实现锁定和释放锁。使用Monitor类可以更加灵活地控制线程同步,但也需要更多的手动操作。 因此,一般情况下推荐使用lock,它更简单、更易于使用,并且在性能上与Monitor类相当。只有在需要更高级的线程同步功能时,才需要使用Monitor类。
7、问:多个任务用Task还是Thread来实现?
比如:上面的Task改写成下面
public static readonly object obj = new object();private static void Main(string[] args){int count = 0;for (int i = 0; i < 10; i++){new Thread(() =>{Monitor.Enter(obj);try{Console.WriteLine(count);count++;}finally{Monitor.Exit(obj);}}).Start();}Console.WriteLine("任务完成.");Console.ReadKey();}
结果:
0
1
2
3
4
5
6
7
8
任务完成.
9
答:在C#中,多个任务执行的选择可以根据具体的需求来决定使用Thread还是Task。
(1) 如果任务是CPU密集型的,即需要大量的计算和处理,那么使用Thread可能更合适。
Thread是基于操作系统的线程,可以直接利用多核处理器的能力,同时也可以更细粒度地控制线程的执行。但是需要注意的是,使用Thread需要手动管理线程的生命周期和同步,需要更多的编码工作。
(2)如果任务是I/O密集型的,即涉及到大量的输入输出操作,那么使用Task可能更合适。
Task是基于线程池的任务调度机制,可以有效地利用线程资源,并且提供了更高级的任务管理和调度功能。Task可以通过使用异步和等待的方式,简化了编程模型,使得代码更易于编写和维护。
注意,Task是建立在Thread之上的,它是一种更高层次的抽象,可以更好地利用并发性能。在一般情况下,推荐使用Task来管理和调度多个任务的执行,因为它提供了更好的可扩展性和灵活性,同时也更符合现代异步编程的趋势。
总结,如果任务是CPU密集型的,可以考虑使用Thread;如果任务是I/O密集型的,或者对任务管理和调度有更高级的需求,可以考虑使用Task。
8、问:Task不一定开线程,即使开了线程,也不一定占用CPU核心?
答:对的。
Thread是基于操作系统的线程,它直接占用一个CPU核心并执行任务。每个Thread都有自己的堆栈和上下文,因此可以直接利用多核处理器的能力。
而Task则是基于线程池的任务调度机制。线程池是一组预先创建好的线程,这些线程可以被多个任务共享。当任务需要执行时,线程池会从池中选择一个空闲的线程来执行任务。这种方式可以避免频繁地创建和销毁线程,提高了性能和资源利用率。
这也是为什么Thread适合CPU跑,而Task适合I/O跑。
9、问:线程不一定占用CPU核心?
答:是的。线程与Thread是有区别的.
线程池中的线程并不一定会直接占用CPU核心。线程池是一种预先创建好的线程集合,这些线程可以被多个任务共享。当有任务需要执行时,线程池会从池中选择一个空闲的线程来执行任务。线程池的目的是为了提高性能和资源利用率,避免频繁地创建和销毁线程。
线程池中的线程会被操作系统调度到可用的CPU核心上执行任务。具体来说,操作系统会根据当前的系统负载情况,决定将线程调度到哪个CPU核心上执行。这样可以充分利用多核处理器的能力,提高并发性能。
注意:线程池的线程并不是一定会占用CPU核心。
如果任务是I/O密集型的,即涉及到大量的输入输出操作,线程可能会处于等待状态,不会占用CPU核心。而当有其他任务需要执行时,线程池会将等待的线程唤醒并分配任务给它们。
总结:线程池中的线程会根据系统负载情况被调度到可用的CPU核心上执行任务,以提高并发性能。但并不是所有线程都会一直占用CPU核心,具体是否占用取决于任务的类型和当前的系统负载情况。
10、问:Wait与Pulse(PulseAll)是冤家对头吗?
答:是的。wait是深度睡眠的等待,pulse中唤醒。
正如医院叫号一样,wait如坐在椅子上的病人,深度睡眠,不能主动去就诊。当用pulse进行叫号,23号!于是23号就唤醒,去就诊,但不一定医生会成功给他看病,但他有机会成功,比如同时唤醒了3个人就诊,只能有一个人成功,剩下的2人就会自动进入wait(深度睡眠,需要再次被唤醒)。
在使用Monitor控制线程同步时,线程可以处于以下三个状态之一:
(1)运行状态(Running):线程正在执行其任务代码。
(2)等待状态(Waiting):线程调用了Monitor.Wait方法,释放了锁并进入等待状态,直到其他线程调用了相同对象上的Monitor.Pulse或Monitor.PulseAll方法来唤醒它。
(3)阻塞状态(Blocked):线程尝试获取锁,但锁已被其他线程占用,因此线程被阻塞,等待锁的释放。
这些状态是Monitor控制的一种常见模式,用于实现线程间的通信和同步。通过Monitor.Wait和Monitor.Pulse方法的配合使用,可以实现线程的等待和唤醒,以及线程间的协调和同步。
问:阻塞也就是等待了,而且等待并非一定在临界区??
答:是的。实际上两者意思相同,只是表达的角度。等待是对外面的线程而言,阻塞是对自身而言。
无论是在Monitor中的等待状态还是阻塞状态,都表示线程暂停执行,直到某种条件满足。这种机制可以用来实现线程之间的协调和同步,确保线程在合适的时机进行操作,避免竞争条件和资源争用的问题。
在Monitor中,线程可以在临界区之前、临界区门口或临界区内部调用Wait方法进行等待阻塞。Wait方法的调用会释放当前线程持有的锁定,并使线程进入等待状态,直到其他线程调用Monitor的Pulse或PulseAll方法来唤醒它。
无论线程在临界区的哪个位置调用Wait方法,它都会进入等待状态,并且在被唤醒后需要重新获取锁定才能继续执行。这种等待阻塞的机制可以用来实现线程之间的协调和同步,确保线程在合适的时机进行操作,避免竞争条件和资源争用的问题。
详细过程(了解):
线程在使用Monitor控制线程同步时,可以从运行状态转变为等待状态,也可以从等待状态转变为运行状态。
(1)运行状态(Running)转变为等待状态(Waiting):
当线程调用Monitor.Wait方法时,它会释放锁并进入等待状态。线程会等待其他线程调用相同对象上的Monitor.Pulse或Monitor.PulseAll方法来唤醒它。
(2)等待状态(Waiting)转变为运行状态(Running):
当其他线程调用相同对象上的Monitor.Pulse方法时,等待的线程将被唤醒,并尝试重新获取锁。一旦获取到锁,线程将从等待状态转变为运行状态,继续执行。
当其他线程调用相同对象上的Monitor.PulseAll方法时,所有等待的线程都会被唤醒,并竞争获取锁。只有一个线程能够获取到锁并进入运行状态,其他线程将继续等待。
另外,线程也可以从等待状态转变为阻塞状态(Blocked):
当线程调用Monitor.Wait方法后,它会释放锁并进入等待状态。如果此时其他线程已经获取了锁并且没有释放锁,那么等待的线程将无法获取锁,进而被阻塞。线程将一直处于阻塞状态,直到获取到锁并进入运行状态,或者被中断(即其他线程调用了该线程的Interrupt方法)。
11、wait/pulse实例
public static readonly object obj = new object();private static void Main(string[] args){Thread thA = new Thread(MyMethod);Thread thB = new Thread(MyMethod);thA.Start();thB.Start();Thread.Sleep(1000);//alock (obj){Monitor.Pulse(obj);//b}thA.Join();//cthB.Join();Console.ReadKey();}private static void MyMethod(){Console.WriteLine($"{Environment.CurrentManagedThreadId}方法开始");lock (obj){Monitor.Wait(obj);//d}Console.WriteLine($"{Environment.CurrentManagedThreadId}方法结束");}
大概意思就是,启动AB线程,但都等待wait,然后再唤醒,逐个完成。
结果是:
4方法开始
3方法开始
4方法结束
上面每步都很紧凑。
d处让每一个线程进入临界区后,马上释放锁进入等待状态(阻塞),这样A和B线程都在阻塞等待状态(一直等待别人的唤醒)
a处的睡眠的目的就是让AB都在等待状态,因为两个线程的执行大约小于10毫秒,但为了确信两都处于等待,所以这里设置了1000毫秒。
b处为什么又能进入临界区呢?因为d处AB两者进入后释放了锁,并处于等待,因为锁其实没有人在用,所以1000毫秒后,锁又可以用了,这时的pulse为什么必须放在锁内呢?起到保险的作用,万一AB都还没有从锁里出来,唤醒就起不到作用,直接一闪而过,用上锁,表示的先后顺序,AB锁上了又释放后,在b处就再上锁再唤醒。次序就不会错乱。
c并不知道是哪个完成了,所以把两个子线程join加入到当前线程(主线程)进行等待,直到子线程完成后,主线程才继续向下执行。如果AB已经完成,再用join也不会报错,子线程会立即返回,主线程不用再等待直接向下执行。
问:为什么上面的结果少了一个3结束?
答:因为程序有bug,3已经死锁了。
刚开始AB即3与4都进入wait等待状态,经b后只能唤醒一个,上面唤醒的是4,所以4结束。而3没有唤醒,所以3还是wait,主程序中用join等待子线程3的完成,而3还在等待主线程的唤醒,两个相互等待形成死锁。
所以还需要再唤醒剩下的等待的线程一次:
Thread thA = new Thread(MyMethod);Thread thB = new Thread(MyMethod);thA.Start();thB.Start();Thread.Sleep(1000);//alock (obj){Monitor.Pulse(obj);//b}lock (obj){Monitor.Pulse(obj);//f}Console.ReadKey();
12、经典的动态平衡
工厂生产产品,只能存储5批货物,满5批就停产等待消费,少于5批就生产。
销售消费每批产品,只要有就消费,没有就只有等工厂生产。
public static readonly object obj = new object();public static Queue<int> buffer = new Queue<int>();//队列排队,先进先出public static int maxSize = 5;//最大仓库private static void Main(string[] args){Thread producerThread = new Thread(Producer);Thread consumerThread = new Thread(Consumer);producerThread.Start();consumerThread.Start();producerThread.Join();consumerThread.Join();Console.ReadKey();}private static void Producer()//工厂{Random r = new Random();while (true){lock (obj){if (buffer.Count >= maxSize){Console.WriteLine("仓库已满,工厂等待消费...");Monitor.Wait(obj);}int item = r.Next(100);buffer.Enqueue(item);Console.WriteLine($"工厂生成出一批产品:{item}个===========仓库共{buffer.Count}批.");Monitor.PulseAll(obj);}Thread.Sleep(r.Next(1000));}}private static void Consumer()//消费{Random r = new Random();while (true){lock (obj){if (buffer.Count == 0){Console.WriteLine("消费完毕,等待生产...");Monitor.Wait(obj);}int item = buffer.Dequeue();Console.WriteLine($"消费了一批产品:{item}个");Monitor.PulseAll(obj);}Thread.Sleep(r.Next(1000));}}
程序会一直执行下去,维持着仓库5批货物的标准,如同现在工厂一样,生产与销售同时进行。
结果:
可以看到,满5就停止生产。
但为什么有些满5没有停止生产?
因为此时正好消费者消费了一批,到了工厂lock生产时,就变成了4于是它就会再生产。
相关文章:
(20)线程安全问题:Lock,双锁问题,Monitor,死锁
一、Lock 1、用多线程给变量自增,10000个线程自增 List<Task> tasks new List<Task>();int AsyncNum 0;for (int i 0; i < 10000; i){tasks.Add(Task.Run(() >{AsyncNum;}));}Task.WaitAll(tasks.ToArray());Console.WriteLine($"AsyncNu…...
医院如何实现安全又稳定的跨网文件数据交换呢?
随着医疗信息化的发展,医院之间需要频繁地进行文件数据交换,以实现诊疗、科研、管理等方面的协同和共享。然而,由于医院网络环境的复杂性和敏感性,跨网文件数据交换面临着安全性和稳定性的双重挑战。如何在保证文件数据不被泄露、…...
关于老项目从JDK8升级到JDK17所需要注意的细节
文章目录 ☀️1.关于老项目从JDK8升级到JDK17所需要注意的细节🌸1.1.更新JDK🌸1.2.修改Idea中的JDK版本🌸1.3.关于修改过程中遇到的异常🌸1.4.IDEA工具栏操作Maven正常,但使用mvn命令运行就报错 ☀️1.关于老项目从JDK…...
《C++ primer》练习3.43-3.45: 打印二维数组的元素
文章目录 1. 使用范围for循环2. 使用普通for循环2.1 使用指针2.2 使用数组下标 类型别名的简化 本文来自于《C primer》的练习3.43-3.45,觉得多维数组的遍历有不同的实现方式,于是记录一下。写的可能没有按题目的顺序来。题目大概含义是定义了一个二维数…...
使用电力系统稳定器 (PSS) 和静态 VAR 补偿器 (SVC) 提高瞬态稳定性(Matlab代码实现)
💥💥💞💞欢迎来到本博客❤️❤️💥💥 🏆博主优势:🌞🌞🌞博客内容尽量做到思维缜密,逻辑清晰,为了方便读者。 ⛳️座右铭&a…...
开源项目-SeaTunnel-UI数据集成系统
哈喽,大家好,今天给大家带来一个开源项目-SeaTunnel-UI数据集成系统 系统主要有任务配置,任务实例,数据源,虚拟表,用户管理等功能 登录 数据源 mysql数据源配置参数说明 kafka参数配置 mysqlcdc配置参数说明 虚拟表...
百度SEO优化策略与经验分享(提升百度排名的8大步骤)
百度关键词优化策略介绍:蘑菇号https://www.mooogu.cn/ 百度搜索引擎优化,简称为百度SEO,是一种通过优化网站结构和内容,提高网站在百度搜索引擎中的排名,从而获得更多有价值的流量和销售机会的行业术语。百度SEO的核…...
【深度学习】- NLP系列文章之 1.文本表示以及mlp来处理分类问题
系列文章目录 1. 文本分类与词嵌入表示,mlp来处理分类问题 2. RNN、LSTM、GRU三种方式处理文本分类问题 3. 评论情绪分类 还是得开个坑,最近搞论文,使用lstm做的ssd的cache prefetching,意味着我不能再划水了。 文章目录 系列文章…...
力扣236 补9.14
做不来,我做中等题基本上都是没有思路,这里需要先遍历祖先节点,那必然用先序遍历,这题还是官方题解容易理解,第二火的题解反而把我弄得脑袋昏昏的。 class Solution { TreeNode ans; public TreeNode lowestCommonAnce…...
一文搞定Postman(菜鸟必看)
什么是Postman? Postman是一个可扩展的 API 测试工具,可以快速集成到 CI/CD 管道中。它于 2012 年作为 Abhinav Asthana 的一个副项目启动,旨在简化测试和开发中的 API 工作流程。API 代表应用程序编程接口,它允许软件应用程序通…...
位图+布隆过滤器+海量数据并查集(它们都是哈希的应用)
一)位图: 首先计算一下存储一下10亿个整形数据,需要多大内存呢,多少个G呢? 2^3010亿,10亿个字节 byte kb mb gb 100000000个字节/1024/1024/10241G 所以10亿个字节就是1G,所以40亿个字节就是4G,也就是10个整…...
MYSQL:Select语句顺序
SELECT子句及其顺序整理表格: 子句 说明是否必须使用SELECT 要返回的列或表达式是FROM 从中检索数据的表仅在从表选择数据使用WHERE 行级过滤否GROUP BY 分组说明仅在按组计算聚…...
Pytest系列-数据驱动@pytest.mark.parametrize(7)
简介 unittest 和 pytest参数化对比: pytest与unittest的一个重要区别就是参数化,unittest框架使用的第三方库ddt来参数化的 而pytest框架: 前置/后置处理函数fixture,它有个参数params专门与request结合使用来传递参数&#x…...
【Qt】QGroundControl入门2:下载、编译、错误处理、运行
1、源码下载 git clone https://github.com/mavlink/qgroundcontrol.git 2、下载依赖库 2.1 查看依赖库的github路径 cat .gitmodules[submodule "src/GPS/Drivers"]path = src/GPS/Driversurl = https://github.com/PX4/GpsDrivers.git [submodule "libs/m…...
【深度学习】Pytorch 系列教程(十):PyTorch数据结构:2、张量操作(Tensor Operations):(4)索引和切片详解
目录 一、前言 二、实验环境 三、PyTorch数据结构 0、分类 1、张量(Tensor) 2、张量操作(Tensor Operations) 1. 数学运算 2. 统计计算 3. 张量变形 4. 索引和切片 使用索引访问单个元素 使用切片访问子集 使用索引和…...
2024字节跳动校招面试真题汇总及其解答(三)
6.jwt与cookie区别 JWT 和 Cookie 都是用于在客户端和服务器之间传输信息的常用方法。但是,它们之间存在一些关键差异。 JWT 是 JSON Web Token 的缩写,它是一种基于 JSON 的加密令牌。JWT 由三部分组成:Header、Payload 和 Signature。Header 包含令牌的类型、加密算法和…...
基于springboot+vue的便利店信息管理系统
博主主页:猫头鹰源码 博主简介:Java领域优质创作者、CSDN博客专家、公司架构师、全网粉丝5万、专注Java技术领域和毕业设计项目实战 主要内容:毕业设计(Javaweb项目|小程序等)、简历模板、学习资料、面试题库、技术咨询 文末联系获取 项目介绍…...
在ubuntu18.04上编译C++版本jsoncpp/opencv/onnxruntime且如何配置CMakelist把他们用起来~
这篇文章背景是笔者在ubuntu上编译C代码,依赖一些包,然后需要编译并配置到CMakelist做的笔记。主要也是一直不太懂CMakellist,做个笔记以防忘记,也给读者提供一站式的参考,可能您需要的不是这几个包,但大同…...
大二上学期学习计划
这个学期主要学习的技术有SpringBoot,Vue,MybatisPlus,redis,还有要坚持刷题,算法不能落下,要坚持一天至少刷2道题目,如果没有布置任务就刷洛谷上面的,有任务的话就尽量完成任务&…...
【python爬虫—星巴克产品】
文章目录 需求爬取星巴克产品以及图片,星巴克菜单 python爬虫爬取结果 需求 爬取星巴克产品以及图片,星巴克菜单 网页分析: 首先,需要分析星巴克官方网站的结构,了解菜单栏的位置、布局以及菜单项的标签或类名等信息…...
shell SQL 变量 Oracle shell调用SQL操作DB
注意 : v\\\$ 用法, “v\\\$session ” ""不能用 sqlplus -S / as sysdba << EOF set pagesize 0 set verify off set feedback off set echo off col coun new_value v_coun select count(*) coun from dual; EOF value"$?"VALUE…...
【校招VIP】java线程池考点之核心线程数
考点介绍: 线程池是这一两年java大厂提问频度飙升的考点,需要从池子的概念理解相关参数和方法 java线程池考点之核心线程数-相关题目及解析内容可点击文章末尾链接查看! 一、考点试题 1、请列举一下启动线程有哪几种方式,之后再…...
[每周一更]-(第61期):Rust入门策略(持续更新)
一门语言的学习,就要从最基本的语法开始认识,再分析不同语言的区别,再加上实战,才能更快的学会,领悟到作者的设计思想; 介绍 Rust编程练习 开发工具VSCode及插件 社区驱动的 rust-analyzerEven Better T…...
线程安全问题的原因及解决方案
要想知道线程安全问题的原因及解决方案,首先得知道什么是线程安全,想给出一个线程安全的确切定义是复杂的,但我们可以这样认为:如果多线程环境下代码运行的结果是符合我们预期的,即在单线程环境应该的结果,…...
基于matlab中点放炮各类地震波时距曲线程序
完整程序: clear all dx50;x-500:dx:500;%炮检距 h100;V11500; theta25*pi/180; V2V1/sin(theta); t1sqrt(x.*x4*h*h)/V1;%反射波时距曲线 t2abs(x)./V1;%直达波时距曲线 %折射波时距曲线 xm2*h*tan(theta);%求盲区 k1; for i1:length(x) if x(i)<-xm …...
vue中el-dialog 中的内容没有预先加载,因此无法获得内部元素的ref 的解决方案 使用强制提前加载dialog方法
问题描述 在没有进行任何操作的时候,使用 this.$refs.xxxx 无法获取el-dialog中的内部元素,这个问题会导致很多bug,其中目前网络上也有许多关于这个问题的解决方案,但是大多数是使用el-dialog中的open在dialog打开的时候使用thi…...
vue-h5移动Web的rem配置
H5移动的适配方案 rem rem适配方案是兼容性比较好的移动端适配方案,rem支持大部分的移动端系统和机型。 rem是相对于根元素的字体大小的单位。本质上就是一个相对单位,和em的区别是:em是依赖父元素的字体来计算,rem是依赖根元素…...
企业级数据仓库-数仓实战
数仓实战 安装包大小 安装清单 环境搭建 一、环境搭建01(机器准备) 准备好三台虚拟机,并进行修改hostname、在hosts文件增加ip地址和主机名映射 。 1、设置每个虚拟机的hostname vi /etc/sysconfig/network 修改HOSTNAMEnode02修改hostna…...
Spring Boot 下载文件(word/excel等)文件名中文乱码问题|构建打包不存在模版文件(templates等)
Spring Boot 下载文件(word/excel等)文件名中文乱码问题|构建打包不存在模版文件(templates等) 准备文件,这里我放在resource下的templates路径 在pom中配置构建打包的资源,更新maven 如果使用了assembly打包插件这样配置可能仍不生效&#…...
Ansible数组同步至Shell脚本数组中
1、ansible中定义数组,我以 ccaPojectList 数组为例子,如下图数组内容 2、需要写一个j2模板的Shell脚本,在j2模板的Shell脚本中引用ansible的 ccaPojectList 数组,大致如下图: {% for item in ccaPojectList %} "{{ item }…...
面试网站开发员/优化大师win10能用吗
偶尔想在宿舍使用下VCS做些模块,从EETOP上下载了2009.12 MX版本的vcs,在自己vmware (X64) (Ubuntu 2.6.38-8-generic (32bit))下安装一路出现了问题首先:在进行安装时出现失败查找install.log出现如下错误chmod: cannot access var: No such…...
加强网站建设 基本措施/搜索引擎优化的内部优化
9.9 NOIP模拟题 T1 两个圆的面积求并 /* 计算圆的面积并 多个圆要用辛普森积分解决 这里只有两个,模拟计算就好 两圆相交时,面积并等于中间两个扇形面积减去两个三角形面积 余弦定理求角度,算出三角形面积 */ #include<cstdio> #inclu…...
做企业平台的网站有哪些/韩国电视剧
本文档介绍在Android下如何查看自己的应用签名及三方APK或系统APK签名信息,包含其中的MD5、SHA1、SHA256值和签名算法等信息。 1、查看自己的应用签名 可以通过两种方式查看 (1) 通过Eclipse查看默认的default.keystore,如下图: (2) 通过某个…...
响应式网站做多大的尺寸/seo推广经验
自定义三维图表绘制 柱状图 环状图 三叶草图 这里仅给出环状图用作统计亚洲航班执航飞机制造公司统计的截图 根据相机高度,自动调节统计框及统计图大小。...
1如何做网站推广/html静态网页制作
10月10日,复旦大学Google Camp正式成立,李开复教授在成立仪式上发表了《21世纪所需要的7种人才》演讲,并回答了复旦同学7个“犀利”的问题,我们将在近期依次整理出这7个问答。一.开复如何评价谷歌的竞争对手——百度。复旦同学&am…...
ui设计的软件/宁波seo公司网站推广
各位医学方的朋友,大家好。我是Flyman!做过下游分析的小伙伴都知道富集分析的重要性,生信类文章大家总会在最后一步针对我们前面筛选出来的差异基因做一下GO/KEGG富集分析,研究一下他们参与到什么信号通路上或者参与什么生物学过程…...