C# 异步多线程
线程的基本使用
线程创建
通过ThreadStart创建
public class ThreadSample
{
public static void CreateThread()
{
Console.WriteLine($"主线程Id:{Thread.CurrentThread.ManagedThreadId}");
var thread = new Thread(BusinessProcess);
thread.Start();
}
//线程1
public static void BusinessProcess()
{
Console.WriteLine($"BusinessProcess 线程Id:{Thread.CurrentThread.ManagedThreadId}");
Console.WriteLine("开始处理业务……");
//业务实现
Console.WriteLine("结束处理业务……");
}
}
c#
通过ParameterizedThreadStart带参创建
public static void CreateThreadParameterized()
{
Console.WriteLine($"主线程Id:{Thread.CurrentThread.ManagedThreadId}");
var thread = new Thread(BusinessProcessParameterized);
//传入参数
thread.Start("Hello World!");
}
//带参业务线程
public static void BusinessProcessParameterized(object? param)
{
Console.WriteLine($"BusinessProcess 线程Id:{Thread.CurrentThread.ManagedThreadId}");
Console.WriteLine($"参数 param 为:{param}");
Console.WriteLine("开始处理业务……");
//业务实现
Console.WriteLine("结束处理业务……");
}
c#
通过表达式创建
internal class Program
{
public static void CreateThreadLambdaParameterized()
{
Console.WriteLine($"主线程Id:{Thread.CurrentThread.ManagedThreadId}");
int param = 11;
var thread1 = new Thread(() => BusinessProcessParameterized(param));
thread1.Start();
param = 22;///
}
///
/// 带参业务线程
///
///
public static void BusinessProcessParameterized(int param)
{
Console.WriteLine($"业务线程Id:{Thread.CurrentThread.ManagedThreadId}");
Console.WriteLine($"参数 param 为:{param}");
}
///
/// 不传参业务线程
///
public static void CreateThreadLambda()
{
Console.WriteLine($"主线程Id:{Thread.CurrentThread.ManagedThreadId}");
var thread = new Thread(() =>
{
Console.WriteLine($"业务线程Id:{Thread.CurrentThread.ManagedThreadId}");
Console.WriteLine("开始处理业务……");
//业务实现
Console.WriteLine("结束处理业务……");
});
//传入参数
thread.Start();
}
private static void Main(string[] args)
{
CreateThreadLambda();
CreateThreadLambdaParameterized();
}
}
c#
表达式中使用任何外部局部变量时,编译器会自动生成一个类,并将该变量作为该类的一个属性。因此这些外部变量并不是存储在栈中,而是通过引用存储在堆中,因此此时param参数实际上在内存中是一个类是一个引用类型,所以两个线程中使用的param都指向了堆中的同一个值。
并且使用Lambda表达式引用另一个C#对象的方式有个专有名词叫闭包。感兴趣的可以去了解下闭包概念。
异常处理
internal class Program
{
//业务线程不处理异常,直接抛出
public static void ThreadThrowException()
{
Console.WriteLine($"业务线程Id:{Thread.CurrentThread.ManagedThreadId}");
Console.WriteLine("开始处理业务……");
//业务实现
Console.WriteLine("结束处理业务……");
throw new Exception("异常");
}
private static void Main(string[] args)
{
Console.WriteLine($"主线程Id:{Thread.CurrentThread.ManagedThreadId}");
try
{
var thread = new Thread(ThreadThrowException);
thread.Start();
}
catch (Exception ex)
{
Console.WriteLine("子线程异常信息:" + ex.Message);
}
}
}
c#
可以看到主线程中并没有捕捉到子线程抛出的异常,而导致程序直接中断。因此我们在处理线程异常时需要特别注意,可以直接在线程中处理异常。
前台线程和后台线程
线程可以分为前台线程和后台线程,两者基本完全相同,唯一区别是前台线程可以在托管执行环境中一直运行,而后台线程不可以。简单来说就是当进程中所有前台进程都停止后,系统会自动停止并关闭该进程内的所有后台线程。
static void Main(string[] args)
{
Console.WriteLine($"主线程 是否为后台线程:{Thread.CurrentThread.IsBackground}");
var thread1 = new Thread(() => Console.WriteLine("Hello World"));
Console.WriteLine($" 线程1 默认为后台线程:{thread1.IsBackground}");
thread1.IsBackground = true;
Console.WriteLine($" 线程1 设置为后台线程:{thread1.IsBackground}");
thread1.Start();
Console.ReadKey();
}
打印输出:
主线程 是否为后台线程:False
线程1 默认为后台线程:False
线程1 设置为后台线程:True
Hello World
c#
线程的优先级
线程作为操作系统中能够单独执行任务的最小单元,那么当一个进程中有多个线程时,应该先执行那个线程呢?因此线程需要一个标记其执行优先级的属性。
在C#中Thread可以通过Priority来设置线程的优先级,告诉系统应该先执行谁。ThreadPriority有以下5种类型:
- Lowest: 最低优先级,在所有优先级中最低,在所有线程中可位于最后执行。
- BelowNormal: 低于正常优先级,在Normal优先级之后,在Lowest优先级之前。
- Normal: 默认优先级,线程默认的优先级
- AboveNormal: 高于正常优先级,在Highest优先级的线程之后,在Normal优先级之前。
- Highest: 最高优先级,在所有优先级中最高,在所有线程中可优先执行。
internal class ThreadPriorityTest
{
//是否执行,确保一个线程修改此值后,其他线程立刻查看到最新值
private static volatile bool isRun = true;
//确保每个线程都有独立的副本存储计数统计值
[ThreadStatic]
private static long threadCount;
//停止运行
public void Stop()
{
isRun = false;
}
//打印线程名称对应优先级以及计数总数
public void Print()
{
threadCount = 0;
while (isRun)
{
threadCount++;
}
Console.WriteLine($"{Thread.CurrentThread.Name} 优先级为{Thread.CurrentThread.Priority,8} 总执行计数为:{threadCount,-13:N0}");
}
}
internal class Program
{
private static void Main(string[] args)
{
var threadPriorityTest = new ThreadPriorityTest();
//创建3个线程,并设置优先级
var thread1 = new Thread(threadPriorityTest.Print)
{
Name = "线程1"
};
var thread2 = new Thread(threadPriorityTest.Print)
{
Name = "线程2",
Priority = ThreadPriority.Lowest
};
var thread3 = new Thread(threadPriorityTest.Print)
{
Name = "线程3",
Priority = ThreadPriority.Highest
};
//启动3个线程
thread1.Start();
thread2.Start();
thread3.Start();
//休眠3秒
Thread.Sleep(10000);
//停止运行
threadPriorityTest.Stop();
//等待所有线程完成
thread1.Join();
thread2.Join();
thread3.Join();
}
}
打印输出:
线程3 优先级为 Highest 总执行计数为:204,570,680
线程1 优先级为 Normal 总执行计数为:244,829,075
线程2 优先级为 Lowest 总执行计数为:226,351,418
c#
可以发现优先级越高,其执行计数值越大。
其中需要注意的是volatile和ThreadStatic的用法。
在这个多线程示例中我们需要准确的统计不同的线程执行计数,因此正常来说可能需要设置多个变量用来对应存储各自线程的统计计数,很显然这样会导致代码臃肿。因此我们选用了另一种办法,使用ThreadStatic标记一个字段,使得该字段对每个线程都有独立的副本。这样可以做到线程之间不会共享这个字段的值,同时还可以做到多个线程只用这一个字段。
另外对于多线程共享的变量,很可能因为CPU缓存导致多个线程共享的变量不一致问题,因此通过volatile告诉编译器和运行时每次访问该字段时都要直接从内存中读取最新值,以此来保证线程之间的可见性。
线程的生命周期
当一个线程被创建后,会经历多个状态,包括未启动、已启动、执行、睡眠、挂起等十个状态,同时Thread类也提供了一些方法,用来控制当前线程状态,比如启动、停止、恢复、中止、挂起以及等待线程等方法。
- Running(运行)—— 线程已启动,而且没有停止;
- StopRequested(请求停止) —— 请求停止线程;
- SuspendRequested(请求挂起) —— 请求线程挂起;
- Background(后台) —— 线程在后台执行;
- Unstarted(未启动) —— 还没有在线程上调用 Start()方法;
- Stopped(停止) —— 线程已完成了其所有的指令,而且已经停止;
- WaitSleepJoin(等待睡眠连接) —— 通过调用 Wait()、Sleep()或 Join()方法,来暂停线程;
- Suspended(挂起) —— 线程处于挂起状态;
- AbortRequested(请求中止) —— Abort()方法已调用,但是线程还没有收到试图终止自己System.Threading.ThreadAbortexception,也就是说,线程还没有停止但不久就会停止;
- Aborted**(中止) —— 线程处于停止状态,但不一定已执行完毕;
线程的常用方法
- Start(): 启动线程,使其状态变更为Running。
- Sleep(): 把正在运行的线程暂停一段时间后自动恢复,线程状态保持活跃。
- Suspend():[已弃用]暂停当前线程的执行,直到调用 Thread.Resume 显式恢复。
- Resume():[已弃用]恢复一个已被暂停的线程。
- Interrupt(): 中断处于 WaitSleepJoin 线程状态的线程。
- Join(): 阻塞调用线程,直到某个线程终止时为止。
- Abort():[已弃用]终止当前线程。
通过源码可以看到Resume和Suspend方法被弃用的原因。这是因为它们有很多问题和缺陷。使用它可能会导致程序的不稳定、死锁或者资源竞争问题。因此,它已经被标记为废弃,不推荐再使用。
在多线程编程中,通常可以通过合理的同步机制来控制线程的执行。比如,使用上述的 Monitor、Mutex、Event 和 Semaphore 来协调多个线程的行为,确保资源访问的安全和正确性。
ThreadPool的基本使用
class Program
{
static void Main(string[] args)
{
///线程池本身都是后台线程
///线程池的线程可以重用
///
//线程池 vs 手动创建线程
//启动一个线程:开辟一块内存控件
//线程非常多的时候,操作系统花销大量的时间切换线程
//ThreadPool.QueueUserWorkItem
// (
// (num)=>
// {
// for (int i = 1; i <=(int)num; i++)
// {
// Console.WriteLine(i);
// }
// },100
// );
//Console.ReadKey();
///手动创建线程与线程池比较
#region
///手动创建线程
//Stopwatch stopwatch = new Stopwatch();
//stopwatch.Start();
//for (int i = 0; i < 100; i++)
//{
// Thread thread = new Thread
// (
// ()=>
// {
// for (int i = 0; i < 100; i++)
// {
// }
// }
// );
// thread.Start();
//}
//stopwatch.Stop();
//Console.WriteLine(stopwatch.Elapsed.TotalMilliseconds);
/////线程池
//stopwatch.Restart();
//for (int i = 0; i < 10000; i++)
//{
// ThreadPool.QueueUserWorkItem
// (
// (num)=>
// {
// for (int i = 0; i < 100; i++)
// {
// }
// //输出当前的线程的Id
// Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
// Thread.Sleep(1000);
// },"无效参数"
// );
//}
//stopwatch.Stop();
//Console.WriteLine(stopwatch.Elapsed.TotalSeconds);
//Console.ReadKey();
#endregion
//获取线程池最大的线程数
int numMax;
//实际运行时的线程数
int runNumMax;
ThreadPool.GetMaxThreads(out numMax, out runNumMax);
Console.WriteLine(numMax+" "+runNumMax);
}
}
线程安全
原子性
- 定义:一个操作是否可以被中断。如果操作是原子的,则它在执行过程中不可分割。
- 问题:某些操作看似简单(如 count++),但实际由多步完成(读取值、修改值、写回值),可能被线程切换打断,导致结果错误。
- 示例:
int count = 0;
public void increment() {
count++; // 非原子操作,可能被线程中断
}
c#
可见性
- **定义:**一个线程对共享变量的修改,是否能立即被其他线程感知。
- **问题:**线程可能将变量缓存到本地(如 CPU 缓存或寄存器),导致其他线程读取到旧值。
示例:
bool running = true;
Thread worker = new Thread(() -> {
while (running) { /* 可能读取到缓存的旧值 */ }
});
// 主线程修改 running 为 false,但工作线程可能未感知到变化。
c#
有序性
- **定义:**程序执行的顺序是否与代码的顺序一致。
- **问题:**编译器或 CPU 可能对指令进行重排序优化,导致多线程环境下执行顺序混乱。
示例:
int a = 0;
boolean flag = false;
// 线程1:a = 1; flag = true;
// 线程2:if (flag) { Console.WriteLine(a); }
// 由于重排序,线程2可能读到 a=0。
c#
线程安全:多线程环境下程序的最终结果正确性
避免资源共享
比如值类型在传递过程中总是被复制,每个线程都会有自己的数据副本,比如看下面这个方法:
public static int Max(int val1, int val2)
{
return val1 > val2 ? val1 : val2;
}
c#
即使这个方法没有使用任何线程同步方法,这个方法也是线程安全的。因为值类型特性原因,所以传给Max的两个int值会复制到方法内部,形成自己的数据副本。此时无论有多少个线程调用Max方法,每个线程处理的都是它自己的数据,线程之间并不会互相干扰。
ThreadStatic(不推荐使用)
ThreadStatic特性可以实现线程本地存储,使得每个线程都有一个独立的字段副本。从而避免不同线程间共享资源。
使用ThreadStatic时需要注意以下几点:
1、ThreadStatic仅能作用于静态字段;。
2、ThreadStatic字段不应使用内联初始化。
3、每个线程都会有独立的_threadLocalVariable实例,当线程退出时,相关的线程本地存储会被清除。
4、由于 ThreadStatic 是线程局部存储,它并不是跨线程共享数据的解决方案。
应用在值类型上:
internal class Program
{
[ThreadStatic]
public static int _threadStaticValue = 1;
static void ThreadStatic1()
{
Console.WriteLine($"线程 Id : {Environment.CurrentManagedThreadId},变量值:{_threadStaticValue}");
}
static void ThreadStatic2()
{
Console.WriteLine($"线程 Id : {Environment.CurrentManagedThreadId},变量值:{_threadStaticValue}");
}
static void ThreadStatic3()
{
Console.WriteLine($"线程 Id : {Environment.CurrentManagedThreadId},变量值:{_threadStaticValue}");
}
private static void Main(string[] args)
{
var thread1 = new Thread(ThreadStatic1);
var thread2 = new Thread(ThreadStatic2);
var thread3 = new Thread(ThreadStatic3);
thread1.Start();
thread2.Start();
thread3.Start();
}
//打印输出
线程 Id : 7,变量值:0
线程 Id : 9,变量值:1
线程 Id : 8,变量值:0
}
C#
应用在引用类型上
internal class Program
{
[ThreadStatic]
public static string _threadStaticValue = "11";
static void ThreadStatic1()
{
Console.WriteLine($"线程 Id : {Environment.CurrentManagedThreadId},变量值:{_threadStaticValue}");
}
static void ThreadStatic2()
{
Console.WriteLine($"线程 Id : {Environment.CurrentManagedThreadId},变量值:{_threadStaticValue}");
}
static void ThreadStatic3()
{
Console.WriteLine($"线程 Id : {Environment.CurrentManagedThreadId},变量值:{_threadStaticValue}");
}
private static void Main(string[] args)
{
var thread1 = new Thread(ThreadStatic1);
var thread2 = new Thread(ThreadStatic2);
var thread3 = new Thread(ThreadStatic3);
thread1.Start();
thread2.Start();
thread3.Start();
}
//打印输出
线程 Id : 8,变量值:11
线程 Id : 9,变量值:
线程 Id : 7,变量值:
}
c#
因此注意项第二点提出ThreadStatic字段不应使用内联初始化,因为这样并不能保证每个线程都能获取到相同的初始值。
也因为ThreadStatic有这个缺陷所以引出了ThreadLocal。
ThreadLocal
可以说ThreadLocal功能和ThreadStatic完全一样,并且还解决了其缺陷,因此更推荐使用ThreadLocal。
应用在值类型上
internal class Program
{
private static ThreadLocal _threadLocalValue = new ThreadLocal(() => 1);
static void ThreadStatic1()
{
Console.WriteLine($"线程 Id : {Environment.CurrentManagedThreadId},变量值:{_threadLocalValue}");
}
static void ThreadStatic2()
{
Console.WriteLine($"线程 Id : {Environment.CurrentManagedThreadId},变量值:{_threadLocalValue}");
}
static void ThreadStatic3()
{
Console.WriteLine($"线程 Id : {Environment.CurrentManagedThreadId},变量值:{_threadLocalValue}");
}
private static void Main(string[] args)
{
var thread1 = new Thread(ThreadStatic1);
var thread2 = new Thread(ThreadStatic2);
var thread3 = new Thread(ThreadStatic3);
thread1.Start();
thread2.Start();
thread3.Start();
}
}
c#
应用在引用类型上
nternal class Program
{
private static ThreadLocal _threadLocalValue = new ThreadLocal(() => "1");
static void ThreadStatic1()
{
Console.WriteLine($"线程 Id : {Environment.CurrentManagedThreadId},变量值:{_threadLocalValue}");
}
static void ThreadStatic2()
{
Console.WriteLine($"线程 Id : {Environment.CurrentManagedThreadId},变量值:{_threadLocalValue}");
}
static void ThreadStatic3()
{
Console.WriteLine($"线程 Id : {Environment.CurrentManagedThreadId},变量值:{_threadLocalValue}");
}
private static void Main(string[] args)
{
var thread1 = new Thread(ThreadStatic1);
var thread2 = new Thread(ThreadStatic2);
var thread3 = new Thread(ThreadStatic3);
thread1.Start();
thread2.Start();
thread3.Start();
}
}
c#
ThreadLocal解决了ThreadStatic有这个缺陷
volatile关键字
volatile关键字就是为了告诉编译器和运行时:该字段的值可能会被多个线程同时修改,因此每次访问该字段时,都应该直接从主内存中读取,而不是使用寄存器或缓存中的值。这样可以防止 CPU 的优化行为导致某些线程读取到过时的值。
// 错误示例:未使用volatile可能导致死循环
bool running = true;
Thread worker = new Thread(() => {
while (running) { } // 可能永远循环,因running被缓存
});
worker.Start();
Thread.Sleep(1000);
running = false; // 线程可能看不到此修改
将running
声明为volatile
将running声明为volatile
c#
原子操作
编程语言中原子操作指:不可分割的操作单元,是指一类不可中断的操作,它在执行时要么全部执行,要么全部不执行,不会被其他操作打断,而执行结果要么全部成功,要不全部失败,没有其他状态。
线程同步的目的——确保多个线程在访问共享资源时能够按顺序、安全地进行操作,从而避免并发执行带来的数据竞争和不一致的状态。
可以说原子操作天然的解决了多线程共享资源安全问题。
原子操作是指在执行过程中不可被中断的操作,确保操作的线程安全性,避免数据竞争(Data Race)。
使用Interlocked
线程同步
概念:在多线程中当多个线程需要同时使用共享资源时,很容易产生互相竞争资源使用权的情况,这一问题也叫竞争条件。此时就可以通过线程同步技术实现多个线程按顺序使用共享资源,从而避免竞争条件。
线程同步是实现线程安全的一种方式
用户模式同步机制
用户模式同步机制指在用户空间内完成线程的阻塞和唤醒操作,由程序自己管理同步对象的一种同步方式,因为不涉及与操作系统内核交换,因此开销较低,更轻量级。
实现方式有SpinLock、SpinWait、Monitor(lock)等。
lock
lock锁当前实例
public class LockThisExample
{
public void Method1()
{
lock (this)
{
var threadId = Thread.CurrentThread.ManagedThreadId;
Console.WriteLine($"线程 {threadId} 通过lock(this)锁进入 Method1");
Console.WriteLine($"进入时间 {DateTime.Now:HH:mm:ss}");
Console.WriteLine($"开始休眠 5 秒");
Console.WriteLine($"------------------------------------");
Thread.Sleep(5000);
}
}
public void Method2()
{
lock (this)
{
var threadId = Thread.CurrentThread.ManagedThreadId;
Console.WriteLine($"线程 {threadId} 通过lock(this)锁进入 Method2");
Console.WriteLine($"进入时间 {DateTime.Now:HH:mm:ss}");
}
}
}
internal class Program
{
static void Main(string[] args)
{
var example = new LockThisExample();
var thread1 = new Thread(example.Method1);
var thread2 = new Thread(example.Method2);
var thread3 = new Thread(() =>
{
lock (example)
{
var threadId = Thread.CurrentThread.ManagedThreadId;
Console.WriteLine($"线程 {threadId} 通过lock(实例)锁进入 Method3");
Console.WriteLine($"进入时间 {DateTime.Now:HH:mm:ss}");
Console.WriteLine($"开始休眠 5 秒");
Console.WriteLine($"------------------------------------");
Thread.Sleep(5000);
}
});
thread3.Start();
thread1.Start();
thread2.Start();
线程 8 通过lock(this)锁进入 Method2
进入时间 22:16:49
线程 9 通过lock(实例)锁进入 Method3
进入时间 22:16:49
开始休眠 5 秒
------------------------------------
线程 7 通过lock(this)锁进入 Method1
进入时间 22:16:54
开始休眠 5 秒
------------------------------------
}
}
c#
lock锁定公共对象
public class PublicLock
{
public static readonly object Lock = new object();
}
public class LockPublic1Example
{
public void Method1()
{
lock (PublicLock.Lock)
{
var threadId = Thread.CurrentThread.ManagedThreadId;
Console.WriteLine($"线程 {threadId} 通过 lock(公共对象) 锁进入 Public1");
Console.WriteLine($"进入时间 {DateTime.Now:HH:mm:ss}");
Console.WriteLine($"开始休眠 5 秒");
Console.WriteLine($"------------------------------------");
Thread.Sleep(5000);
}
}
}
public class LockPublic2Example
{
public void Method1()
{
lock (PublicLock.Lock)
{
var threadId = Thread.CurrentThread.ManagedThreadId;
Console.WriteLine($"线程 {threadId} 通过 lock(公共对象) 锁进入 Public2");
Console.WriteLine($"进入时间 {DateTime.Now:HH:mm:ss}");
}
}
}
internal class Program
{
static void Main(string[] args)
{
var example1 = new LockPublic1Example();
var example2 = new LockPublic2Example();
var thread1 = new Thread(example1.Method1);
var thread2 = new Thread(example2.Method1);
thread1.Start();
thread2.Start();
线程 8 通过 lock(公共对象) 锁进入 Public2
进入时间 22:10:42
线程 7 通过 lock(公共对象) 锁进入 Public1
进入时间 22:10:42
开始休眠 5 秒
}
}
c#
Lock锁定字符串
在C#中,字符串因其不可变性和字符串池的原因,在整个程序中一个字符串一旦创建就不会更改,如果对其修改则产生新的字符串对象,而原字符串对象保持不变;同时如果创建两个相同内容的字符串,则它们共享同一个内存地址。
这就导致锁定字符串极其危险尤其危险,因为整个程序中任何给定字符串都只有一个实例,而在整个程序中只有锁定相同内容的字符串都会形成竞争条件。
public class LockString1Example
{
public void Method1()
{
lock ("abc")
{
var threadId = Thread.CurrentThread.ManagedThreadId;
Console.WriteLine($"线程 {threadId} 通过 lock(字符串) 锁进入 String1");
Console.WriteLine($"进入时间 {DateTime.Now:HH:mm:ss}");
Console.WriteLine($"开始休眠 5 秒");
Console.WriteLine($"------------------------------------");
Thread.Sleep(5000);
}
}
}
public class LockString2Example
{
public void Method1()
{
lock ("abc")
{
var threadId = Thread.CurrentThread.ManagedThreadId;
Console.WriteLine($"线程 {threadId} 通过 lock(字符串) 锁进入 String2");
Console.WriteLine($"进入时间 {DateTime.Now:HH:mm:ss}");
}
}
}
internal class Program
{
static void Main(string[] args)
{
var example1 = new LockString1Example();
var example2 = new LockString2Example();
var thread1 = new Thread(example1.Method1);
var thread2 = new Thread(example2.Method1);
thread1.Start();
thread2.Start();
线程 7 通过 lock(字符串) 锁进入 String2
进入时间 22:18:41
线程 6 通过 lock(字符串) 锁进入 String1
进入时间 22:18:41
开始休眠 5 秒
------------------------------------
}
}
c#
可以发现虽然在两个类中分别使用了两个字符串“abc”,但对于整个程序来说它们都指向了同一个实例,因此共用了一把锁。
小心锁定非readonly对象
这是因为如果锁对象为非只读对象,就可能发生某个lock代码块中修改锁对象,从而导致
锁对象变更,进而使得其他线程可以畅通无阻的进入该代码块。
public class LockNotReadonlyExample
{
private object _lock = new object();
public void Method1()
{
lock (_lock)
{
_lock = new object();
var threadId = Thread.CurrentThread.ManagedThreadId;
Console.WriteLine($"线程 {threadId} 进入 Method1 , 时间 {DateTime.Now:HH:mm:ss}");
Console.WriteLine($"------------------------------------");
Thread.Sleep(5000);
}
}
}
internal class Program
{
static void Main(string[] args)
{
var example = new LockNotReadonlyExample();
var thread1 = new Thread(example.Method1);
var thread2 = new Thread(example.Method1);
var thread3 = new Thread(example.Method1);
thread1.Start();
thread2.Start();
thread3.Start();
线程 8 进入 Method1 , 时间 22:21:40
------------------------------------
线程 7 进入 Method1 , 时间 22:21:40
------------------------------------
线程 6 进入 Method1 , 时间 22:21:40
------------------------------------
}
}
c#
可以发现三个线程几乎同时进入,lock根本就没有起到锁的作用。
小心锁定静态对象
public class LockStaticExample
{
//这是一个实例字段,意味着类的每个实例都会有一个独立的锁对象。
//如果你希望类的每个实例有自己独立的锁来控制并发访问,这种方式更合适。
private readonly object _lock1 = new object();
//这是一个静态字段,意味着类的所有实例共享同一个锁对象。
//如果你希望类的所有实例都共享同一个锁来同步对某个静态资源访问,这种方式更合适。
private static readonly object _lock2 = new object();
public void Method1()
{
lock (_lock1)
{
// 临界区代码
}
}
public void Method2()
{
lock (_lock2)
{
// 临界区代码
}
}
public static void Method3()
{
lock (_lock2)
{
// 临界区代码
}
}
}
c#
Monitor
lock (obj)
{
//同步代码块
}
c#
最终会被解析成以下代码:
Monitor.Enter(obj);
try
{
//同步代码块
}
finally
{
Monitor.Exit(obj);
}
避免锁定值类型
public class LockValueTypeExample
{
private static readonly int _lock = 88;
public void Method1()
{
try
{
Monitor.Enter(_lock);
var threadId = Thread.CurrentThread.ManagedThreadId;
Console.WriteLine($"线程 {threadId} 通过 lock(值类型) 锁进入 Method1");
Console.WriteLine($"进入时间 {DateTime.Now:HH:mm:ss}");
Console.WriteLine($"开始休眠 5 秒");
Console.WriteLine($"------------------------------------");
Thread.Sleep(5000);
}
finally
{
Console.WriteLine($"开始释放锁 {DateTime.Now:HH:mm:ss}");
Monitor.Exit(_lock);
Console.WriteLine($"完成锁释放 {DateTime.Now:HH:mm:ss}");
}
}
}
internal class Program
{
static void Main(string[] args)
{
var example = new LockValueTypeExample();
var thread1 = new Thread(example.Method1);
thread1.Start();
线程 7 通过 lock(值类型) 锁进入 Method1
进入时间 22:27:22
开始休眠 5 秒
------------------------------------
开始释放锁 22:27:27
Unhandled exception. System.Threading.SynchronizationLockException: Object synchronization method was called from an unsynchronized block of code.
at System.Threading.Monitor.Exit(Object obj)
at ConsoleApp5.LockValueTypeExample.Method1() in C:\Users\xkgoo\Desktop\ConsoleApp1\ConsoleApp5\Program.cs:line 21
}
}
c#
可以发现在释放锁的时候抛出异常,大致意思是:“对象同步方法在未同步的代码块中被调用。”,这就是因为锁定的地方和释放的地方锁已经不一样了。
小心try/finally
如上面的例子,Monitor.Enter方法是写在try块中,试想一下:如果在Monitor.Enter方法之前抛出了异常会怎样异常?看下面这段代码:
public class LockBeforeExceptionExample
{
private static readonly object _lock = new object();
public void Method1()
{
try
{
if (new Random().Next(2) == 1)
{
Console.WriteLine($"在调用Monitor.Enter前发生异常");
throw new Exception("在调用Monitor.Enter前发生异常");
}
Monitor.Enter(_lock);
}
catch (Exception ex)
{
Console.WriteLine($"捕捉到异常:{ex.Message}");
}
finally
{
Console.WriteLine($"开始释放锁 {DateTime.Now:HH:mm:ss}");
Monitor.Exit(_lock);
Console.WriteLine($"完成锁释放 {DateTime.Now:HH:mm:ss}");
}
}
}
internal class Program
{
private static void Main(string[] args)
{
var example = new LockBeforeExceptionExample();
var thread1 = new Thread(example.Method1);
thread1.Start();
//调试模式报错Object synchronization method was called from an unsynchronized block of code.”
这是因为还没有执行锁定就抛出异常,导致释放一个没有锁定的锁。
}
}
只需要判断lockTaken即可决定是否需要执行释放锁操作,具体代码如下:
public class LockSolveBeforeExceptionExample
{
private static readonly object _lock = new object();
public void Method1()
{
var lockTaken = false;
try
{
if (new Random().Next(2) == 1)
{
Console.WriteLine($"在调用Monitor.Enter前发生异常");
throw new Exception("在调用Monitor.Enter前发生异常");
}
Monitor.Enter(_lock, ref lockTaken);
}
catch (Exception ex)
{
Console.WriteLine($"捕捉到异常:{ex.Message}");
}
finally
{
if (lockTaken)
{
Console.WriteLine($"开始释放锁 {DateTime.Now:HH:mm:ss}");
Monitor.Exit(_lock);
Console.WriteLine($"完成锁释放 {DateTime.Now:HH:mm:ss}");
}
else
{
Console.WriteLine($"未执行锁定,无需释放锁");
}
}
}
}
internal class Program
{
private static void Main(string[] args)
{
var example = new LockSolveBeforeExceptionExample();
var thread1 = new Thread(example.Method1);
thread1.Start();
}
}
c#
TryEnter设置超时时间
public class LockTryEnterExample
{
private static readonly object _lock = new object();
public void Method1()
{
try
{
Monitor.Enter(_lock);
Console.WriteLine($"Method1 | 获取锁成功,并锁定 5 秒");
Thread.Sleep(5000);
}
finally
{
Monitor.Exit(_lock);
}
}
public void Method2()
{
Console.WriteLine($"Method2 | 尝试获取锁");
if (Monitor.TryEnter(_lock, 3000))
{
try
{
}
finally
{
}
}
else
{
Console.WriteLine($"Method2 | 3 秒内未获取到锁,自动退出锁");
}
}
public void Method3()
{
Console.WriteLine($"Method3 | 尝试获取锁");
if (Monitor.TryEnter(_lock, 7000))
{
try
{
Console.WriteLine($"Method3 | 7 秒内获取到锁");
}
finally
{
Console.WriteLine($"Method3 |开始释放锁");
Monitor.Exit(_lock);
Console.WriteLine($"Method3 |完成锁释放");
}
}
else
{
Console.WriteLine($"Method3 | 7 秒内未获取到锁,自动退出锁");
}
}
}
internal class Program
{
private static void Main(string[] args)
{
var example = new LockTryEnterExample();
var thread1 = new Thread(example.Method1);
var thread2 = new Thread(example.Method2);
var thread3 = new Thread(example.Method3);
thread1.Start();
thread2.Start();
thread3.Start();
//打印输出
Method2 | 尝试获取锁
Method3 | 尝试获取锁
Method1 | 获取锁成功,并锁定 5 秒
Method2 | 3 秒内未获取到锁,自动退出锁
Method3 | 7 秒内获取到锁
Method3 |开始释放锁
Method3 |完成锁释放
}
}
c#
实现生产者-消费者模式
除了上面介绍的方法,Monitor类还有Wait、Pulse、PulseAll等方法。
Wait: 该方法用于将当前线程放入等待队列,直到收到其他线程的信号通知。
Pulse: 该方法用于唤醒等待队列中的一个线程。当一个线程调用 Pulse 时,它会通知一个正在等待该对象锁的线程继续执行。
PulseAll: 该方法用于唤醒等待队列中的所有线程。
然后我们利用Monitor类的这些功能来实现一个简单的生产者-消费者模式。大致思路如下:
1.首先启动生产者线程,获取锁,然后生成数据;
2.当生产者生产的数据小于数据队列长度,则生产一条数据同时通知消费者线程进行消费,否则暂停当前线程等待消费者线程消费数据;
3.然后启动消费者线程,获取锁,然后消费数据;
4.当数据队列中有数据,则消费一条数据同时通知生产者线程可以生产数据了,否则暂停当前线程等待生产者线程生产数据;
public class LockProducerConsumerExample
{
private static Queue queue = new Queue();
private static object _lock = new object();
//生产者
public void Producer()
{
while (true)
{
lock (_lock)
{
Console.ForegroundColor = ConsoleColor.Red;
if (queue.Count < 3)
{
var item = new Random().Next(100);
queue.Enqueue(item);
Console.WriteLine($"生产者,生产: {item}");
//唤醒消费者
Monitor.Pulse(_lock);
}
else
{
//队列满时,生产者等待
Console.WriteLine($"队列已满,生产者等待中……");
Monitor.Wait(_lock);
}
}
Thread.Sleep(500);
}
}
// 消费者
public void Consumer()
{
while (true)
{
lock (_lock)
{
Console.ForegroundColor = ConsoleColor.Blue;
if (queue.Count > 0)
{
var item = queue.Dequeue();
Console.WriteLine($"消费者,消费: {item}");
//唤醒生产者
Monitor.Pulse(_lock);
}
else
{
//队列空时,消费者等待
Console.WriteLine($"队列已空,消费者等待中……");
Monitor.Wait(_lock);
}
}
Thread.Sleep(10000);
}
}
}
internal class Program
{
private static void Main(string[] args)
{
var example = new LockProducerConsumerExample();
var thread1 = new Thread(example.Producer);
var thread2 = new Thread(example.Consumer);
thread1.Start();
thread2.Start();
thread1.Join();
thread2.Join();
}
}
SpinLock
SpinLock是一个高效的自旋锁实现,用于提供一种轻量级的锁机制。SpinLock通过在等待锁的过程中执行自旋(即不断尝试获取锁)来避免线程上下文切换,从而减少系统开销
SpinLock是一个结构体,使用上和Monitor类很像,都是通过Enter或TryEnter方法持有锁,同时默认支持lockTaken模式,然后通过Exit释放锁。
示例:
public class SpinLockExample
{
//自旋锁
private static SpinLock _spinLock = new SpinLock();
//共享资源计数器
private static int _counter = 0;
//计数
public void Count()
{
var lockTaken = false;
try
{
//持有锁
_spinLock.Enter(ref lockTaken);
//访问并修改共享资源
_counter++;
var threadId = Thread.CurrentThread.ManagedThreadId;
Console.WriteLine($"线程号:{threadId} 递增共享变量 _counter 为:{_counter}");
}
finally
{
if (lockTaken)
{
//释放锁
_spinLock.Exit();
}
}
}
//打印
public void Print()
{
Console.WriteLine($"---------------------------------------");
Console.WriteLine($"_counter 最终值为:{_counter}");
}
}
internal class Program
{
private static void Main(string[] args)
{
var example = new SpinLockExample();
//启动10个线程
var threads = new Thread[10];
for (var i = 0; i < threads.Length; i++)
{
threads[i] = new Thread(example.Count);
threads[i].Start();
}
for (var i = 0; i < threads.Length; i++)
{
threads[i].Join();
}
example.Print();
}
}
c#
小心传递SpinLock实例
在传递SpinLock实例时,需要十分小心,这是因为SpinLock是结构体即为值类型,当通过值传递,会导致创建该结构体的副本,复制一个新的实例,而不是传递引用。
#region 在传递SpinLock实例时,需要十分小心,这是因为SpinLock是结构体即为值类型,当通过值传递,会导致创建该结构体的副本,复制一个新的实例,而不是传递引用。
//public class CopySpinLockExample
//{
// public void Method1(int thread, SpinLock lockCopy)
// {
// var lockTaken = false;
// //尝试获取锁
// lockCopy.Enter(ref lockTaken);
// if (lockTaken)
// {
// Console.WriteLine($"线程 {thread},成功获取锁");
// }
// else
// {
// Console.WriteLine("线程 {thread},未获取到锁");
// }
// }
//}
#endregion
public class RefCopySpinLockExample
{
public void Method1(int thread, ref SpinLock lockCopy)
{
var lockTaken = false;
//尝试获取锁
lockCopy.Enter(ref lockTaken);
if (lockTaken)
{
Console.WriteLine($"线程 {thread},成功获取锁");
lockCopy.Exit();
Console.WriteLine($"线程 {thread},释放锁");
}
else
{
Console.WriteLine("线程 {thread},未获取到锁");
}
}
public void Method2(int thread, ref SpinLock lockCopy)
{
var lockTaken = false;
//尝试获取锁
lockCopy.Enter(ref lockTaken);
if (lockTaken)
{
Console.WriteLine($"线程 {thread},成功获取锁");
}
else
{
Console.WriteLine("线程 {thread},未获取到锁");
}
}
}
internal class Program
{
private static void Main(string[] args)
{
var example = new RefCopySpinLockExample();
SpinLock spinLock = new SpinLock();
example.Method1(1, ref spinLock);
example.Method2(2, ref spinLock);
spinLock.Exit();
Console.WriteLine("主线程,释放锁");
}
}
c#
实现原理
从上面代码可以发现从使用上来说,SpinLock和互斥锁Monitor基本一样,那为什么还要SpinLock呢?
首先互斥锁Monitor在获取锁时会阻塞线程,同时线程会进行上下文切换,把CPU资源让出来给其他线程使用,直到锁可用。从这里也可以看出互斥锁Monitor适用锁竞争时间较长的场景,否则线程上下文切换比等待资源消耗代价更高就不划算了。
针对上面提到的问题,就引发了需要一种非阻塞线程的锁方案,因此SpinLock就应用而生。
如何实现非阻塞线程呢?
首先我们需要理解非阻塞的意义,它是为了解决进行线程上下文切换的代价比锁的等待代价更大的问题。说白了就是不要让线程进行上下文切换,比如最简单粗暴的方式就是直接使用while(true){},使得线程一直处于活动状态。
而SpinLock底层实现原理的确通过使用while(true){},使得线程原地停留且又不阻塞线程。因为while(true)自动循环的特点才叫自旋锁。当然SpinLock底层实现不止这么简单,比如还用到了原子操作Interlocked.CompareExchange。
总结下来SpinLock 的工作原理,大致分为以下两步:
1.当前线程尝试获取锁,如果获取成功,进入同步代码块。
2.如果未能取锁(即锁已经被另一个线程持有),则当前线程会在一个循环(自旋)中重复尝试,直到获取到锁。
SpinLock主要优势在于它不会将线程挂起即不会发生线程上下文切换,而是让线程在一个循环(自旋)中等待,直到锁被释放后再获取。同样因为线程一直自旋等待,如果线程需要等待时间很长又会导致CPU占用过高以及资源浪费。
结合SpinLock实现原理,有如下建议:
1.在需要大量锁(高并发)并且锁持有时间又非常短的场景下,特别适合使用SpinLock。
2.避免在单核CPU上使用SpinLock,因为自旋等待会浪费CPU资源。
实现一个简单的自旋锁
下面我们可以根据SpinLock实现原理来自己实现一个简单的自旋锁。
大致思路如下:
1.通过在while(true)循环中,使用原子操作Interlocked.CompareExchange进行设置锁,从而实现持有锁方法Enter;
2.通过直接标记锁状态为未锁定状态,来实现锁释放方法Exit;
具体代码如下:
public class MySpinLock
{
// 0 - 未锁定, 1 - 锁定
private volatile int _isLocked = 0;
//获取锁
public void Enter()
{
while (true)
{
//使用原子操作检查和设置锁
if (Interlocked.CompareExchange(ref _isLocked, 1, 0) == 0)
{
//成功获得锁
return;
}
}
}
//释放锁
public void Exit()
{
//释放锁,直接设置为未锁定状态
_isLocked = 0;
}
}
C#
Semaphore
在 C# 中,信号量(Semaphore)是一种用于线程同步的机制,能够控制对共享资源的访问。它的工作原理是通过维护一个计数器来控制对资源的访问次数。它常用于限制对共享资源(如数据库连接池、文件系统、网络资源等)的并发访问。
信号量有三个核心概念:
1.计数:信号量的核心是一个计数器,表示当前资源的可用数量;
2.等待:当线程请求资源时,此次如果计数器大于0,则线程可以继续执行,同时计数器减1;如果计数器等于0,则线程被阻塞直至其他线程释放资源,即有线程增加计数器的值;
3.释放:当线程使用完资源后,则需要释放信号量,同时计数器加1,并唤醒其他等待的线程;
相信理解了信号量核心概念,其工作原理就不言而喻。
应用场景:
通过对信号量的工作原理了解,我们可以总结为:信号量就是为了控制对共享资源的访问,保证共享资源不会被过度使用,因此可以引申出以下适用于信号量的场景:
1.控制各种连接池:限制同时打开各种资源的连接数量(比如连接打印机数量,数据库连接数据,文件访问数量);
2.限制网络请求:防止服务器过载,导致服务器崩溃;
3.协调多个线程的执行顺序:通过信号量控制生成者和消费者之间的资源访问, 实现生产者和消费者模型;
C#中的信号量实现
C#提供了两种信号量类型:Semaphore和SemaphoreSlim。其中两者功能基本相同,却又有所不同,而SemaphoreSlim是更轻量、更快速的信号量实现。下面是两种简单比较:
Semaphore: 是基于系统内核实现,属于内核级别同步,支持跨进程资源同步,因此性能较低,内存占用较大;它可以一次释放多个信号量,但是没有提供原生的异步支持;
SemaphoreSlim: 是用户级别同步,并不依赖系统内核,因此不支持跨进程资源同步,因此性能更高,内存占用更低;它一次只能释放一个信号量,但是提供了原生异步支持;
Semaphore使用示例
通过对信号量原理的详细了解,而作为对信号量实现类Semaphore,这些原理也同样适用,因此Semaphore类的构造函数就指定了用于控制线程数量的参数。其构造函数如下:
public Semaphore(int initialCount, int maximumCount);
public Semaphore(int initialCount, int maximumCount, string name);
initialCount: 初始化信号量的计数,表示初始时可以同时访问资源的线程数量。
maximumCount: 信号量的最大计数,表示允许同时访问资源的最大线程数。
name: 可选的名称,用于命名信号量对象(可在多个进程间共享信号量)。
然后可以用WaitOne方法获取信号量,使用Release方法释放信号量。
public class SemaphoreExample
{
//初始化最多2个线程同时进入, 最大允许3个线程
private static Semaphore semaphore = new Semaphore(2, 3);
//用于不同的线程显示不同的颜色,方便观察结果
private static ConsoleColor[] colors = new ConsoleColor[5]
{
ConsoleColor.Red,
ConsoleColor.White,
ConsoleColor.Yellow,
ConsoleColor.Green,
ConsoleColor.Blue
};
public static void Worker(object? i)
{
var id = (int)i;
var color = colors[id];
PrintText.SafeForegroundColor($"线程 {id} 等待进入...", color);
//请求进入信号量(如果资源不可用,则返回)
semaphore.WaitOne();
PrintText.SafeForegroundColor($"线程 {id} 已 [ 进入 ] 同步代码块.", color);
//业务处理
Thread.Sleep(2000);
PrintText.SafeForegroundColor($"线程 {id} 已 [ 离开 ] 同步代码块.", color);
//释放信号量(让其他线程可以进入)
semaphore.Release();
}
}
C#
Semaphore使用注意事项
1.确保每个调用 WaitOne方法 的线程最终都会调用 Release方法。否则,可能会导致死锁。
2.确保Release方法的调用次数不应超过 WaitOne方法的调用次数,否则会抛出 SemaphoreFullException异常
3.如果单进程程序尽量选择SemaphoreSlim,因为SemaphoreSlim性能更好。
4.如果需要跨进程同步可以使用带名称的构造函数 Semaphore(int initialCount, int maximumCount, string name)。
内核模式同步机制
内核模式同步机制是指在操作系统内核空间就完成线程的挂起与恢复,由操作系统管理同步对象的一种同步方式,因为每次线程同步操作都需要操作系统参与,因此必然回涉及内核态的上下文切换,同时还是涉及到操作系统内部的数据结构和资源管理,因此内核模式同步机制往往会导致较高的开销。
实现方式有Semaphore、Mutex、AutoResetEvent等。
混合模式同步机制
混合模式同步机制在某些情况下会根据线程竞争的情况在用户模式和内核模式之间切换。通常,当资源访问冲突较小或线程阻塞较少时,采用用户模式同步;当资源争用较多或有较大的线程等待时,自动切换到内核模式同步。
实现方式有SemaphoreSlim、ManualResetEventSlim、CountDownEvent、Barrier、ReaderWriterLockSlim等。
异步编程
同步与异步的区别
同步
///
/// 同步
///
///
///
private void btnSyn_Click(object sender, EventArgs e)
{
Stopwatch sw = new Stopwatch();
sw.Start();
Console.WriteLine("同步方法开始");
Console.WriteLine("线程ID:" + Thread.CurrentThread.ManagedThreadId);
var request = WebRequest.Create("https://github.com/");//为了更好的演示效果,我们使用网速比较慢的外网
var WebResponse = request.GetResponse();//发送请求
var stream = WebResponse.GetResponseStream();//获取返回数据流
using (StreamReader reader = new StreamReader(stream))
{
StringBuilder sb = new StringBuilder();
while (!reader.EndOfStream)
{
var content = reader.ReadLine();
sb.Append(content);
}
Console.WriteLine(sb.ToString().Trim().Substring(0, 100) + "...");//只取返回内容的前100个字符
btnAsyn.Invoke((Action)(() => { btnAsyn.Text = "执行完毕!"; }));//这里跨线程访问UI需要做处理
}
Console.WriteLine("线程ID:" + Thread.CurrentThread.ManagedThreadId);
btnSyn.Text = "同步方法结束!";
sw.Stop();
Console.WriteLine($"耗时{sw.Elapsed.TotalSeconds}");
//打印输出
同步方法开始
线程ID:1
//执行完成后的回调
{
var response = request.EndGetResponse(t);
var stream = response.GetResponseStream();//获取返回数据流
using (StreamReader reader = new StreamReader(stream))
{
StringBuilder sb = new StringBuilder();
while (!reader.EndOfStream)
{
var content = reader.ReadLine();
sb.Append(content);
}
Console.WriteLine( sb.ToString().Trim().Substring(0, 100) + "...");//只取返回内容的前100个字符
Console.WriteLine("异步线程ID:" + Thread.CurrentThread.ManagedThreadId);
btnAsyn.Invoke((Action)(() => { btnAsyn.Text = "执行完毕!"; }));//这里跨线程访问UI需要做处理
}
}), null);
Console.WriteLine("主线程ID:" + Thread.CurrentThread.ManagedThreadId);
sw.Stop();
Console.WriteLine($"耗时{sw.Elapsed.TotalSeconds}");
//打印输出
主线程ID:1
主线程ID:1
耗时0.0223099
//执行完成后的回调
{
var response = request.EndGetResponse(t);
var stream = response.GetResponseStream();//获取返回数据流
using (StreamReader reader = new StreamReader(stream))
{
StringBuilder sb = new StringBuilder();
while (!reader.EndOfStream)
{
var content = reader.ReadLine();
sb.Append(content);
}
Console.WriteLine( sb.ToString().Trim().Substring(0, 100) + "...");//只取返回内容的前100个字符
Console.WriteLine("异步线程ID:" + Thread.CurrentThread.ManagedThreadId);
btnAsyn.Invoke((Action)(() => { btnAsyn.Text = "执行完毕!"; }));//这里跨线程访问UI需要做处理
}
}), null);
Console.WriteLine("主线程ID:" + Thread.CurrentThread.ManagedThreadId);
//打印输出:
//主线程ID:1
//主线程ID:1
//
/// 耗时操作
///
public void DoSomeThing()
{
Console.WriteLine("耗时操作开始,当前线程"+Thread.CurrentThread.ManagedThreadId);
for (int i = 0; i < 100; i++)
{
Thread.Sleep(20);
}
Console.WriteLine("耗时操作结束,当前线程" + Thread.CurrentThread.ManagedThreadId);
}
Console.WriteLine("异步回调开始,当前线程" + Thread.CurrentThread.ManagedThreadId);
Action action = DoSomeThing;
///回调函数
AsyncCallback asyncCallback = ar => { Console.WriteLine("异步回调,完成异步操作后调用,当前线程" + Thread.CurrentThread.ManagedThreadId); };
action.BeginInvoke(asyncCallback, null);
Console.WriteLine("异步回调结束,当前线程" + Thread.CurrentThread.ManagedThreadId);
//打印输出
//异步回调开始,当前线程1
//异步回调结束,当前线程1
//耗时操作开始,当前线程3
//耗时操作结束,当前线程3
//异步回调,完成异步操作后调用,当前线程3
c#
异步IsCompleted
Console.WriteLine("异步IsCompleted,当前线程" + Thread.CurrentThread.ManagedThreadId);
Action action = DoSomeThing;
///回调函数
AsyncCallback asyncCallback = ar => { Console.WriteLine("异步回调,完成异步操作后调用,当前线程" + Thread.CurrentThread.ManagedThreadId); };
IAsyncResult asyncResult = action.BeginInvoke(asyncCallback, null);
if (!asyncResult.IsCompleted)
{
Console.WriteLine("异步IsCompleted还没完成,当前线程" + Thread.CurrentThread.ManagedThreadId);
}
Console.WriteLine("异步IsCompleted,当前线程" + Thread.CurrentThread.ManagedThreadId);
//打印输出
//异步IsCompleted,当前线程1
//异步IsCompleted还没完成,当前线程1
//异步IsCompleted,当前线程1
//耗时操作开始,当前线程3
//耗时操作结束,当前线程3
//异步回调,完成异步操作后调用,当前线程3
异步EndInvoke
///
/// 打印
///
///
public string Print(string str)
{
//模拟耗时操作
Console.WriteLine("耗时操作开始,当前线程" + Thread.CurrentThread.ManagedThreadId);
Thread.Sleep(20);
Console.WriteLine($"打印{str}");
Console.WriteLine("耗时操作结束,当前线程" + Thread.CurrentThread.ManagedThreadId);
return str;
}
//开始
Console.WriteLine("异步EndInvoke,当前线程" + Thread.CurrentThread.ManagedThreadId);
//添加方法
Func func = Print;
//回调函数
AsyncCallback asyncCallback = ar =>
{
Console.WriteLine("异步EndInvoke,完成异步操作后调用,当前线程" + Thread.CurrentThread.ManagedThreadId);
};
IAsyncResult asyncResult = func.BeginInvoke("Hello World!", asyncCallback, null);
//异步返回值
Console.WriteLine("异步EndInvoke,返回值为" + func.EndInvoke(asyncResult));
//结束
Console.WriteLine("异步EndInvoke,当前线程" + Thread.CurrentThread.ManagedThreadId);
//打印输出
//异步EndInvoke,当前线程1
//耗时操作开始,当前线程3
//打印Hello World!
//耗时操作结束,当前线程3
//异步EndInvoke,完成异步操作后调用,当前线程3
//异步EndInvoke,返回值为Hello World!
//异步EndInvoke,当前线程1
c#
自定义APM
public IAsyncResult MyBeginXX(AsyncCallback callback)
{
var asyncResult = new MyWebRequest(callback, null);
var request = WebRequest.Create("https://github.com/");
new Thread(() => //重新启用一个线程
{
using (StreamReader sr = new StreamReader(request.GetResponse().GetResponseStream()))
{
var str = sr.ReadToEnd();
asyncResult.SetComplete(str);//设置异步结果
}
}).Start();
return asyncResult;//返回一个IAsyncResult
}
public string MyEndXX(IAsyncResult asyncResult)
{
MyWebRequest result = asyncResult as MyWebRequest;
return result.Result;
}
Console.WriteLine("主线程ID:" + Thread.CurrentThread.ManagedThreadId);
var aa= MyBeginXX(new AsyncCallback(t =>
{
var result = MyEndXX(t);
Console.WriteLine(result.Trim().Substring(0, 100) + "...");
Console.WriteLine("异步线程ID:" + Thread.CurrentThread.ManagedThreadId);
btnAsyn.Invoke((Action)(()=> { btnAsyn.Text = "执行完成"; }));//跨UI线程处理
}));
var ab = (MyWebRequest)aa;
var cc = aa.AsyncWaitHandle.WaitOne(); //用于等待 MyWebRequest 的异步操作完成。
Console.WriteLine("主线程ID:" + Thread.CurrentThread.ManagedThreadId);
//打印输出:
//主线程ID:1
//主线程ID:1
//
//
{
Thread.Sleep(2000);
Console.WriteLine("异步线程ID:" + Thread.CurrentThread.ManagedThreadId);
});//注册事件来实现异步
worker.RunWorkerAsync(this);
Console.WriteLine("主线程ID:" + Thread.CurrentThread.ManagedThreadId);
//打印输出:
//主线程ID:1
//主线程ID:1
//异步线程ID:3
c#
TAP异步编程模型
Task使用
///
/// 耗时任务
///
///
///
public static void DoSomeThing(int num1,int num2)
{
Console.WriteLine("任务"+num1+"开始"+"当前线程为"+Thread.CurrentThread.ManagedThreadId);
for (int i = 0; i < num2; i++)
{
Thread.Sleep(1);
}
Console.WriteLine("任务" + num1 + "结束" + "当前线程为" + Thread.CurrentThread.ManagedThreadId);
}
static void Main(string[] args)
{
List tasks = new List();
tasks.Add(Task.Run(() => { DoSomeThing(1, 1000); }));
tasks.Add(Task.Run(() => { DoSomeThing(2, 2000); }));
tasks.Add(Task.Run(() => { DoSomeThing(3, 3000); }));
tasks.Add(Task.Run(() => { DoSomeThing(4, 4000); }));
tasks.Add(Task.Run(() => { DoSomeThing(5, 5000); }));
tasks.Add(Task.Run(() => { DoSomeThing(6, 6000); }));
//堵塞当前线程,直到异步线程全部完成
//Task.WaitAll(tasks.ToArray());
//Console.WriteLine("任务全部完成");
//堵塞当前线程,直到异步线程任意一个完成
//Task.WaitAny(tasks.ToArray());
//Console.WriteLine("任务部分任务");
TaskFactory taskFactory = new TaskFactory();
//完成全部异步线程开启一个新线程(可能是新线程,可能是刚执行完子线程的线程)
taskFactory.ContinueWhenAll(tasks.ToArray(),c => { Console.WriteLine("任务全部完成"); });
//完成任意异步线程开启一个新线程(可能是新线程,可能是刚执行完子线程的线程)
taskFactory.ContinueWhenAny(tasks.ToArray(), c => { Console.WriteLine("任务全部完成"); });
Console.ReadKey();
}
c#
Task常用方法
//查看异步线程的执行过程
Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
Task task = Task.Run(() =>
{
Console.WriteLine("异步线程开始" + Thread.CurrentThread.ManagedThreadId);
Thread.Sleep(3000);
Console.WriteLine("异步线程结束" + Thread.CurrentThread.ManagedThreadId);
});
//打印当前线程
Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
//判断线程是否完成
Console.WriteLine(task.IsCompleted);//false
task.Wait();//堵塞直至task完成操作
Console.WriteLine(task.IsCompleted);
//打印结果:
//1
//1
//False
//异步线程开始3
//异步线程结束3
//True
c#
拿到Task异步线程的结果
Task task = Task.Run(() =>
{
Console.WriteLine("Foo");
Thread.Sleep(10000);
return 2;
});
//拿到异步线程的结果
int result = task.Result;//如果task没完成,那么就堵塞
Console.WriteLine(result);
//打印结果:
//Foo
//2
c#
Task异常,Task异常捕获并不能在外部捕获到
Task task = Task.Run(() => { throw null; });//Task异常
try
{
//当task.Wait(); 被调用并且任务内部抛出了异常时,这些异常会被封装进一个AggregateException。
task.Wait();
}
catch (Exception x)
{
if (x is NullReferenceException)
{
Console.WriteLine("Null");//无法捕捉
}
else
{
throw;
}
}
c#
修改后
Task task = Task.Run(() => { throw null; });
try
{
task.Wait();
}
catch (AggregateException ae)
{
foreach (var ex in ae.InnerExceptions)
{
if (ex is NullReferenceException)
{
Console.WriteLine("Null");
}
else
{
// 对于其他类型的异常,可以选择重新抛出或其他处理方式
throw;
}
}
}
c#
OnCompleted
Task task = Task.Run(() =>
{
int num = 0;
for (int i = 0; i < 100000; i++)
{
num += i;
}
return num;
});
var awaiter = task.GetAwaiter();
//当task执行完或发生故障的时候执行
awaiter.OnCompleted(() =>
{
int result = awaiter.GetResult();
Console.WriteLine(result);
});
Console.Read();
c#
小技巧,完成10000任务,但只需要10个线程
List list = new List();
for (int i = 0; i < 10000; i++)
{
list.Add(i);
}
//完成10000任务,但只需要10个线程
Action action = i =>
{
Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
Thread.Sleep(new Random(i).Next(100, 300));
};
List tasksList = new List();
foreach (var i in list)
{
int k = i;
tasksList.Add(Task.Run(() => action.Invoke(k)));
if (tasksList.Count > 10)
{
Task.WaitAny(tasksList.ToArray());//堵塞当前线程,直到异步线程任意一个完成
tasksList = tasksList.Where(t => t.Status != TaskStatus.RanToCompletion).ToList();
}
}
Task.WhenAll(tasksList.ToArray());//堵塞当前线程,直到异步线程全部完成
c#
多个线程任务执行状态判断
///
/// 耗时任务
///
///
///
public static void DoSomeThing(int num1,int num2)
{
Console.WriteLine("任务"+num1+"开始"+"当前线程为"+Thread.CurrentThread.ManagedThreadId);
for (int i = 0; i < num2; i++)
{
Thread.Sleep(1);
}
Console.WriteLine("任务" + num1 + "结束" + "当前线程为" + Thread.CurrentThread.ManagedThreadId);
}
static void Main(string[] args)
{
List tasks = new List();
tasks.Add(Task.Run(() => { DoSomeThing(1, 1000); }));
tasks.Add(Task.Run(() => { DoSomeThing(2, 2000); }));
tasks.Add(Task.Run(() => { DoSomeThing(3, 3000); }));
tasks.Add(Task.Run(() => { DoSomeThing(4, 4000); }));
tasks.Add(Task.Run(() => { DoSomeThing(5, 5000); }));
tasks.Add(Task.Run(() => { DoSomeThing(6, 6000); }));
//堵塞当前线程,直到异步线程全部完成
//Task.WaitAll(tasks.ToArray());
//Console.WriteLine("任务全部完成");
//堵塞当前线程,直到异步线程任意一个完成
//Task.WaitAny(tasks.ToArray());
//Console.WriteLine("任务部分任务");
TaskFactory taskFactory = new TaskFactory();
//完成全部异步线程开启一个新线程(可能是新线程,可能是刚执行完子线程的线程)
taskFactory.ContinueWhenAll(tasks.ToArray(),c => { Console.WriteLine("任务全部完成"); });
//完成任意异步线程开启一个新线程(可能是新线程,可能是刚执行完子线程的线程)
taskFactory.ContinueWhenAny(tasks.ToArray(), c => { Console.WriteLine("任务全部完成"); });
Console.ReadKey();
}
c#
await关键字的使用
public class Test
{
///
/// 不加await
///
public void NoRetrnOne()
{
Console.WriteLine("开始方法当前线程" + Thread.CurrentThread.ManagedThreadId);
Task.Run
(
() =>
{
Console.WriteLine("异步开始当前线程" + Thread.CurrentThread.ManagedThreadId);
Thread.Sleep(100);
Console.WriteLine("异步结束当前线程" + Thread.CurrentThread.ManagedThreadId);
}
);
Console.WriteLine("结束方法结束当前线程" + Thread.CurrentThread.ManagedThreadId);
}
}
public class Program
{
private static void Main(string[] args)
{
Test test = new Test();
Console.WriteLine("开始,线程为" + Thread.CurrentThread.ManagedThreadId);
test.NoRetrnOne();
Console.WriteLine("结束,线程为" + Thread.CurrentThread.ManagedThreadId);
Console.ReadKey();
//打印结果为:
//开始,线程为1
//开始方法当前线程1
//结束方法结束当前线程1
//结束,线程为1
//异步开始当前线程3
//异步结束当前线程3
}
}
c#
使用Task.Run开启一个线程,方法使用了 await关键字,Vs会提示在方法前面要加async,且返回值为Task,或带返回值的Task
public class Test
{
///
/// 加了await
///
///
public async Task NoRetrnTwo()
{
Console.WriteLine("开始方法当前线程" + Thread.CurrentThread.ManagedThreadId);
Task task = Task.Run
(
() =>
{
Console.WriteLine("异步开始当前线程" + Thread.CurrentThread.ManagedThreadId);
Thread.Sleep(100);
Console.WriteLine("异步结束当前线程" + Thread.CurrentThread.ManagedThreadId);
}
);
await task;//当主线程达到,返回调用线程,做自己的事/await后面的代码成了task的回调
Console.WriteLine("结束方法当前线程" + Thread.CurrentThread.ManagedThreadId);//由子线程完成
}
}
public class Program
{
private static void Main(string[] args)
{
Test test = new Test();
Console.WriteLine("开始,线程为" + Thread.CurrentThread.ManagedThreadId);
test.NoRetrnOne();
Console.WriteLine("结束,线程为" + Thread.CurrentThread.ManagedThreadId);
Console.ReadKey();
//打印结果为:
//开始,线程为1
//开始方法当前线程1
//异步开始当前线程3
//结束,线程为1
//异步结束当前线程3
//结束方法当前线程3
}
}
c#
使用 ContinueWith 代替 await ,虽然执行的线程不同,但是效果都是一样,可以认为ContinueWith等效await
public class Test
{
///
/// 使用 task.ContinueWith替代await task
///
///
public async Task NoRetrnThree()
{
Console.WriteLine("开始方法当前线程" + Thread.CurrentThread.ManagedThreadId);
Task task = Task.Run
(
() =>
{
Console.WriteLine("异步开始当前线程" + Thread.CurrentThread.ManagedThreadId);
Thread.Sleep(100);
Console.WriteLine("异步结束当前线程" + Thread.CurrentThread.ManagedThreadId);
}
);
//await task;//当主线程达到,返回调用线程,做自己的事/await后面的代码成了task的回调
//Console.WriteLine("结束方法当前线程" + Thread.CurrentThread.ManagedThreadId);//由子线程完成
//验证await后面的代码成了task的回调(但线程Id不同)
task.ContinueWith(c => Console.WriteLine("结束方法当前线程"+Thread.CurrentThread.ManagedThreadId));
}
}
public class Program
{
private static void Main(string[] args)
{
Test test = new Test();
Console.WriteLine("开始,线程为" + Thread.CurrentThread.ManagedThreadId);
test.NoRetrnOne();
Console.WriteLine("结束,线程为" + Thread.CurrentThread.ManagedThreadId);
Console.ReadKey();
//打印结果为:
//开始,线程为1
//开始方法当前线程1
//结束,线程为1
//异步开始当前线程3
//异步结束当前线程3
//结束方法当前线程4
}
}
c#
Parallel并行使用
private void button1_Click(object sender, EventArgs e)
{
Console.WriteLine("线程开始!当前线程"+Thread.CurrentThread.ManagedThreadId);
//运行的时候界面,主线程参与计算
Parallel.Invoke
(
() => this.DoSomeThing("张三"),
() => this.DoSomeThing("李四"),
() => this.DoSomeThing("王五")
);
Console.WriteLine("线程结束!当前线程" + Thread.CurrentThread.ManagedThreadId);
}
private void DoSomeThing(string name)
{
Console.WriteLine(name+"子线程开始!当前线程" + Thread.CurrentThread.ManagedThreadId);
Console.WriteLine(name+"Hello World !");
for (int i = 0; i < 1000; i++)
{
}
Console.WriteLine(name+"子线程结束!当前线程" + Thread.CurrentThread.ManagedThreadId);
}
//打印结果:几乎同时打印
//线程开始!当前线程1
//张三子线程开始!当前线程1
//张三Hello World !
//张三子线程结束!当前线程1
//王五子线程开始!当前线程4
//王五Hello World !
//王五子线程结束!当前线程4
//李四子线程开始!当前线程3
//李四Hello World !
//李四子线程结束!当前线程3
//线程结束!当前线程1
c#
线程安全案例
多线程的执行顺序的不确定的
for (int i = 0; i < 5; i++)
{
int k = i;
Task.Run(() =>
{
Console.WriteLine($"k为:{k},当前线程为:{Thread.CurrentThread.ManagedThreadId}");
});
}
Console.ReadKey();
//想要输出结果:
//k为:1当前线程为:5
//k为:2当前线程为:6
//k为:3当前线程为:7
//k为:4当前线程为:8
//k为:5当前线程为:3
//实际输出结果:
//k为:2,当前线程为:5
//k为:3,当前线程为:6
//k为:4,当前线程为:7
//k为:1,当前线程为:8
//k为:0,当前线程为:3
c#
当多个线程操作同一个内存后面的修改会覆盖前面的
List list = new List();
for (int i = 0; i < 1000; i++)
{
Task.Run(() =>
{
list.Add(i);
});
}
Thread.Sleep(1000);
Console.WriteLine(list.Count);
//预计输出结果为:1000
//实际输出结果为:966
c#
主线程和异步线程锁住同一个变量和不同变量的区别
主线程和异步线程锁住同一个变量会相互堵塞
///
/// 静态方法
///
public static class Test
{
public static readonly Object Lock = new object();
public static void Show()
{
for (int i = 0; i < 5; i++)
{
int k = i;
Task.Run(() =>
{
lock (Lock)
{
Console.WriteLine("k=" + k + "当前线程" + Thread.CurrentThread.ManagedThreadId + "开始");
Thread.Sleep(100);
Console.WriteLine("k=" + k + "当前线程" + Thread.CurrentThread.ManagedThreadId + "结束");
}
});
}
}
}
internal class Program
{
private static void Main(string[] args)
{
Test.Show();
//锁同一个变量
for (int i = 0; i < 5; i++)
{
int k = i;
Task.Run(() =>
{
lock (Test.Lock)
{
Console.WriteLine("k=" + k + "当前主线程" + Thread.CurrentThread.ManagedThreadId + "开始");
Thread.Sleep(100);
Console.WriteLine("k=" + k + "当前主线程" + Thread.CurrentThread.ManagedThreadId + "结束");
}
});
}
Console.ReadKey();
//打印输出
k=1当前线程8开始
k=1当前线程8结束
k=0当前线程3开始
k=0当前线程3结束
k=4当前线程6开始
k=4当前线程6结束
k=3当前主线程5开始
k=3当前主线程5结束
k=2当前主线程12开始
k=2当前主线程12结束
k=2当前线程4开始
k=2当前线程4结束
k=4当前主线程10开始
k=4当前主线程10结束
k=3当前线程7开始
k=3当前线程7结束
k=0当前主线程9开始
k=0当前主线程9结束
k=1当前主线程11开始
k=1当前主线程11结束
}
}
c#
主线程和异步线程锁住不同变量并不会
internal class Program
{
private static readonly Object Lock = new object();
///
/// 静态方法
///
public static class Test
{
public static readonly Object Lock = new object();
public static void Show()
{
for (int i = 0; i < 5; i++)
{
int k = i;
Task.Run(() =>
{
lock (Lock)
{
Console.WriteLine("k=" + k + "当前线程" + Thread.CurrentThread.ManagedThreadId + "开始");
Thread.Sleep(100);
Console.WriteLine("k=" + k + "当前线程" + Thread.CurrentThread.ManagedThreadId + "结束");
}
});
}
}
}
private static void Main(string[] args)
Test.Show();
//锁不同变量
for (int i = 0; i < 5; i++)
{
int k = i;
Task.Run(() =>
{
lock (Lock)
{
Console.WriteLine("k=" + k + "当前主线程" + Thread.CurrentThread.ManagedThreadId + "开始");
Thread.Sleep(100);
Console.WriteLine("k=" + k + "当前主线程" + Thread.CurrentThread.ManagedThreadId + "结束");
}
});
}
Console.ReadKey();
}
//输出
k=0当前线程3开始
k=0当前主线程9开始
k=0当前主线程9结束
k=2当前主线程11开始
k=0当前线程3结束
k=3当前线程4开始
k=3当前线程4结束
k=1当前线程6开始
k=2当前主线程11结束
k=3当前主线程7开始
k=3当前主线程7结束
k=1当前主线程10开始
k=1当前线程6结束
k=4当前线程8开始
k=1当前主线程10结束
k=4当前线程8结束
k=2当前线程5开始
k=4当前主线程12开始
k=2当前线程5结束
k=4当前主线程12结束
c#
同一个实例对象,同一个方法不同的参数不可并发(锁住了同一个变量)
public class TestClass
{
private readonly Object Lock = new object();
public void Show(int num)
{
for (int i = 0; i < 5; i++)
{
int k = i;
Task.Run(() => {
lock (Lock)
{
Console.WriteLine(num + "开始k=" + k + "当前线程" + Thread.CurrentThread.ManagedThreadId + "开始");
Thread.Sleep(100);
Console.WriteLine(num + "结束k=" + k + "当前线程" + Thread.CurrentThread.ManagedThreadId + "结束");
}
});
}
}
}
internal class Program
{
static void Main(string[] args)
{
TestClass testClass = new TestClass();
testClass.Show(1);
testClass.Show(2);
Console.ReadKey();
//输出结果
1开始k=0当前线程6开始
1结束k=0当前线程6结束
1开始k=1当前线程7开始
1结束k=1当前线程7结束
1开始k=2当前线程8开始
1结束k=2当前线程8结束
1开始k=3当前线程9开始
1结束k=3当前线程9结束
1开始k=4当前线程10开始
1结束k=4当前线程10结束
2开始k=0当前线程11开始
2结束k=0当前线程11结束
2开始k=1当前线程12开始
2结束k=1当前线程12结束
2开始k=2当前线程13开始
2结束k=2当前线程13结束
2开始k=3当前线程14开始
2结束k=3当前线程14结束
2开始k=4当前线程15开始
2结束k=4当前线程15结束
}
}
c#
同一个类,不同实例对象,同一个方法不同的参数可并发(锁住了不同的变量)
public class TestClass
{
private readonly Object Lock = new object();
public void Show(int num)
{
for (int i = 0; i < 5; i++)
{
int k = i;
Task.Run(() => {
lock (Lock)
{
Console.WriteLine(num + "开始k=" + k + "当前线程" + Thread.CurrentThread.ManagedThreadId + "开始");
Thread.Sleep(100);
Console.WriteLine(num + "结束k=" + k + "当前线程" + Thread.CurrentThread.ManagedThreadId + "结束");
}
});
}
}
}
internal class Program
{
static void Main(string[] args)
{
TestClass testClass1 = new TestClass();
testClass1.Show(1);
TestClass testClass2 = new TestClass();
testClass2.Show(2);
Console.ReadKey();
//输出结果
1开始k=0当前线程6开始
2开始k=0当前线程11开始
2结束k=0当前线程11结束
1结束k=0当前线程6结束
2开始k=2当前线程13开始
1开始k=1当前线程7开始
1结束k=1当前线程7结束
1开始k=2当前线程8开始
2结束k=2当前线程13结束
2开始k=1当前线程12开始
1结束k=2当前线程8结束
2结束k=1当前线程12结束
1开始k=3当前线程9开始
2开始k=3当前线程14开始
1结束k=3当前线程9结束
2结束k=3当前线程14结束
2开始k=4当前线程15开始
1开始k=4当前线程10开始
1结束k=4当前线程10结束
2结束k=4当前线程15结束
}
}
c#
相同内容的字段串只有一个(锁住了同个string)
public class TestString
{
private readonly string Lock = "字符串";
public void Show(int num)
{
for (int i = 0; i < 5; i++)
{
int k = i;
Task.Run(() =>
{
lock (Lock)
{
Console.WriteLine(num + "开始k=" + k + "当前线程" + Thread.CurrentThread.ManagedThreadId + "开始");
Thread.Sleep(100);
Console.WriteLine(num + "结束k=" + k + "当前线程" + Thread.CurrentThread.ManagedThreadId + "结束");
}
});
}
}
}
internal class Program
{
TestString testString1 = new TestString();
testString1.Show(1);
TestString testString2 = new TestString();
testString2.Show(2);
}
//输出结果
1开始k=0当前线程3开始
1结束k=0当前线程3结束
1开始k=3当前线程8开始
1结束k=3当前线程8结束
2开始k=0当前线程4开始
2结束k=0当前线程4结束
2开始k=1当前线程9开始
2结束k=1当前线程9结束
1开始k=1当前线程5开始
1结束k=1当前线程5结束
2开始k=2当前线程11开始
2结束k=2当前线程11结束
2开始k=4当前线程13开始
2结束k=4当前线程13结束
2开始k=3当前线程19开始
2结束k=3当前线程19结束
1开始k=2当前线程14开始
1结束k=2当前线程14结束
1开始k=4当前线程6开始
1结束k=4当前线程6结束
this锁定的是当前类型的实例(可以并发)
public class TestThis
{
public void Show(int num)
{
for (int i = 0; i < 5; i++)
{
int k = i;
Task.Run(() =>
{
lock (this)//当前实例
{
Console.WriteLine(num + "开始k=" + k + "当前线程" + Thread.CurrentThread.ManagedThreadId + "开始");
Thread.Sleep(100);
Console.WriteLine(num + "结束k=" + k + "当前线程" + Thread.CurrentThread.ManagedThreadId + "结束");
}
});
}
}
}
internal class Program
{
TestThis testThis1 = new TestThis();
testThis1.Show(1);
TestThis testThis2 = new TestThis();
testThis2.Show(2);
}
//输出结果
1开始k=0当前线程3开始
2开始k=0当前线程8开始
1结束k=0当前线程3结束
2结束k=0当前线程8结束
2开始k=4当前线程12开始
1开始k=3当前线程6开始
1结束k=3当前线程6结束
1开始k=1当前线程9开始
2结束k=4当前线程12结束
2开始k=3当前线程5开始
2结束k=3当前线程5结束
1结束k=1当前线程9结束
1开始k=2当前线程11开始
2开始k=1当前线程4开始
1结束k=2当前线程11结束
1开始k=4当前线程7开始
2结束k=1当前线程4结束
2开始k=2当前线程10开始
2结束k=2当前线程10结束
1结束k=4当前线程7结束
锁定的都是同一个实例(不能并发)
public class TestThis
{
public void Show(int num)
{
for (int i = 0; i < 5; i++)
{
int k = i;
Task.Run(() =>
{
lock (this)//当前实例
{
Console.WriteLine(num + "开始k=" + k + "当前线程" + Thread.CurrentThread.ManagedThreadId + "开始");
Thread.Sleep(100);
Console.WriteLine(num + "结束k=" + k + "当前线程" + Thread.CurrentThread.ManagedThreadId + "结束");
}
});
}
}
}
internal class Program
{
//锁定当前实例
TestThis testThis1 = new TestThis();
testThis1.Show(1);
// 锁定当前实例
for (int i = 0; i < 5; i++)
{
int k = i;
Task.Run(() =>
{
lock (testThis1)
{
Console.WriteLine("k=" + k + "主线程当前线程" + Thread.CurrentThread.ManagedThreadId + "开始");
Thread.Sleep(100);
Console.WriteLine("k=" + k + "主线程当前线程" + Thread.CurrentThread.ManagedThreadId + "结束");
}
});
}
}
//输出结果
1开始k=0当前线程3开始
1结束k=0当前线程3结束
1开始k=2当前线程13开始
1结束k=2当前线程13结束
1开始k=4当前线程8开始
1结束k=4当前线程8结束
k=0主线程当前线程9开始
k=0主线程当前线程9结束
k=3主线程当前线程4开始
k=3主线程当前线程4结束
k=2主线程当前线程11开始
k=2主线程当前线程11结束
k=1主线程当前线程10开始
k=1主线程当前线程10结束
k=4主线程当前线程20开始
k=4主线程当前线程20结束
1开始k=1当前线程5开始
1结束k=1当前线程5结束
1开始k=3当前线程12开始
1结束k=3当前线程12结束
递归锁住当前实例对象,不会造成死锁,因为都是在同一个线程内
///
/// 递归死锁
///
public class TestThisOther
{
private int time = 0;
public void Show()
{
for (int i = 0; i < 5; i++)
{
time++;
int k = i;
lock (this)//当前实例
{
Console.WriteLine("开始k=" + k + "当前线程" + Thread.CurrentThread.ManagedThreadId + "开始");
Thread.Sleep(100);
Console.WriteLine("结束k=" + k + "当前线程" + Thread.CurrentThread.ManagedThreadId + "结束");
if (time < 5)
{
this.Show();
}
else
{
break;
}
}
}
}
}
internal class Program
{
TestThisOther testThisOther = new TestThisOther();
testThisOther.Show();
Console.ReadKey();
}
//输出结果:
开始k=0当前线程1开始
结束k=0当前线程1结束
开始k=0当前线程1开始
结束k=0当前线程1结束
开始k=0当前线程1开始
结束k=0当前线程1结束
开始k=0当前线程1开始
结束k=0当前线程1结束
开始k=0当前线程1开始
结束k=0当前线程1结束
开始k=1当前线程1开始
结束k=1当前线程1结束
开始k=1当前线程1开始
结束k=1当前线程1结束
开始k=1当前线程1开始
结束k=1当前线程1结束
开始k=1当前线程1开始
结束k=1当前线程1结束
死锁
死锁产生条件
互斥条件:
互斥即非此即彼,一个资源要不是我拥有,要不是你拥有,就是不能我们俩同时拥有。也就是互斥条件是指至少有一个资源处于非共享状态,一次只能有一个线程可以访问该资源。
占有并等待
该条件是指一个线程在拥有至少一个资源的同时还在等待获取其他线程拥有的资源。
不可掠夺
该条件是指一个线程一旦获取了某个资源,则不可被强行剥夺对该资源的所有权,只能等待该线程自己主动释放。
循环等待
循环等待是指线程等待资源形成的循环链,比如线程A等待资源B,线程B等待资源C,线程C等待资源A,但是资源A被线程A拥有,资源B被线程B拥有,资源C被线程C拥有,如此形成了依赖死循环,都在等待其他线程释放资源。
示例:
internal class Program
{
//锁1
private static readonly object lock1 = new();
//锁2
private static readonly object lock2 = new();
//模拟两个线程死锁
public static void ThreadDeadLock()
{
//线程1
var thread1 = new Thread(Thread1);
//线程2
var thread2 = new Thread(Thread2);
//线程1 启动
thread1.Start();
//线程2 启动
thread2.Start();
//等待 线程1 执行完毕
thread1.Join();
//等待 线程2 执行完毕
thread2.Join();
}
//线程1
public static void Thread1()
{
//线程1 首先获取 锁1
lock (lock1)
{
Console.WriteLine("线程1: 已获取 锁1");
//模拟一些操作
Thread.Sleep(1000);
Console.WriteLine("线程1: 等待获取 锁2");
//线程1 等待 锁2
lock (lock2)
{
Console.WriteLine("线程1: 已获取 锁2");
}
}
}
//线程2
public static void Thread2()
{
//线程2 首先获取 锁2
lock (lock2)
{
Console.WriteLine("线程2: 已获取 锁2");
//模拟一些操作
Thread.Sleep(1000);
Console.WriteLine("线程2: 等待获取 锁1");
//线程2 等待 锁1
lock (lock1)
{
Console.WriteLine("线程2: 已获取 锁1");
}
}
}
private static void Main(string[] args)
{
ThreadDeadLock();
}
}
打印输出:
线程1: 已获取 锁1
线程2: 已获取 锁2
线程1: 等待获取 锁2
线程2: 等待获取 锁1
c#
排查死锁
VS 调试
Dump文件分析
死锁解决
只要打断死锁死锁的产生条件,就能避免死锁,其中互斥和不可掠夺不可打断
那就只能打断打断占用并等待和循环等待
打断占用并等待避免死锁
使用超时机制
使用一些其他锁机制,比如使用Monitor.TryEnter方法尝试获取锁,如果在指定时间内没有获取到锁,则释放当前所拥有的锁,以此来避免死锁。
通过Thead结合CancellationToken实现超时机制,避免线程无限等待。当然可以直接使用Task,因为Task本身就支持CancellationToken,提供了内置的取消支持使用起来更方便。
打断循环等待
顺序加锁,串行化
//线程1
public static void Thread1New()
{
//线程1 首先获取 锁1
lock (lock1)
{
Console.WriteLine("线程1: 已获取 锁1");
//模拟一些操作
Thread.Sleep(1000);
Console.WriteLine("线程1: 等待获取 锁2");
//线程1 等待 锁2
lock (lock2)
{
Console.WriteLine("线程1: 已获取 锁2");
}
}
}
//线程2
public static void Thread2New()
{
//线程2 首先获取 锁2
lock (lock1)
{
Console.WriteLine("线程2: 已获取 锁2");
//模拟一些操作
Thread.Sleep(1000);
Console.WriteLine("线程2: 等待获取 锁1");
//线程2 等待 锁1
lock (lock2)
{
Console.WriteLine("线程2: 已获取 锁1");
}
}
}
c#
避免嵌套使用锁
一个线程在拥有一个锁的同时尽量避免再去申请另一个锁,这样可以避免循环等待。
//锁1
private static readonly object lock1 = new();
//锁2
private static readonly object lock2 = new();
//模拟两个任务死锁
public static async Task TaskDeadLock()
{
//启动 任务1
var task1 = Task.Run(() => Task1());
//启动 任务2
var task2 = Task.Run(() => Task2());
//等待两个任务完成
await Task.WhenAll(task1, task2);
}
//任务1
public static async Task Task1()
{
//任务1 首先获取 锁1
lock (lock1)
{
Console.WriteLine("任务1: 已获取 锁1");
//模拟一些操作
Task.Delay(1000).Wait();
//任务1 等待 锁2
Console.WriteLine("任务1: 等待获取 锁2");
lock (lock2)
{
Console.WriteLine("任务1: 已获取 锁2");
}
}
}
//任务2
public static async Task Task2()
{
//线程2 首先获取 锁2
lock (lock2)
{
Console.WriteLine("任务2: 已获取 锁2");
//模拟一些操作
Task.Delay(100).Wait();
// 任务2 等待 锁1
Console.WriteLine("任务2: 等待获取 锁1");
lock (lock1)
{
Console.WriteLine("任务2: 获取 锁1");
}
}
}
c#
全部评论