一、什么是线程
程序执行的最小单元
一次页面的渲染、一次点击事件的触发、一次数据库的访问、一次登录操作都可以看作是一个一个的进程
在一个进程中同时启用多个线程并行操作,就叫做多线程
由CPU来自动处理
线程有运行、阻塞、就绪三态
代码示例:
class Program
{
static void Main(string[] args)
{
Thread thread = new Thread(() =>
{
Print1();
});
thread.Start();
for(int i = 0; i < 1000; i++)
{
Console.Write(0);
}
Console.Read();
}
static void Print1()
{
for (int i = 0;i < 1000; i++)
{
Console.Write(1);
}
}
}
运行结果为
可以看到,在结果中,0和1的输出是交织在一起的,原因为两个线程交替着被运行,不断反复直到结束。
另外一个常用操作为Sleep();
让时间暂停,使得线程进入静默状态。
二、前台线程、后台线程与线程池托管
代码举例:
class Program
{
static void Main(string[] args)
{
Thread thread = new Thread(PrintHello);
thread.Start();
Console.WriteLine("退出主程序");
}
private static void PrintHello(object? obj)
{
while (true)
{
Thread.Sleep(1000);
Console.WriteLine("Hello from PrintHello!");
}
}
}
运行会发现,即使主线程运行结束了,子线程依旧在持续运行;持续运行的子线程就称为前台线程。
一般来说,只有等待前台线程运行完毕后,程序才可以进行关闭。
与之对应的是 后台线程,可以通过thread.IsBackground = true;//切换为后台线程
将前台 线程切换到后台线程,这样再次运行会发现,当主线程结束后,后台线程就会被强制结束。
一般来说,前台线程用于需要时间比较长的等待业务,比如监听客户端请求,而后台线程适用于时较短的业务比如执行客户端发来的请求,后台进程不会影响程序的终止。
托管在线程池中的线程全部为后台线程。
所有使用new Thread创建的线程默认均为前台线程。
三、线程池
示例代码
for(int i = 0;i < 100; i++)
{
ThreadPool.QueueUserWorkItem((o) =>
{
Console.WriteLine($"循环次数{i} 线程id {Thread.CurrentThread.ManagedThreadId}");
});
}
可以看到执行结果出现了id重复的状况,原因就是线程池会重复使用已经完成的线程,极大节约硬件资源。
另外,可以看到,for循环有100次,但是从输出结果来看,只执行了十几次,原因为线程池创建的线程均为后台线程,只要主程序退出,线程池的后台线程就会被停止,而主程序main执行的时间很短,因此线程池内线程没有来得及执行就被停止了。
对于重要的并发量小的线程,需要手动创建管理,对于并发量大而又不太重要的线程,最好托管到线程池中。
四、结束线程与CancellationToken
不管程序有多少个进程,进程内部的资源都是被共享的。所以C#对进程的取消代码作了更高层次的抽象,把进程的取消过程封装成为了Token的形式,也就是CancellationToken(取消令牌)。不仅可以使用在多线程中,还可以用于异步操作。
class Program
{
static void Main(string[] args)
{
CancellationTokenSource cts = new CancellationTokenSource();
Thread thread = new Thread(() => { PrintHello(cts.Token); });
thread.Start();
//下载文件
Thread.Sleep(5000);
//关闭子进程
//cts.Cancel();
cts.CancelAfter(3000);//在下载完成后3s失效
Console.WriteLine("退出主程序");
}
private static void PrintHello(CancellationToken tokenSource)
{
while (!tokenSource.IsCancellationRequested)
{
Thread.Sleep(1000);
Console.WriteLine("Hello from PrintHello!");
}
}
}
五、Join与IsAlive
对于子线程执行时间不确定的情况,需要使用Join的方法,加入至主程序执行中,或者使用IsAlive方法进行判断
class Program
{
static void Main(string[] args)
{
Thread thread = new Thread(() => { PrintHello(); });
thread.Start();
//方法一
//thread.Join();
//方法二
while(thread.IsAlive)
{
Console.WriteLine("子线程仍在工作");
Thread.Sleep(100);
}
Console.WriteLine("退出主程序");
}
private static void PrintHello()
{
int i = 0;
while (i++ < 10)
{
Thread.Sleep(new Random().Next(100, 1000));
Console.WriteLine("Hello from PrintHello!");
}
}
}
六、资源竞争与线程锁lock
使用线程可以并发的在CPU的核心中执行任务,最大化CPU的利用率,但是并发执行任务也可能产生各种各样的资源竞争问题。
举例:
private static void AddText()
{
File.AppendAllText(@"D:\test.txt", $"开始{Thread.CurrentThread.ManagedThreadId}");
Thread.Sleep(100);
File.AppendAllText((@"D:\test.txt", $"结束{Thread.CurrentThread.ManagedThreadId}");
}
当两个线程同时需要使用同一个文件资源时,产生资源竞争,导致系统崩溃。
因此必须保证同一时刻只能有一个线程访问资源,避免出现资源恶性竞争。
使用线程锁就可以解决
class Program
{
static object lockedObj = new object();
static void Main(string[] args)
{
for(int i = 0; i < 10; i++)
{
var t = new Thread(AddText);
t.Start();
}
Console.WriteLine("退出主程序");
}
private static void AddText()
{
lock(lockedObj)
{
File.AppendAllText(@"D:\test.txt", $"开始{Thread.CurrentThread.ManagedThreadId}");
Thread.Sleep(100);
File.AppendAllText(@"D:\test.txt", $"结束{Thread.CurrentThread.ManagedThreadId}");
}
}
}
七、异步
在之前项目中,我们实现的所有操作都是同步进行的,然而当有同时10000个请求发生时,会使得用户有很长的等待,服务器会等待数据库的响应,完成后反馈至用户。
而异步操作要实现,不要等待数据库,继续执行下一个请求,当数据返回数据以后,再回头继续处理上一个请求。
然而对于更高级别的数量请求,仅仅依靠异步也是不够的,因此需要:
异步服务+每个机器多开进程+多个机器组合实现;
K8s, Kubernetess容器化分布式部署;
.NET Core对容器化非常非常友好、支持度极高
八、异步编程Task
我们使用异步处理并行,使用多线程处理并发。
异步逻辑是要基于方法没有依赖关系的,例如
class Program
{
static void Main(string[] args)
{
Calculate();
Console.Read();
}
static void Calculate()
{
//Task->异步,Thread->线程
Task.Run(() =>
{
Calculate1();
});
Task.Run(() =>
{
Calculate2();
});
Task.Run(() =>
{
Calculate3();
});
}
static int Calculate1()
{
var result = 3;
Console.WriteLine($"Calculate1: {result}");
Task.Delay(2000);
return result;
}
static int Calculate2()
{
var result = 4;
Console.WriteLine($"Calculate2: {result}");
Task.Delay(3000);
return result;
}
static int Calculate3()
{
var result = 5;
Console.WriteLine($"Calculate3: {result}");
Task.Delay(1000);
return result;
}
}
但如果是具有依赖关系的,例如,Caculate2()需要Caculate1()的结果,Caculate3需要Caculate1()和Caculate2()的结果,那么就需要做如下调整
class Program
{
static void Main(string[] args)
{
Calculate();
Console.Read();
}
static void Calculate()
{
//Task->异步,Thread->线程
var task1 = Task.Run(() =>
{
return Calculate1();
});
var awaiter1 = task1.GetAwaiter();//获得异步等待对象
awaiter1.OnCompleted(() =>
{
var result1 = awaiter1.GetResult();//获得异步逻辑的最终计算结果
var task2 = Task.Run(() =>
{
return Calculate2(result1);
});
var awaiter2 = task2.GetAwaiter();
awaiter2.OnCompleted(() =>
{
var result2 = awaiter2.GetResult();
var result = Calculate3(result1, result2);
Console.WriteLine(result);
});
});
}
static int Calculate1()
{
var result = 3;
Console.WriteLine($"Calculate1: {result}");
Task.Delay(2000);
return result;
}
static int Calculate2(int a)
{
var result = a * 2;
Console.WriteLine($"Calculate2: {result}");
Task.Delay(3000);
return result;
}
static int Calculate3(int a, int b)
{
var result = a + b;
Console.WriteLine($"Calculate3: {result}");
Task.Delay(1000);
return result;
}
}
九、C#的异步 async/await
可以看到,上面的异步操作代码非常复杂繁琐,接下来使用async/await化解上面操作
同步方法
指程序调用某个方法,需要等待执行完成以后才进行下一步操作
异步方法
指程序调用某个方法的时候,不做任何等待,在处理完成之前就返回该方法,继续执行接下来的操作,即函数在执行完成前就可以先返回调用方,然后继续执行接下来的逻辑完成任务的函数
举例:
public async Task<int> DoSomethingAsync()
{
//创建一个计算1万毫秒的任务
Task<int> longRunningTask = LongRunningTaskAsync();
//使用await执行这个任务
int result = await longRunningTask;
return result;
}
//假装计算1w毫秒,输出为1
private async Task<int> LongRunningTaskAsync()
{
await Task.Delay(10000);//延迟10s
return 1;
}
1.需要使用async关键词
2.返回类型为:void
、Task
、Task<T>
和 IAsyncEnumerable<T>
3.命名规范:Async结尾
4.需要有await表达式
5.要有返回值
6.async函数只能被async函数调用
共有三个部分:
第一部分异步调用:Task<int> longRunningTask = LongRunningTaskAsync();
第二部分执行异步:int result = await longRunningTask;
第三部分异步方法:private async Task<int> LongRunningTaskAsync(){}
注:
- [ 在函数声明中,async关键字要放到返回类型之前 ]
- [ async函数本身不创建异步操作,只有在调用await的时候才会进行异步操作 ]
下面对之前的异步代码进行优化:
class Program
{
static void Main(string[] args)
{
Calculate();
Console.Read();
}
static async void Calculate()
{
var result1 = await Calculate1Async();
var result2 = await Calculate2Async(result1);
var result = await Calculate3Async(result1, result2);
Console.WriteLine(result);
}
static async Task<int> Calculate1Async()
{
var result = 3;
Console.WriteLine($"Calculate1: {result}");
await Task.Delay(2000);
return result;
}
static async Task<int> Calculate2Async(int a)
{
var result = a * 2;
Console.WriteLine($"Calculate2: {result}");
await Task.Delay(3000);
return result;
}
static async Task<int> Calculate3Async(int a, int b)
{
var result = a + b;
Console.WriteLine($"Calculate3: {result}");
await Task.Delay(1000);
return result;
}
}
十、Task VS. Thread
异步不是多线程!!!
异步用来处理并行,多线程用于处理并发
class Program
{
static void Main(string[] args)
{
TaskTest();
ThreadTest();
Console.Read();
}
static void TaskTest()
{
var sw = new Stopwatch();
sw.Start();
for(int i = 0; i < 100; i++)
{
Task.Factory.StartNew(() => { });
}
sw.Stop();
Console.WriteLine($"Task {sw.ElapsedMilliseconds}");
}
static void ThreadTest()
{
var sw = new Stopwatch();
sw.Start();
for (int i = 0; i < 100; i++)
{
new Thread(() => { }).Start();
}
sw.Stop();
Console.WriteLine($"Thread {sw.ElapsedMilliseconds}");
}
}
执行结果为
可以看到Task的执行速度要远高于Thread!
异步并不会创建线程,只是通过主线程来执行,同时开出一条分路来执行其他任务,非同步分别执行。但是在最后会创建一个非常轻量级的Worker Thread,用于通知主程序异步结束,也称为回调Call Back.