一、概述
1、进程
是操作系统分配资源的基本单位,每个进程都有自己的内存空间。当一个程序开始运行时,比如qq、微信、浏览器,他们都是进程。一个进程是由多个线程组成的
2、线程
操作系统调度的基本单位,是进程中的一个执行单元,一个进程可以包含多个线程,共享同一进程的内存空间和资源。
3、多线程
3.1、概念
多线程是指程序中包含多个执行流,即在一个程序中可以同时运行多个不同的线程来执行不同的任务,也就是说允许单个程序创建多个并行执行的线程来完成各自的任务。
3.2、优缺点
优点:
可以提高CPU的利用率。在多线程程序中,一个线程必须等待的时候,CPU可以运行其它的线程而不是等待,这样就大大提高了程序的效率。(牺牲空间计算资源,来换取时间)
缺点:
- 线程也是程序,所以线程运行需要占用计算机资源,线程越多占用资源也越多。(占内存多)
- 多线程需要协调和管理,所以需要CPU跟踪线程,消耗CPU资源。(占cpu多)
- 线程之间对共享资源的访问会相互影响,必须解决竞用共享资源的问题。(多线程存在资源共享问题)
- 线程太多会导致控制太复杂,最终可能造成很多Bug。(管理麻烦,容易产生bug)
3.3、多线程使用场景
何时建议使用多线程:
- 当主线程试图执行冗长的操作,但系统会卡界面,体验非常不好,这时候可以开辟一个新线程,来处理这项冗长的工作。
- 当请求别的数据库服务器、业务服务器等,可以开辟一个新线程,让主线程继续干别的事。
- 利用多线程拆分复杂运算,提高计算速度。
何时不建议使用多线程:
- 当单线程能很好解决,就不要为了使用多线程而用多线程。
4、同步,异步
同步:
线性执行,从上往下依次执行,同步方法执行慢,消耗的计算机资源少。
异步:
线程和线程之间,不再线型执行,多个线程总的耗时少,执行快,消耗的计算机资源多,各线程执行是无序的。
二、Thread
1、基本使用
static void Main(string[] args)
{
Thread thread = new Thread(() =>
{
new Program().Console_Method();
});
thread.Start();
Console.WriteLine($"主线程ID:{Thread.CurrentThread.ManagedThreadId}");
Console.WriteLine("主线程结束");
}
private void Console_Method()
{
for (int i = 0; i < 10; i++)
{
Console.WriteLine($"第{i}个子线程ID:{Thread.CurrentThread.ManagedThreadId}");
Thread.Sleep(1000);
}
}
2、线程等待
thread.Join();//主线程等待,直到当前线程thread执行完毕
//thread.Join(500);//主线程等待500毫秒,500ms后不管当前线程执行是否完毕,都继续往后执行
3、前、后台线程
ThreadPool中的线程都是后台线程,使用new Thread
方式创建的线程默认都是前台线程。
//可以通过IsBackground 属性来切换前、后台线程
thread.IsBackground = true;//后台线程,界面关闭,线程也就随之消失
thread.IsBackground = false;//前台线程,界面关闭,线程会等待执行完才结束
4、线程的优先级
但是设置优先级也只是提高了他被优先执行的概率
//通过属性Priority 设置线程的优先级
thread.Priority = ThreadPriority.Highest;//优先级最高
thread1.Priority = ThreadPriority.Lowest;//优先级最低
5、简单实例
实现两个委托多线程顺序执行
static void Main(string[] args)
{
Action action1 = () =>
{
Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
};
Action action2 = () =>
{
Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
};
Method1(action1, action2);
}
private static void Method1(Action action1, Action action2)
{
Thread thread = new Thread(() =>
{
action1();
action2();
});
thread.Start();
Console.WriteLine("俩线程执行结束");
}
三、ThreadPool
如果需要使用线程,就可以直接到线程池中去获取直接使用,如果使用完毕,在自动的回放到线程池去;
1、线程池如何分配一个线程
QueueUserWorkItem方法,将方法排入队列以便开启异步线程,它有两个重载。
QueueUserWorkItem(WaitCallback callBack),WaitCallback是一个有一个object类型参数且无返回值的委托。
QueueUserWorkItem(WaitCallback callBack, object state),WaitCallback是一个有一个object类型参数且无返回值的委托,state即WaitCallback中需要的参数, 不推荐这么使用,存在拆箱装箱的转换问题,影响性能。
ThreadPool.QueueUserWorkItem(o =>
{
Console.WriteLine("ok1");
Console.WriteLine(o);
}, "kawa1");
Console.WriteLine("ok2");//要多写个这个,不然不会打印上面那俩
2、线程池如何控制线程数量
通过SetMinThreads/SetMaxThreads来设置线程的数量
ThreadPool.SetMaxThreads(5,8);//第一个参数是线程池中的工作线程数,第二个是线程池中异步 I/O 线程的数目
四、Task
Task是在ThreadPool的基础上进行的封装,远胜于Thread和ThreadPool
1、Task线程开启
//1、
Task task1 = new Task(() =>
{
Console.WriteLine("task1");
});
task1.Start();
Task<string> task2 = new Task<string>(() =>
{
Console.WriteLine("task2");
return "task2";
});
task2.Start();
//2、
Task task3 = Task.Run(() =>
{
Console.WriteLine("task3");
});
Task<string> task4 = Task.Run<string>(() =>
{
Console.WriteLine("task4");
return "task4";
});
//3、也可以用TaskFactory
Task task5 = Task.Factory.StartNew(() =>
{
Console.WriteLine("task5");
});
Task<string> task6 = Task.Factory.StartNew<string>(() =>
{
Console.WriteLine("task6");
return "task6";
});
Console.WriteLine(task6.Result);//task6
2、线程等待
2.1、task.Wait()
Task实例方法,等待task内部执行完毕,才会往后执行,卡主线程。
task.Wait(1000);//等待1000毫秒后就往后执行不管task有没有执行结束
2.2、Task.WaitAny()
Task静态方法,任意一个任务执行结束后,去触发一个动作,卡主线程
与之对应的还有Task.WaitAll()
场景:数据有可能是来自于第三方接口,缓存,数据库,查询的时候,我们不确定,开启几个线程同时查询,只要一个返回了就返回界面
List<Task> tasks = new List<Task>();
TaskFactory taskFactory = new TaskFactory();
tasks.Add(taskFactory.StartNew(() =>
{
Thread.Sleep(2000);//模拟拿数据的时间
Console.WriteLine("从缓存拿到了数据");
}));
tasks.Add(taskFactory.StartNew(() =>
{
Thread.Sleep(1000);//模拟拿数据的时间
Console.WriteLine("从第三方接口拿到了数据");
}));
tasks.Add(taskFactory.StartNew(() =>
{
Thread.Sleep(4000);//模拟拿数据的时间
Console.WriteLine("从数据库拿到了数据");
}));
Task.WaitAny(tasks.ToArray());
Console.WriteLine("拿到了数据");
//从第三方接口拿到了数据
//拿到了数据
2.3、Task.WhenAny()
Task静态方法,当传入的线程中任何一个线程执行完毕,继续执行ContinueWith中的任务,不卡主线程
与之对应的还有Task.WhenAll()
List<Task> tasks = new List<Task>();
TaskFactory taskFactory = new TaskFactory();
tasks.Add(taskFactory.StartNew(() =>
{
Thread.Sleep(2000);//模拟拿数据的时间
Console.WriteLine("从缓存拿到了数据");
}));
tasks.Add(taskFactory.StartNew(() =>
{
Thread.Sleep(1000);//模拟拿数据的时间
Console.WriteLine("从第三方接口拿到了数据");
}));
tasks.Add(taskFactory.StartNew(() =>
{
Thread.Sleep(4000);//模拟拿数据的时间
Console.WriteLine("从数据库拿到了数据");
}));
//Task.WhenAny(tasks);//如果执行这个,并且不Thread.Sleep(1000);,则只会打印拿到了数据,因为上面的都是后台线程
Task.WhenAny(tasks).ContinueWith(o =>
{
Console.WriteLine(o.Result);
});
Thread.Sleep(1000);
Console.WriteLine("拿到了数据");
//从第三方接口拿到了数据
//System.Threading.Tasks.Task
//拿到了数据
2.4、factory.ContinueWhenAny()
TaskFactory实例方法。某一个任务执行结束后,去触发一个动作,等价于WhenAny+ContinueWith,不卡主线程
与之对应的还有ContinueWhenAll()
List<Task> tasks = new List<Task>();
TaskFactory factory = new TaskFactory();
tasks.Add(factory.StartNew(() =>
{
Thread.Sleep(2000);//模拟拿数据的时间
Console.WriteLine("从缓存拿到了数据");
}));
tasks.Add(factory.StartNew(() =>
{
Thread.Sleep(1000);//模拟拿数据的时间
Console.WriteLine("从第三方接口拿到了数据");
}));
tasks.Add(factory.StartNew(() =>
{
Thread.Sleep(4000);//模拟拿数据的时间
Console.WriteLine("从数据库拿到了数据");
}));
factory.ContinueWhenAny(tasks.ToArray(), task =>
{
Console.WriteLine(task.GetType());
});
Thread.Sleep(1000);
Console.WriteLine("拿到了数据");
//拿到了数据
//从第三方接口拿到了数据
//System.Threading.Tasks.Task
3、TaskCreationOptions枚举类详解
一个Task内部,可以开启线程,Task内部的线程可以理解为子线程,Task为父线程,创建Task实例的时候可以传入TaskCreationOptions枚举参数来影响线程的运行方式。
3.1、None,默认
父线程不会等待子线程执行结束才结束。
Task task = new Task(() =>
{
Task.Run(() =>
{
Console.WriteLine("子线程task1");
});
Task.Run(() =>
{
Console.WriteLine("子线程task2");
});
Console.WriteLine("父线程task");
});
task.Start();
Console.WriteLine("结束");
//结束
//父线程task
//子线程task1
//子线程task2
3.2、AttachedToParent
子线程附加到父线程,父线程必须等待所有子线程执行结束才能结束
Task task = new Task(() =>
{
Task task1 = new Task(() =>
{
Thread.Sleep(1000);
Console.WriteLine("子线程task1");
},TaskCreationOptions.AttachedToParent);
Task task2 = new Task(() =>
{
Thread.Sleep(1000);
Console.WriteLine("子线程task2");
}, TaskCreationOptions.AttachedToParent);
task1.Start();
task2.Start();
Console.WriteLine("父线程task");
});
task.Start();
task.Wait();//需要加等待,不然会不执行上面的后台线程
Console.WriteLine("结束");
//父线程task
//子线程task2
//子线程task1
//结束
3.3、PreferFairness
相对来说比较公平,先申请的线程优先执行。
Task task1 = new Task(() =>
{
Console.WriteLine("task1");
},TaskCreationOptions.PreferFairness);
Task task2 = new Task(() =>
{
Console.WriteLine("task2");
},TaskCreationOptions.PreferFairness);
task1.Start();
task2.Start();
Console.WriteLine("结束");
//task1
//结束
//task2
3.4、其他
LongRunning 事先知道是长时间执行的线程就加这个参数,线程调度会优化。
4、延迟执行
Task.Delay() 不卡主线程
//开启线程后,线程等待3000毫秒后执行动作,不卡主线程
Task.Delay(3000).ContinueWith(t =>
{
this.DoSomething("张三");
});
5、多线程捕获异常
5.1、线程不等待,捕捉不到异常
多线程中,如果发生异常,使用try-catch包裹,捕捉不到异常,因为异常还没发生,主线程已经执行结束
//捕捉不到异常
try
{
Task task = Task.Run(() =>
{
int i = 0;
int j = 10;
int k = j / i; //尝试除以0,会异常
});
}
catch (AggregateException aex)
{
foreach (var exception in aex.InnerExceptions)
{
Debug.WriteLine($"线程不等待:异常{exception.Message}");
}
}
5.2、线程不等待,线程内部捕捉异常
多线程中,如果要捕捉异常,可以在线程内部try-catch,可以捕捉到异常
//捕捉到异常
try
{
Task task = Task.Run(() =>
{
try
{
int i = 0;
int j = 10;
int k = j / i; //尝试除以0,会异常
}
catch (Exception ex)
{
Debug.WriteLine($"线程内异常{ex.Message}");
}
Console.WriteLine("线程内部");
});
}
catch (AggregateException aex)
{
foreach (var exception in aex.InnerExceptions)
{
Debug.WriteLine($"线程不等待:异常{exception.Message}");
}
}
5.3、线程等待,能够捕获异常
-
多线程中,如果要捕捉异常,需要设置主线程等待子线程执行结束,可以捕捉到异常
-
多线程内部发生异常后,抛出的异常类型是system.AggregateException
//捕捉到异常
try
{
Task task = Task.Run(() =>
{
int i = 0;
int j = 10;
int k = j / i; //尝试除以0,会异常
});
//线程等待
task.Wait();
}
catch (AggregateException aex)
{
foreach (var exception in aex.InnerExceptions)
{
Console.WriteLine($"线程等待:异常{exception.Message}");
}
}
6、线程取消
6.1、基础使用
CancellationTokenSource cts = new CancellationTokenSource();
//CancellationTokenSource cts = new CancellationTokenSource(2000);//2s后取消
cts.Token.Register(() =>
{
Console.WriteLine("这是线程取消后执行的回调函数");
});
Task.Run(() =>
{
while (!cts.IsCancellationRequested)
{
Console.WriteLine("aaa");
Thread.Sleep(1000);
}
});
Console.WriteLine("结束");
Thread.Sleep(5000);
cts.Cancel();//cts.CancelAfter(2000);//2s后取消
//结束
//aaa
//aaa
//aaa
//aaa
//aaa
//这是线程取消后执行的回调函数
6.2、CreateLinkedTokenSource组合取消
利用CreateLinkedTokenSource构建CancellationTokenSource的组合体,其中任何一个体取消,则组合体就取消
CancellationTokenSource source1 = new CancellationTokenSource();
CancellationTokenSource source2 = new CancellationTokenSource();
CancellationTokenSource source3 = new CancellationTokenSource();
var combineSource = CancellationTokenSource.CreateLinkedTokenSource(source1.Token, source2.Token,source3.Token);
source2.Cancel();//取消source2
Console.WriteLine($"source1.IsCancellationRequested={source1.IsCancellationRequested}");//false
Console.WriteLine($"source2.IsCancellationRequested={source2.IsCancellationRequested}");//true
Console.WriteLine($"source3.IsCancellationRequested={source3.IsCancellationRequested}");//false
Console.WriteLine($"combineSource.IsCancellationRequested={combineSource.IsCancellationRequested}");//true
7、线程返回值
Task<int> task1 = Task.Run(() =>
{
Console.WriteLine("task1");
return 2;
});
Task<string> task2 = task1.ContinueWith((t) =>
{
Console.WriteLine("task2");
//这里的t代表 task1
var num = t.Result + 2;
return num.ToString();
});
Console.WriteLine($"我是主线程,我要读取子线程task1的返回值为:{task1.Result}--{Thread.CurrentThread.ManagedThreadId.ToString("00")}--{DateTime.Now.ToString("HH:mm:ss.fff")}");
Console.WriteLine($"我是主线程,我要读取子线程task2的返回值为:{task2.Result}--{Thread.CurrentThread.ManagedThreadId.ToString("00")}--{DateTime.Now.ToString("HH:mm:ss.fff")}");
Console.WriteLine("结束:最后执行这句");//最后执行这句,因为获取获取线程的值时,会阻塞主线程
task1
task2
我是主线程,我要读取子线程task1的返回值为:2--01--13:51:36.514
我是主线程,我要读取子线程task2的返回值为:4--01--13:51:36.519
结束:最后执行这句
Task<int> task1 = Task.Run(() =>
{
Console.WriteLine("task1");
return 1;
});
Task<int> task2 = Task.Run(() =>
{
Console.WriteLine("task2");
return 2;
});
Task<Task<int>> task = Task.WhenAny(new Task<int>[2] { task1, task2 });
//下面的值可能是1,也可能是2
Console.WriteLine($"我是主线程,我要读取子线程的返回值为:{task.Result.Result}--{Thread.CurrentThread.ManagedThreadId.ToString("00")}--{DateTime.Now.ToString("HH:mm:ss.fff")}");
//task1
//task2
//我是主线程,我要读取子线程的返回值为:1--01--14:11:51.816
8、线程安全
线程安全:一段业务逻辑,单线程执行和多线程执行后的结果如果完全一致,是线程安全的,否则就是线程不安全的
8.1、加锁解决线程安全问题
- 锁的本质:是独占引用,加锁是反多线程的,可以解决线程安全问题,但是不推荐大家使用,加锁会影响性能
- 锁的标准写法: private readonly static object obj_Lock = new object(); 锁对象,不要去锁String锁This
List<int> intlist = new List<int>();
List<Task> tasklist = new List<Task>();
for (int i = 0; i < 10000; i++)
{
Task.Run(() =>
{
lock (obj_Lock)
{
intlist.Add(i);
}
});
}
Task.WaitAll(tasklist.ToArray());
Console.WriteLine($"intlist中有{intlist.Count}条数据");//10000
8.2、使用线程安全对象
BlockingCollection<int> blockinglist = new BlockingCollection<int>();
ConcurrentBag<int> conocurrentbag = new ConcurrentBag<int>();
ConcurrentDictionary<string, int> concurrentDictionary = new ConcurrentDictionary<string, int>();
ConcurrentQueue<int> concurrentQueue = new ConcurrentQueue<int>();
ConcurrentStack<int> concurrentStack = new ConcurrentStack<int>();
9、解决中间变量问题
9.1、单线程执行
for (int i = 0; i < 5; i++)
{
Debug.WriteLine($"ThreadID={Thread.CurrentThread.ManagedThreadId.ToString("00")}_i={i}");
}
9.2、多线程执行问题
Task开启线程的时候,延迟开启,在循环的时候,不会阻塞主线程,循环很快,线程执行业务逻辑的时候,循环已经结束了,i已经变成5了,所以打出来的都是5
for (int i = 0; i < 5; i++)
{
Task.Run(() =>
{
Debug.WriteLine($"ThreadID={Thread.CurrentThread.ManagedThreadId.ToString("00")}_i={i}");
});
}
9.3、解决方案:多线程执行+中间变量
可以另外定义个变量,在每次循环的时候赋值,循环多少次,就会有多少个k,每个线程使用的是每一次循环内部的k
for (int i = 0; i < 5; i++)
{
int k = i;
Task.Run(() =>
{
Debug.WriteLine($"ThreadID={Thread.CurrentThread.ManagedThreadId.ToString("00")}_k={ k}");
});
}
五、Parallel
1、基本使用
- 可以传入多个委托,多个委托中的内容是会开启线程来执行,执行的线程可能是新的线程,也可能是主线程
- 会阻塞主线程,相当于是主线程等待子线程执行结束
//会阻塞主线程
Parallel.Invoke(() =>Console.WriteLine("a1"),
() =>Console.WriteLine("a2"),
() =>Console.WriteLine("a3"));
Console.WriteLine("结束");
//a1
//a2
//a3
//结束
- 可以传入options.MaxDegreeOfParallelism来限制开启的线程数量,可以做到不影响线程池的线程数量又能控制当前执行所用的线程数量
ParallelOptions parallelOptions = new ParallelOptions();
parallelOptions.MaxDegreeOfParallelism = 2;
//会阻塞主线程
Parallel.Invoke(parallelOptions, () =>Console.WriteLine("a1"),
() =>Console.WriteLine("a2"),
() =>Console.WriteLine("a3"));
Console.WriteLine("结束");
//a1
//a2
//a3
//结束
- 把Parallel包在一个Task里面实现不卡主线程
Task.Run(() =>
{
Parallel.Invoke(() => Console.WriteLine("a1"),
() => Console.WriteLine("a2"),
() => Console.WriteLine("a3"));
});
Console.WriteLine("结束");
//结束
2、Parallel.For
实现循环开启线程执行动作,可以获取索引,可以控制开启的线程数量
Parallel.For(0, 10, index =>
{
Console.WriteLine($"index:{index}, Id:{Thread.CurrentThread.ManagedThreadId}");
});
Console.WriteLine("结束");
//index:0, Id:5
//index:1, Id:1
//index:2, Id:8
//index:3, Id:9
//index:4, Id:10
//index:5, Id:11
//index:6, Id:12
//index:7, Id:13
//index:8, Id:5
//index:9, Id:1
//结束
3、Parallel.ForEach
List<int> intlist = new List<int>() { 1, 2, 3, 5, 7, 11, 13, 17 };
Parallel.ForEach(intlist, index =>
{
Console.WriteLine($"index:{index}, Id:{Thread.CurrentThread.ManagedThreadId}");
});
Console.WriteLine("结束");
//index:7, Id:10
//index:1, Id:1
//index:3, Id:5
//index:2, Id:8
//index:5, Id:9
//index:11, Id:11
//index:13, Id:12
//index:17, Id:13
//结束