C#中多线程的初步使用---个人笔记
c#多线程
Thread 多线程
什么叫做进程?
进程:是一个计算机概念,占用的CPU 内存 磁盘...打包到一起 就是一个进程
程序在服务器上运行时,占据的计算资源合集,就叫做进程,进程之间不会相互干扰
-----进程之间的通讯会比较困难(分布式)
线程:程序执行的最小单位,响应操作的最小的执行流
线程也包含自己的计算资源
线程是属于进程的,一个进程有多个线程
多线程:一个进程里面,有多个线程的并发执行
任何的异步多线程:都离不开委托delegate--lambda--action/func
多线程:Thread 是一个封装,是.netFramework对线程对象的抽象封装
通过Thread 去完成的操作,最终是通过操作系统请求得到的执行流
Thread中常用的
CurrentThread:当前线程-----任何操作执行都是线程完成的,运行当前代码的线程
ManagedThreadId: 是CLR给Thread起的名字 就是一个int 值,尽量不重复
同步单线程方法:按顺序执行,每次调用完成之后才能进入下一行,就一个线程来运行
异步多线程: 发起调用,不等待结束,直接进入下一行(主线程),动作会由一个新线程来执行(子线程) 这样线程并发了
可以在委托之后BeginInvoke 这就是异步的多线程之一
BeginInvoke上万次,可以开上万个线程么?
不可以 , 有线程池---ThreadPool---- 有上线的
Thread--可以启动那么多个,但是会死机
同步的单线程会卡页面 会占用页面UI线程 导致页面假死
用同步很容易阻塞线程
异步的多线程方法不会卡页面: 计算的任务都在创建的子线程中,主页面UI线程就不会卡
多线程:就是资源换性能 占用更多的资源 换更好的性能
线程并不是越多越好,在资源不够的情况下,有可能会起到反作用
多线程的无序性:
启动无序,结束无序 同一个时间启动 结束不一样时间
执行时间的不确定性 和 同步单线程一样 即使是同个线程 一个任务的耗时也是不一定的
跟操作系统的调度策略有关,CPU分片(计算能力强,1s可以拆分成1000份,宏观上就是并发了)
线程的优先级可以影响操作系统的优先级的调度
正是因为多线程具备不可预测性,很多时候想法都不一定能够贯彻实施
也许大多数情况下Ok,总有一定概率出问题
什么场景下需要用异步多线程
需要在业务操作后记录日志
***如果有顺序要求 并且有需要多线程的效率
BeginInvoke 第一个是触发的异步多线程
第一个参数是 根据你Action需要的参数传入的对象
第二个是回调委托,在程序结束之后进行一个回调,执行你写的AsyncCallback委托
最后一个是可以根据 AsyncCallback写的委托的 属性 AsyncState这个属性调出来
BeginInvoke 返回值是 IAsyncResult类型的接口
可以根据 IAsyncResult类型的接口创建的对象的属性 IsCompleted 来判断这个异步多线程动作是否结束
IAsyncResult asyncResult= action.BeginInvoke("傻逼", callback, new List<int> { 1,23,4,5});
类似: While(!asyncResult.IsCompleted){}
IsCompleted的值 异步操作完成的时候 为True 没有完成则为Flase
所以我们这边取反 进入这个While 进行一个 就是操作未完成的时候的 提示页面上面的
IsCompleted有缺陷 每次都要判断这个 属性 这边会造成误差
这个可以用 信号量来解决
WiatOne多线程等待操作
利用 WiatOne 来进行多线程等待
asyncResult.AsyncWaitHandle.WaitOne();
//一直等待 会阻塞当前线程,直到收到从asyncResult收到的信号量
asyncResult.AsyncWaitHandle.WaitOne(1000); 可以填写任意数字 单位ms 等待且最多等待的意思
//等待且最多只等待 1000ms,这个可以用来做超时控制,在通讯PLC 通讯硬件的时候 可以使用
这个操作主要是为了 在异步操作之后 有可能还有什么读写操作 等等, 等待这些全部都完了
我们用WiatOne可以 进行一个阻塞 把线程挡在这边 等所有操作完成之后才进行下一步操作
有返回值的异步多线程 :
Func<int> a = this.ABC; ABC为返回值为int的 方法
IAsyncResult asyncResult2 = a.BeginInvoke(null, null); 创建异步多线程asyncResult2
int data= a.EndInvoke(asyncResult2); 根据IasyncResult 为参数 调用 EndInvoke来获取这个带返回值参数的 异步返回值
这个EndInvoke 也可以放在 BeginInvoke的回调中去做
IAsyncResult asyncResult2 = a.BeginInvoke("a", ar => {
int mm=a.EndInvoke(ar);}, null);
*****不能多次EndInvoke 会报错
.Net Framework 1.0 1.1
Thread
实例化之后需要各种参数
我这边写 ThreadStart 为参数的
ThreadStart threadStart = () => {
MessageBox.Show("123");
Thread.Sleep(1000);
MessageBox.Show("456");
};
Thread thread = new Thread(threadStart);
thread.Start();
使用的这种方法是 .Net Framework1.0 1.1 时代 使用Thread的方法 ,多线程用法 也不会卡见面
Thread的API非常的丰富
Thread.Supend();挂起
Thread.Resume();恢复
Thread.Join();在当前多线程等待
Thread.IsBackground 用来设置前台线程后台线程
Thread.Abort();销毁线程
Thread.ResetAbort(); 能销毁线程之后 又可以重置回来
这些都可以很花哨,丰富,但是玩不好,因为线程资源 都是操作系统管理的
.net封装的Thread 只是一个发给操作系统的一个命令, 并不是那么灵敏,和好控制
这些都是比较早的API写法.Net Framework1.0 1.1 时代
.NetFramework 2.0时代(新的CLR,泛型也是这个时代出的)
ThreadPool 池化资源管理设计思想,线程是一种资源,之前每次要用线程
就是申请一个线程,使用完之后就释放掉;
线程池:池化之后 做了一个容器,容器提前申请(XXX)个线程,程序需要使用线程
直接找容器获取,用完在放回容器中(控制状态) 避免频繁的申请和销毁
池化的容器自己会根据闲置的数量去申请和释放;
1. 线程复用 2. 可以限制最大线程数量
后面的那些Task await async 那些都是从线程池中拿的线程
ThreadPool
缺点:API太少了,线程等待顺序控制特别若.会有个MRE 很麻烦 影响了
ThreadPool之后的 Parallel 也是多线程的方法
Parallel.Invoke(()=>{
Messagebox.Show(); , Messagebox.Show(); , Messagebox.Show();
});
也把主线程也参与线程工作 也会卡死界面
Parallel
也可以通过ParallelOptions 轻松控制最大并发数量
********************* T A S K ************************
Task.ContinueWith//Task的回调
Parallel 之后 .NetFramework 3.0 Task是多线程的最好用的
Task线程全部都是线程池线程 可以带来线程池的好处
提供了丰富的API 等待....回调...批量等待...批量回调....
既需要多线程来提升性能,有需要在多线程全部完成之后执行 一个操作
这个时候 可以 用 Task.WaitAll(); 中间需要传的参数就是 我们执行的任务
只有执行完这些任务 才会进入下一步
MessageBox.Show("开始");
List<Task> taskList= new List<Task>();
taskList.Add(Task.Run(() => {
MessageBox.Show("运行1");
}));
taskList.Add(Task.Run(() => {
MessageBox.Show("运行2");
}));
taskList.Add(Task.Run(() => {
MessageBox.Show("运行3");
}));
taskList.Add(Task.Run(() => {
MessageBox.Show("运行4");
}));
Task.WaitAll(taskList.ToArray());
MessageBox.Show("结束");
这上面的代码 如果没有WaitAll 就会导致 没有执行中间我们要多线程执行的
运行1~ 运行4 会在 结束之后才开始出来
所以我们这边用WaitAll来做 就可以让程序在执行运行操作之后 才结束
Task.WaitAll 是当前线程的 所以 会阻塞当前线程 当前线程被阻塞 会卡界面
Task.WaitAny(taskList.ToArray());
会阻塞当前线程,任意一个任务完成之后就 进入下一步 也会阻塞当前线程
Task.WaitAll 和 Task.WaitAny 一个是根据全部完成了才进行下一步 一个是完成了任意一个任务就进行下一步
List<Task> taskList = new List<Task>();
taskList.Add(Task.Run(() =>
{
MessageBox.Show("运行1");
}));
taskList.Add(Task.Run(() =>
{
MessageBox.Show("运行2");
}));
taskList.Add(Task.Run(() =>
{
MessageBox.Show("运行3");
}));
taskList.Add(Task.Run(() =>
{
MessageBox.Show("运行4");
}));
Task.Run(() =>
{
Task.WaitAny(taskList.ToArray());
MessageBox.Show("已经完成了一部分");
Task.WaitAll(taskList.ToArray());
MessageBox.Show("已经全部完成");
});
这个方法 可以实现 不卡线程 用一层Task来包住 Task.WaitAll和Task.WaitAny 这样不会卡线程
但是这个方法 是线程套线程 会有不好的效果 所以 尽量不要线程套线程
这里全都是子线程完成的, 子线程也不能直接操作界面
********尽量不要线程套线程**********
TaskFactory
TaskFactory taskFactory = new TaskFactory();
//等着任意一个完成任务后 进入委托
taskFactory.ContinueWhenAny(taskList.ToArray(), t => {
MessageBox.Show("完成了其中一个");
});
//等着全部任务完成后,启动一个新的Task来完成后续动作
taskFactory.ContinueWhenAll(taskList.ToArray(), tArray => {
MessageBox.Show("都完成了");
});
和之前用的BeginInvoke 里面的那个回调方法很像 可以根据选择 全部完成之后
在进行一个多线程委托的操作 都是在多线程里面做的
taskFactory.ContinueWhenAny
taskFactory.ContinueWhenAll
这两个方法 后续线程都是 新的线程 也有可能是刚完成任务的线程
也有可能是同一个线程 这个每次都不一定 是不确定的;
taskFactory.ContinueWhenAny 和 WaitAny 这两个方法启动的顺序是没办法排序的
taskFactory.ContinueWhenAll 和 WaitAll 这两个方法启动的顺序是没办法排序的
多线程的不可预测性
如果需要就是 我们程序前面完成之后 在完成后面的操作的 我们可以进行一个 taskList.Add
把我们
taskList.Add(taskFactory.ContinueWhenAll(taskList.ToArray(), tArray => {
Console.WriteLine("都完成了");
}));
这一段话 我们添加进去 之前创建的taskList 这个 List<Task>集合之中
之后我们用 Task.WaitAll(taskList.ToArray()); 这样我们就可以等待这个都完成了
********************* 多线程安全 ************************
多线程安全问题:一段代码,单线程执行和多线程执行结果不一致,说明有线程安全问题
在一个循环中 我们 原本写的是for 正常把I作为参数 给我们的集合 或者数组中当做索引
但是 有多线程了之后 我们就会发现 这个是错的 要嘛都是 索引都是最大的
或者直接报错 outofrange 这个错误
所以我们在for循环里面 或者别的什么地方的时候 就应该 我们要创建一个变量 来接收这个 I的值
每一次都是创建一个新的变量 这样就不会出现问题
错误代码:
for (int i = 0; i < 5; i++)
{
Task.Run(() => {
MessageBox.Show($"{i} Start...{Thread.CurrentThread.ManagedThreadId}");
Thread.Sleep(1000);
MessageBox.Show($"{i} End...{Thread.CurrentThread.ManagedThreadId}");
});
}
最后输出的 结果 那个I 都是 5
正确代码 :
for (int i = 0; i < 5; i++)
{
int k = i;
Task.Run(() => {
MessageBox.Show($"{i} Start...{Thread.CurrentThread.ManagedThreadId}");
Thread.Sleep(1000);
MessageBox.Show($"{i} End...{Thread.CurrentThread.ManagedThreadId}");
});
}
加上一个数据 来接收我们 正在变的 I 之后每一次递增的时候
都会创建一个 Int K 来接收我们的I 这样就不会出现 你想索引到的 地方 不一样了
多线程安全问题 最主要的问题 是在修改同一个对象中的时候会出现
在多线程赋值的时候 有可能会有丢失数据的情况
为什么会数据丢失?
List<int> listint = new List<int> { };
for (int i = 0; i < 10000; i++)
{
Task.Run(() => {
listint.Add(i);
});
}
这段代码就是经典的线程安全问题 最后listint的 Count 会小于10000
分析:
List是个数组结构,在内存上面是连续摆放,假如同一个时间,去增加一个数据
既然是连续摆放的,那这个结构就是固定的 0就在 0号位上面 1就在1号位上面
像架子一样 上面一个个摆放齐整的, 但是多线程是什么 是操作系统的执行流,
你来了多个线程,都在操作同一个List, 我操作了 索引1这个数据 你也是往这个 内存位置
索引1发送数据 , 内存先把我的 数据存入 内存位置索引1 之后在将 你的数据存入 内存位置索引1
这样就操作了同一个 内存位置索引1
那这样就引起了 数据的丢失 导致上面的那段代码的 数据长度不为10000
解决方案: 加锁
标准锁的写法: private static readonly object LOCK = new object();
for (int i = 0; i < 10000; i++)
{
Task.Run(() =>
{
lock (LOCK)
{
listint.Add(i);
}
});
}
********************* lock 锁 ************************
加lock之后 就可以解决线程安全问题
是怎么解决线程安全问题的?
就是单线程化
lock 就是保证方法块,任意时候都只有一个线程能进去,其他线程进行一个排队
lock的原理: 是一个语法塘
lock (LOCK) {
}
这种写法 实际上的语法是
Monitor.Enter(LOCK); Monitor.Exit(LOCK)
锁定一个内存引用 (引用类型)
引用类型的定义是 栈 上存着引用类型的地址 指向 堆去上分配的一块数据区域
lock就是 那个指向引用类型的, 所以lock要求是引用类型的
*lock的对象不能是一个空对象 null //
lock可以看成第一个来的线程 占据了 锁的那个对象 , 第二个线程来的时候
看到你这个对象被占据了 只能等第一个来的 做完他的事情 第二个线程才可以开始 以此类推.....
如果·我们在程序中 我们需要一个第一个方法 多线程并发 用lock之后
我们在另外一个程序中 也用到lock的那个对象 这样就会看顺序 之后执行 这样就会阻塞 两个会按顺序执行
如果没有lock同一个对象 就可以两个多线程lock锁之后并发
如果共用一个锁变量 就会出现相互阻塞 如果想阻塞就共用一个 lock锁变量
锁相同变量----阻塞 锁不同变量----并发
锁引用变量类型都可以并发 但是 string引用类型有特殊情况
我们lock 锁的是内存引用 ------字符串是享元的
享元的概念:
string a="123" ; string b =“123”;
这两个都在堆中只开辟了一个内存 就是"123"
所以在lock锁的时候 只要是 string类型只要两个的值一样 就会造成阻塞
就不能并发 两个lock锁的内容就并发不了
同样的泛型变量int 也不能并发
泛型类,在类型参数相同的时候,是同一个类
解释:
在普通的泛型类里面 例子: ABC<T> 中 有 一个静态字段
private static readonly object D = new object();
有一个方法 Show(int ddd);//Show方法里面有用使用多线程 并且用lock 锁 来锁这个D字段
我们在外部调用的时候 ABC<int>.Show(111); //// 1
ABC<int>.Show(6666); //// 2
ABC<string>.Show(111); //// 3
这样调用之后 我们的ABC<int> 两个同样使用int的泛型类
他们其实在内存中 指向的区域是同一个。
之前刚学Static的时候 说 Static静态字段是唯一的
其实在泛型类中 他并不是唯一的
泛型类只有传不同类型的参数的时候 他就会再次创建一个内存堆区
所以 这就是为什么 1和2会相互阻塞 不能并发 , 2或者1 和 3都可以并发的原因
********************* await async ************************
await async 是一个新语法,出现在C#5.0 .netFramework在4.5及以上版本(CLR在4.0及以上)
await async 是一个语法糖 不是一个全新的异步多线程的使用方式
本身并不会启动(产生) 新的线程,但是依托于 Task存在, 所以程序执行过程中, 会有多线程
async可以不搭配await
但是await 不能没有async 不能单独使用, 而且只能出现在Task前面
await/async可以不用写 return
有了await/async之后 原本没有返回值,可以返回Task
原本返回(XXX)类型的 ,可以返回Task<XXX>类型的
并且await 之后要跟Task await task 后的 也是使用 Task的线程
都是子线程 不是主线程 但是 在一个方法里面 Task前面的 会跟调用这个方法地方的线程一致
public async Task DoSomething()
{
Console.WriteLine($"DoSomething+{Thread.CurrentThread.ManagedThreadId}");
Task task= Task.Run(() => {
Console.WriteLine($"内部+{Thread.CurrentThread.ManagedThreadId}");
});
await task;
Console.WriteLine($"DoSomethingData+{Thread.CurrentThread.ManagedThreadId}");
}
我在主线程 调用这个方法 那 方法第一句 就是 跟着主线程走的, 之后我们Task出来一个多线程委托
在多线程内部 写了个输出, 最后Task后 跟着 await task(这个await 必须在Task之后)
await task 之后的 那一句程序 也是使用Task的线程 、
如果没有await 最后一段程序 就是调用地方的线程执行的
可以认为 加了 await 就等同于将 await后面的代码,包装成一个回调
await 后 之后在使用await Task.Run 会在开启一个线程
Console.WriteLine($"DoSomething+{Thread.CurrentThread.ManagedThreadId}");
Task task= Task.Run(() => {
Console.WriteLine($"内部+{Thread.CurrentThread.ManagedThreadId}");
});
await task;
Console.WriteLine($"DoSomethingData+{Thread.CurrentThread.ManagedThreadId}");
await Task.Run(() => {
Console.WriteLine($"第二次内部+{Thread.CurrentThread.ManagedThreadId}");
});
Console.WriteLine($"第二次外部+{Thread.CurrentThread.ManagedThreadId}");
结束完第一个 Task的任务之后 会进入await 执行DoSomething 之后在进入 下面的Task.Run
最后在执行 第二次外部 那一段代码
这就是await 的执行顺序
await 可以进行顺序控制 在线程里面
可以用同步编程的形式,来进行去写异步编程
小总结: await/async 之前 我们为了并发,为了不阻塞线程,会才用异步多线程的方法
但是执行顺序又非常奇怪,需要调用什么 信号量,等待的方法来做。
await/async 之后 我们可以直接运用 async配合 await 进行一个 以同步的方式,来执行异步的代码
await/async 就只是一个语法糖
浙公网安备 33010602011771号