多线程基础知识
什么是进程
程序在服务器上运行,占据的计算资源集合,称之为进程。
进程之间不会相互干扰。
进程间通信比较困难
什么是线程
程序执行的最小单位,也有自己的计算资源
线程是属于进程,一个进程可以有多个线程
什么是多线程
一个进程里面,有多个线程并发执行
什么是Thread
Thread是一个封装类,是NetFramework对线程对象的抽象封装
通过Thread去完成的操作,最终是通过向操作系统请求得到的执行流
CurrentThread
CurrentThread表示当前线程
ManagedThreadId
ManagedThreadId是Net平台给Thread起的名字,一般是Int值,尽量不重复
任何异步多线程都离不开委托delegate
lambda
action
func
异步多线程
异步多线程发起调用,不等待结束,直接执行下一行代码。动作由新线程(子线程)来执行
同步单线程与异步多线程的区别
1.同步单线程界面卡顿,异步多线程界面不卡顿;
因为同步单线程的时候,主线程忙于处理逻辑计算,不能响应;异步多线程则将计算任务交给子线程,主线程已空闲,可以响应界面操作。
2.同步单线程处理事件较长,异步多线程处理快速;
同步单线程因为只有一个线程处理,异步多线程有多个线程并发计算。多线程是利用资源换取性能,短时间那些占用更多资源。
3.多线程的协调管理成本是存在的,多线程的效率不一定是线性增加的;
4.资源也一样上限,多线程就像有多辆车在路上行驶,但是车道的数量是有限;
5.线程也不是越多越好,管理成本会上升
6.多线程执行的无序性。启动无序(需要CPU分片处理),结束无序,执行时间不确定,同一线程同一任务耗时也不相同,这和操作系统的调度策略有关(CPU分片),线程优先级可以印象操作系统的调度
使用多线程不要通过延时去掌控顺序
多线程进行顺序控制
1.异步回调
2.IsComplete属性。用于判断当前异步操作是否完成
3. 利用信号量控制。WaitOne完成异步等待
阻塞当前线程,直到收到信号量,从AsyncResult发出,无延迟
waitOne(-1);//表示一直等待
waitOne(100);//最多等100ms,超时不再等待
4.EndInvoke获取异步结果返回值
如果想获取异步调用的真是返回值,只能使用Func委托中的EndInvoke
各版本多线程比对
1. .NetFrameWork 1.0 1.1
ThreadStart start = ()=>
{
//逻辑处理
};
Thread thread = new Thread(start);
thread.Start();
优点
1.Thread的API丰富,如挂起(Supend),恢复(Resume),等待(Join),后台线程,销毁(Abort),重置销毁(ResetAbort)
缺点
1.线程资源是操作系统管理的,响应并不灵敏,所以不好控制
2.Thread启动线程是没有限制的,可能导致死机
2…NetFrameWork 2.0(推出新的CLR)
ThreadPool—池化资源管理,就是做个容器,容器提前申请线程,程序需要使用线时,直接找容器获取,用往后再放回容器(控制状态),避免频繁的申请和销毁,容器会根据线程的数量去申请和释放
WaitCallback callback = o=>
{
//执行逻辑
};
ThreadPool.QueueUserWorkItem(callback);
优点
1.线程复用
2.可以限制最大线程数量,ThreadPool.SetMaxThreads(),ThreadPoolSetMinThreads
缺点
1.API太少,在线程等待和线程控制方面不足
3…NetFrameWork 3.0 Task
Task被称为多线程的最佳实践
Action action = ()=>
{
//执行逻辑
};
Task task = new Task(action);
task.Start();
优点
1.Task线程全部是线程池线程
2.提供了丰富的API,适合开发实践
并行编程Parallel
Parallel.Invoke(action,action1,action2);
特点
1.Parallel可以启动多线程,主线程也参与计算,可以节约一个线程
2.Parallel可以指定最大的并发数量,通过设置MaxDegreeOfParallelism属性
Task应用
List<Task> task_list = new List<Task>();
task_list .Add(Task.Run(()=>方法1(参数1,参数2)));
task_list .Add(Task.Run(()=>方法2(参数1,参数2)));
task_list .Add(Task.Run(()=>方法3(参数1,参数2)));
task_list .Add(Task.Run(()=>方法4(参数1,参数2)));
task_list .Add(Task.Run(()=>方法5(参数1,参数2)));
//急需要多线程提升性能,又需要在多线程全部执行完成后才能执行的操作
Task.WaitAll(task_list.ToArray());
//阻塞当前线程,直到任一任务结束---会阻塞当前现场,直到任务全部结束
Task.WaitAny(task_list.ToArray());
//优化,不卡顿界面
List<Task> task_list = new List<Task>();
task_list .Add(Task.Run(()=>方法1(参数1,参数2)));
task_list .Add(Task.Run(()=>方法2(参数1,参数2)));
task_list .Add(Task.Run(()=>方法3(参数1,参数2)));
task_list .Add(Task.Run(()=>方法4(参数1,参数2)));
task_list .Add(Task.Run(()=>方法5(参数1,参数2)));
//但是不推荐这样
//1.尽量不要线程嵌套,容易出问题
//2.这全部是子线程完成,子线程不能直接操作界面
Task.Run(()=>
{
//急需要多线程提升性能,又需要在多线程全部执行完成后才能执行的操作
Task.WaitAll(task_list.ToArray());
//阻塞当前线程,直到任一任务结束---会阻塞当前现场,直到任务全部结束
Task.WaitAny(task_list.ToArray());
});
//更好方法
TaskFactory taskFactory = new TaskFactory();
//等任一任务完成后,启动新的Task完成后续动作
taskFactory .ContinueWhenAny(task_list.ToArray(),tArray=>
{
//等待task_list执行完成后,执行的逻辑
});
//等全部任务完成后,启动新的Task完成后续动作
taskFactory.ContinueWhenAll(task_list.ToArray(),tArray=>
{
//等待task_list执行完成后,执行的逻辑
});
Task.WaitAll,Task.WaitAny会阻塞当前线程,直到全部任务结束
Continue的后续线程可能是新线程,可能是刚完成任务的线程,还可能是同一线程,但不可能是主线程
多线程安全问题
for(int i=0;i<5;i++)
{
Task.Run(()=>
{
Console.Writeline($"当前是{i}");
});
}
//输出的i结果全为 5,并没有出现0,1,2,3,4
//修改如下
for(int i=0;i<5;i++)
{
int j=i;//增加新变量
Task.Run(()=>
{
Console.Writeline($"当前是{j}");
});
}
//输入正常 0,1,2,3,4
多线程访问一个集合一般没有问题,线程安全问题一般都在修改一个对象的时候出现
List<int> intList = new List<int>();
for(int i=0;i<100000;i++)
{
intList.Add(i);
}
Console.WriteLine(intList.Count);//输出结果为100000
//改成多线程
for(int i=0;i<100000;i++)
{
Task.Run(()=>
{
intList.Add(i);
});
}
Console.WriteLine(intList.Count);//输出结果小于100000,有数据丢失,出现线程安全问题
//原因:
//List是数组结构,在内存上是连续摆放,如果在同一时刻去增加一个数组,会存在同一时间操作同一内存位置,操作后会出现数据被覆盖
线程安全问题解决
加锁
//锁
private static readonly object LOCK = new object();
List<int> intList = new List<int>();
for(int i=0;i<100000;i++)
{
Task.Run(()=>
{
lock(LOCK)
{
intList.Add(i);
}
});
}
Console.WriteLine(intList.Count);//输出结果为100000
//加lock可以解决线程安全问题,其实就是单线程化,lock保证方法块在任一时刻都是只有一个线程能操作,其他线程排队
Lock原理
Lock是一种语法糖,等价于Monitor,锁定一个内存引用,所以不能是值类型,也不能是null,因为需要占据一个引用
Lock相关测试
//新建类
public class TestLock
{
public static readonly object TESTLOCK_LOCK = new object();
public static void Show()
{
for(int i=0;i<5;i++)
{
Task.Run(()=>
{
lock(TESTLOCK_LOCK )
{
Console.Writeline($"开始{i} ");
Thread.Sleep(2000);
Console.Writeline($"结束{i}");
}
}
});
}
}
}
TestLock.Show();
//外部多线程
for(int i=0;i<5;i++)
{
Task.Run(()=>
{
lock(TestLock.TESTLOCK_LOCK )//使用的是同一个锁对象
{
Console.Writeline($"开始{i} ");
Thread.Sleep(2000);
Console.Writeline($"结束{i}");
}
}
});
}
//使用同一对象锁,会等待同一个对象锁释放,会出现阻塞
锁 锁定的是一个内存引用
锁的使用和锁关键字this
//新建类
public class TestLockGeneric<T>
{
public static readonly object TESTLOCK_LOCK = new object();
public static void Show(int index)
{
for(int i=0;i<5;i++)
{
Task.Run(()=>
{
lock(TESTLOCK_LOCK )
{
Console.Writeline($"开始{i} TestLockGeneric {index}");
Thread.Sleep(2000);
Console.Writeline($"结束{i} TestLockGeneric {index}");
}
}
});
}
}
}
TestLockGeneric<int>.Show(1);
TestLockGeneric<int>.Show(2);
TestLockGeneric<TestLock>.Show(3);
//单词调用内部是加锁顺序执行
//1 ,2不能并发,因为是同一变量
//1 ,3能并发,因为是同变量,因为泛型类,在类型参数相同时,是同一个类;类型参数不同时,是不同的类
锁this变量
public class TestLock
{
public void ShowThis(int index)
{
for(int i=0;i<5;i++)
{
Task.Run(()=>
{
lock(this)//this是当前变量
{
Console.Writeline($"开始{i} ShowThis {index}");
Thread.Sleep(2000);
Console.Writeline($"结束{i} ShowThis{index}");
}
}
});
}
}
}
TestLock lock1 = new TestLock();
lock1.ShowThis(1);
TestLock lock2 = new TestLock();
lock2.ShowThis(2);
//可以并发执行,因为在不同的变量里面This是不同的
await 、async
1.await/async是个新语法,出现在C#5.0 .NetFramwork 4.5以上 (CLR4.0)
2.是一个语法糖(编译器提供的新功能),不是一个全新的异步多线程使用方式
3.他本身不会产生新的线程,但是依托于Task而存在,所以程序执行时也是多线程的
语法
1.async可以随便添加,可以不用await
2.await只能出现在Task前面,但是方法必须声明async,不能单独出现
3.await/async之后原本没有返回值的,返回Task