一、什么是多线程
多线程是指在一个程序中同时运行多个线程,每个线程都是独立的执行流。多线程可以提高程序的执行效率,特别是在需要同时执行多个任务的情况下。多线程可以同时进行多个任务,而不需要等待某个任务的完成才能进行下一个任务。多线程可以在单个程序中同时处理多个任务,并且可以充分利用多核处理器的能力。
主线程与子线程
主线程通常是程序启动时自动创建并执行的第一个线程。它是程序的入口点,在程序开始时启动,并在程序结束时终止,是整个程序的起点和终点,还负责管理由它创建的子线程,包括创建、启动、挂起、停止等操作。
子线程则是由主线程创建的额外线程。子线程可以并行执行,独立于主线程,通常用于执行耗时的任务,以避免阻塞主线程。子线程的生命周期可以独立于主线程,可以在主线程运行期间创建和终止,子线程的阻塞不会影响主线程的执行。
主线程和子线程之间的交互通常通过线程同步机制来实现。
二、ThreadPool
提供了一个线程池,用于管理和执行等待队列中的任务。使用线程池可以避免频繁地创建和销毁线程,从而提高应用程序的性能和响应能力。
线程池的工作方式是将任务放入队列,然后在线程创建后启动这些任务。如果线程数量超过了最大数量,超出数量的线程会排队等候,等其他线程执行完毕,再从队列中取出任务来执行。
1、使用
在Main中(代表主线程)。
使用QueueUserWorkItem方法将任务提交到线程池当中。
using NewTask;
//将方法提交到线程池当中
ThreadPool.QueueUserWorkItem(NewPool.NewThreadPool,"你好");//第一个参数需要执行的方法,第二个参数为传参
//主线程的线程ID
Console.WriteLine("我是主线程" + "我的线程ID:" + Thread.CurrentThread.ManagedThreadId);
//等待输入,避免主线程消亡
Console.ReadLine();
创建一个NewPool类(代表子线程) 。
public class NewPool
{
public static void NewThreadPool(Object message)
{
//接收传参输出
Console.WriteLine(message.ToString());
// 模拟耗时操作
Thread.Sleep(2000);
//子线程的线程ID
Console.WriteLine("我是子线程" + "我的线程ID:" + Thread.CurrentThread.ManagedThreadId);
}
}
NewThreadPool方法作为线程池中的一个任务被提交,并且主线程继续执行其他任务。当任务在线程池中运行时,主线程不会等待它完成。
注意:当主线程被干掉,那么子线程大概率也活不下来。
2、线程池大小
线程池的大小不是直接设置的,而是由系统根据工作负载和可用资源动态管理的,但是我们可以控制线程数量的最小值和最大值。
ThreadPool.SetMinThreads(10, 0); // 设置最小工作线程数为10
ThreadPool.SetMaxThreads(20, 20); // 设置最大工作线程数为20
进程的线程池的默认大小取决于虚拟地址空间的大小等各种因素,需求较低时,线程池线程的实际数量可能低于最小值。
ThreadPool的三个属性:
CompletedWorkItemCount 获取迄今为止已处理的工作项数。
PendingWorkItemCount 获取当前已加入处理队列的工作项数。
ThreadCount 获取当前存在的线程池线程数。
三、Task
Task实际上并不直接创建线程,而是利用线程池(ThreadPool)来执行工作,当你创建一个Task并调用时,它会被提交到线程池,线程池会负责安排一个线程来执行该任务,现在官方推荐使用Task。
1、创建任务
使用Task.Run来创建一个任务,提交到线程池处理。
Console.WriteLine("我是主线程ID:"+Thread.CurrentThread.ManagedThreadId);
//创建一个任务并交给线程池处理
Task task1 = Task.Run(() =>
{
Console.WriteLine("我是task1子线程ID:" + Thread.CurrentThread.ManagedThreadId);
});
Task task2 = Task.Run(() =>
{
Console.WriteLine("我是task2子线程ID:" + Thread.CurrentThread.ManagedThreadId);
});
//等待输入,避免主线程死亡
Console.ReadLine();
2、处理异常
Task抛出的异常会存在AggregateException中。
try
{
//创建任务
}
catch(AggregateException ae)
{
foreach (var innerException in ae.InnerExceptions)
{
Console.WriteLine(innerException.Message);
}
}
3、解决数据不一致
两个或多个线程在访问共享数据时(共同一个资源),由于它们的执行顺序不确定,导致最终结果依赖于线程的执行顺序,可能会导致数据不一致的问题。
创建一个Account类
public class Account
{
private readonly object _lock = new object(); // 锁对象
private int _mone;//余额
public Account(int @int)
{
_mone = @int;
}
//进行消费
public void Withdraw(int amount)
{
lock (_lock) // 获取锁
{
//判断余额
if (_mone >= amount)
{
_mone -= amount;
Console.WriteLine($"消费{amount}元,余额{_mone}元");
}
else
{
Console.WriteLine($"消费{amount}元,余额{_mone}元,余额不足");
}
} // 离开lock代码块释放锁
}
public int GetBalance()
{
// 这里不需要lock,因为只是读取数据,但如果有复合操作则仍需要锁保护
return _mone;
}
}
在进入Withdraw方法中lock确保只能有一个线程能够修改余额,当线程进入到lock时会获取锁,如果此时已经有一个线程占用该锁,那么第一个线程就会阻塞,一直等到第二个线程释放锁,这样就只会有一个线程能够使用该方法,避免了数据不一致的问题。
在Main中
using NewTask;
//初始一千余额
Account account = new Account(1000);
//创建任务并交给线程池
Task task1 = Task.Run(() => account.Withdraw(1000));
Task task2 = Task.Run(() => account.Withdraw(50));
//等待两个线程完成
await Task.WhenAll(task1, task2);
//查看余额
Console.WriteLine(account.GetBalance());
4、任务等待
(1)、Task.WhenAll
用于等待一组任务全部完成,它接受一个Task对象数组或IEnumerable<Task> 集合作为参数,可以方便地并行执行多个任务,并等待它们全部完成。
Console.WriteLine("我是主线程ID:" + Thread.CurrentThread.ManagedThreadId);
//创建一个任务并交给线程池处理
Task task1 = Task.Run(() =>
{
Console.WriteLine("我是task1子线程ID:" + Thread.CurrentThread.ManagedThreadId);
});
Task task2 = Task.Run(() =>
{
//模拟耗时操作
Thread.Sleep(2000);
Console.WriteLine("我是task2子线程ID:" + Thread.CurrentThread.ManagedThreadId);
});
//等待所有任务都完成再进行后续任务
await Task.WhenAll(task1,task2);
Console.WriteLine("我是下一步");
(2)、Task.WhenAny
用于等待一组任务中的任何一个完成,它接受一个Task对象数组或IEnumerable<Task> 集合作为参数,在需要并行执行多个任务,但只需要等待其中一个任务完成即可。
Console.WriteLine("我是主线程ID:" + Thread.CurrentThread.ManagedThreadId);
//创建一个任务并交给线程池处理
Task task1 = Task.Run(() =>
{
Console.WriteLine("我是task1子线程ID:" + Thread.CurrentThread.ManagedThreadId);
});
Task task2 = Task.Run(() =>
{
//模拟耗时操作
Thread.Sleep(2000);
Console.WriteLine("我是task2子线程ID:" + Thread.CurrentThread.ManagedThreadId);
});
//等待其中一个任务完成就进行后续任务
await Task.WhenAny(task1,task2);
Console.WriteLine("我是下一步");
Console.ReadLine();
(3)、Wait
阻塞当前线程,直到任务完成执行后面任务,该方法可以传入一个int类型的参数,如:task2.Wait(1000),表示阻塞1000毫秒后执行后续任务,如果等待的线程500毫秒完成,那么会在501毫秒执行后续任务,如果等待的线程2000毫秒完成,那么会在1000毫秒执行后续任务。
Console.WriteLine("我是主线程ID:" + Thread.CurrentThread.ManagedThreadId);
//创建一个任务并交给线程池处理
Task task1 = Task.Run(() =>
{
Console.WriteLine("我是task1子线程ID:" + Thread.CurrentThread.ManagedThreadId);
});
Task task2 = Task.Run(() =>
{
//模拟耗时操作
Thread.Sleep(2000);
Console.WriteLine("我是task2子线程ID:" + Thread.CurrentThread.ManagedThreadId);
});
//等待task2任务完成
task2.Wait();
Console.WriteLine("我是下一步");
其他的一些方法:
Task.WaitAll与Task.WhenAll类似,但是Task.WhenAll是异步,而Task.WaitAll是同步。
Task.WaitAny与Task.WhenAny类似,但是Task.WhenAny是异步,而Task.WaitAny是同步。
5、取消任务
取消一个Task任务通常使用CancellationToken,创建一个CancellationTokenSource实例,生成一个CancellationToken,将该属性传递到任务当中,任务需要定期检查IsCancellationRequested属性,用于判断是否发送了取消请求,所以如果没有检查,那么任务会一直执行下去。
创建一个类Doless。
public class Doless
{
public static void DoWork(CancellationToken token)
{
//判断任务是否取消
while (!token.IsCancellationRequested)
{
Console.WriteLine("你好");
Thread.Sleep(500); // 模拟耗时操作
}
}
}
在Main中。
using NewTask;
using (CancellationTokenSource cts = new CancellationTokenSource())
{
// 获取CancellationToken
CancellationToken token = cts.Token;
// 启动Task并传递CancellationToken
Task task = Task.Run(() => Doless.DoWork(token), token);
// 注册一个取消回调,当取消发生时执行
token.Register(() =>
{
Console.WriteLine("结束");
});
// 模拟一些工作,然后取消Task
Thread.Sleep(2000); // 等待一段时间
cts.Cancel(); // 发送取消请求
Console.ReadLine();
}