C# 多线程
概述
进程和线程
进程:指在系统中运行的一个应用程序。
线程:进程中的一个执行任务。一个进程至少有一个线程,一个进程可以有多个线程,多个线程可共享数据。
多线程
多线程:在一个程序中同时运行多个线程,每个线程执行各自的任务。
优点:使用多线程可以提高应用程序的响应能力,并利用多处理器或多核系统提高应用程序吞吐量。
缺点:死锁和争用条件
多线程适用场景:任务执行比较耗时的情况,也可以解决一些非常耗时的且长时间占用cpu资源的程序。
多线程的特点:
1、运行顺序不确定。
2、线程之间平行执行。
前台线程和后台线程
前台线程必须全部执行完,即使主线程关闭掉,这时进程仍然存活。后台线程在未执行完成时,如果前台线程关掉,则后台线程也会停掉。后台线程会随着主线程的关闭,而自动关闭。
补充
1、新创建的Thread默认是前台线程,可以通过设置IsBackground属性将其改为后台线程
2、线程池中的线程是后台线程
3、Task开启的线程是后台线程
4、前台线程适用场合:重要核心的,或者需要长时间等待的任务,例如:UI界面线程、发送数据的线程
5、后台线程适用:非核心且用于处理时间较短的任务适用。
Thread
Thread开启的线程默认都是后台线程
开启线程
//命名空间
using System.Threading;
Thread thread = new Thread(SayHi);
void SayHi()
{
Thread.Sleep(3000);
Console.WriteLine("Hello World!");
}
thread.Start();
线程传参
//传递单个参数,thread.Start只支持传一个参数
Thread thread = new Thread((fileName) =>
{
Console.WriteLine($"正在下载的文件名是{fileName}");
});
thread.Start("原神.apk");
//传递多个参数 定义一个专门类,通过构造函数传参
//自定义类
class WriteInfo
{
private string _name;
private int _age;
public WriteInfo(string name, int age)
{
_name = name;
_age = age;
}
public void ShowHumanInfo()
{
Console.WriteLine($"我叫{_name},今年{_age}岁");
}
}
public void ShowInfo(string name, int age)
{
WriteInfo info = new WriteInfo(name, age);
Thread t = new Thread(info.ShowHumanInfo);
t.Start();
}
常用属性
属性 | 描述 |
CurrentThread | 获取当前正在运行的线程 |
IsAlive | 获取当前线程的执行状态 |
IsBackground | 某个线程是否为后台线程 |
IsThreadPoolThread | 线程是否属于托管线程池 |
ManagedThreadId | 获取当前托管线程的唯一标识符 |
Name | 获取或设置线程的名称 |
Priority | 线程的优先级可以影响线程的调用顺序 |
ThreadState | 当前线程的状态 |
线程状态
Aborted | 线程状态包括 AbortRequested 并且该线程现在已死,但其状态尚未更改为 Stopped |
AbortRequested | 已对线程调用了 Abort(Object) 方法,但线程尚未收到试图终止它的挂起的 ThreadAbortException |
Background | 线程正作为后台线程执行。 此状态可以通过设置 IsBackground 属性来控制 |
Running | 线程已启动且尚未停止 |
Stopped | 线程已停止 |
StopRequested | 正在请求线程停止 |
Suspended | 线程已挂起 |
SuspendRequested | 正在请求线程挂起 |
Unstarted | 尚未对线程调用 Start() 方法 |
WaitSleepJoin | 线程已被阻止。 这可能是调用 Sleep(Int32) 或 Join()、请求锁定或在线程同步对象上等待的结果 |
常用方法
方法 | 描述 |
Start | 开启线程 |
Abort | 终止线程 |
Sleep | 暂停线程一段时间 |
Join | 阻塞调用线程,直到某个线程终止 |
例子:等待Thread线程完成后进行后续操作
// 创建并启动新线程
Thread newThread = new Thread(() =>
{
Console.WriteLine("子线程开始运行");
// 模拟耗时操作
Thread.Sleep(2000);
Console.WriteLine("子线程结束运行");
});
newThread.Start();
// 等待子线程结束
newThread.Join();
Console.WriteLine("主线程结束");
ThreadPool 线程池
1、线程池创建的线程默认都是后台线程,不能把池中的线程修改为前台线程,也不能修改线程池中优先级与名称。
2、线程池中的线程只能用于时间比较短的任务,如果后台线程需要长时间运行,则需要单独开启,不适合用线程池。
3、手动创建多个Thread线程可能会消耗较多性能,通过线程池可以提高效率。
4、缺点
ThreadPool
不支持线程的取消、完成、失败通知等操作;
ThreadPool
不支持线程执行的先后次序;
void ThreadPoolTest()
{
ThreadPool.QueueUserWorkItem(GenFile, "原神.apk");
ThreadPool.QueueUserWorkItem(GenFile, "王者荣耀.apk");
ThreadPool.QueueUserWorkItem(GenFile, "蛋仔排队.apk");
ThreadPool.QueueUserWorkItem(GenFile, "炉石传说.apk");
}
void GenFile(object fileName)
{
Thread.Sleep(3000);
Console.WriteLine($"生成了文件{fileName}.txt");
}
Task 任务
Task开启的线程是后台线程
开启线程
//第一种方式
Task task = new Task(() =>
{
});
task.Start();
//第二种方式 Task.Run
Task task = Task.Run(() =>
{
});
//第三种方式 TaskFactory
TaskFactory taskFactory = new TaskFactory();
Task task = taskFactory.StartNew(() =>
{
});
//第四种方式 Task.Factory
Task task = Task.Factory.StartNew(() =>
{
});
常用方法
方法 | 描述 |
Wait | 等待Task完成执行 |
WaitAny | 等待列表中任一Task完成执行,同步方法,会阻塞主线程 |
WaitAll | 等待列表中所有Task完成执行,同步方法,会阻塞主线程 |
WhenAny | 创建一个任务,该任务将在任一提供的任务完成时完成。异步方法,不会阻塞主线程 |
WhenAll | 创建一个任务,该任务将在数组中的所有 Task 对象完成时完成。异步方法,不会阻塞主线程 |
ContinueWith | 在任务完成后回调一个延续任务,参数是调用方的任务信息 |
//并行运行多个任务,等待任务都运行完后添加延续事件逻辑
List<Task> list = new List<Task>();
list.Add(Task.Run(() =>
{
Console.WriteLine("开始做菜:");
Thread.Sleep(3000);
Console.WriteLine("做好素菜了!");
}));
list.Add(Task.Run(() =>
{
Console.WriteLine("开始做菜:");
Thread.Sleep(5000);
Console.WriteLine("做好荤菜了!");
}));
Task.WhenAll(list).ContinueWith(t =>
{
Console.WriteLine("菜都做好了,开饭吧!");
});
WaitAll和WhenAll的区别?
1、Task.WaitAll 是一种同步方法,它会阻塞调用线程,直到所有提供的任务都已完成。当需要确保一组任务在继续之前已完成时,该方法很有用,但它以阻塞方式执行,这意味着调用 Task.WaitAll 的线程会被占用,直到所有任务都完成为止。
2、Task.WhenAll 是一种异步方法,当所有提供的任务都完成后,该方法将返回单个任务。与 Task.WaitAll 不同,它不会阻止调用线程。相反,它允许调用代码继续异步执行。
async/await + Task
async void TestAsync()
{
await Task.Run(() =>
{
Console.WriteLine("开始做菜:");
Thread.Sleep(3000);
Console.WriteLine("做好素菜了!");
});
Console.WriteLine("开饭了!");
}
多线程隐患
争用条件
程序的结果取决于两个或更多个线程中的哪一个先到达某一特定代码块时出现的一种 bug。 多次运行程序会产生不同的结果,并且无法预测任何给定运行的结果。
public int gameState;
void DeadLock()
{
Thread t1 = new Thread(ChangeMyState);
Thread t2 = new Thread(ChangeMyState);
t1.Start();
t2.Start();
}
void ChangeMyState()
{
while (true)
{
gameState++;
if (gameState == 100)
{
Console.WriteLine("啊哦,好像出现问题了");
}
gameState = 100;
}
}
解决方案:用 lock 语句锁定在线程中共享的资源。
public int gameState;
public object objLock = new object();
void DeadLock()
{
Thread t1 = new Thread(ChangeMyState);
Thread t2 = new Thread(ChangeMyState);
t1.Start();
t2.Start();
}
void ChangeMyState()
{
lock (objLock)
{
while (true)
{
gameState++;
if (gameState == 100)
{
Console.WriteLine("啊哦,好像出现问题了");
}
gameState = 100;
}
}
}
死锁
如果使用lock不当,就会产生死锁情况!
描述:两个线程中的每一个线程都尝试锁定另外一个线程已锁定的资源时,就会发生死锁,两个线程都不能继续执行。(公共资源被多个线程争抢,导致一个线程永远等待另一个线程释放资源的异常情况)
死锁的四个必要条件:
1.互斥条件:资源一次只能被一个线程占用。
2.持有并等待条件:线程持有一个资源并等待获取其他资源。
3.不可剥夺条件:线程已获得的资源条件不能被强行剥夺,只能由线程自己释放。
4.循环等待条件:存在一组线程,每个线程都在等待下一线程持有的资源,形成一个环形等待。
死锁问题复现
object resourceA = new object();
object resourceB = new object();
int result;
public void CheckDeadlock()
{
var thread1 = new Thread(Logic1);
var thread2 = new Thread(Logic2);
thread1.Start();
thread2.Start();
thread1.Join();
thread2.Join();
Console.WriteLine(result);
}
public void Logic1()
{
lock (resourceA)
{
Thread.Sleep(100);
lock (resourceB)
{
result += 1;
}
}
}
public void Logic2()
{
lock (resourceB)
{
Thread.Sleep(100);
lock (resourceA)
{
result += 2;
}
}
}
解决方案
方法一 调整锁的顺序
确保所有线程按相同的顺序请求锁。这可以打破死锁的循环等待条件。只要所有的线程都以相同的顺序请求资源,死锁就不会发生。
public void Logic1()
{
lock (resourceA)
{
Thread.Sleep(100);
lock (resourceB)
{
result += 1;
}
}
}
public void Logic2()
{
lock (resourceA)
{
Thread.Sleep(100);
lock (resourceB)
{
result += 2;
}
}
}
方法二 锁的超时机制
使用Monitor.TryEnter 来设置获取锁的超时时间。如果超过指定时间无法获取锁,线程可以退出或执行其他操作。
public void Thread1Work()
{
if (Monitor.TryEnter(resourceA, TimeSpan.FromSeconds(1))) // 尝试获取锁1,超时时间1秒
{
try
{
Console.WriteLine("Thread 1 acquired lock1");
Thread.Sleep(100);
if (Monitor.TryEnter(resourceB, TimeSpan.FromSeconds(1))) // 尝试获取锁2,超时时间1秒
{
try
{
result += 1;
}
finally
{
Monitor.Exit(resourceB); // 释放锁2
}
}
else
{
Console.WriteLine("Thread 1 failed to acquire lock2, potential deadlock detected.");
}
}
finally
{
Monitor.Exit(resourceA); // 释放锁1
}
}
else
{
Console.WriteLine("Thread 1 failed to acquire lock1, potential deadlock detected.");
}
}
public void Thread2Work()
{
if (Monitor.TryEnter(resourceB, TimeSpan.FromSeconds(1))) // 尝试获取锁2,超时时间1秒
{
try
{
Thread.Sleep(100);
if (Monitor.TryEnter(resourceA, TimeSpan.FromSeconds(1))) // 尝试获取锁1,超时时间1秒
{
try
{
result += 2;
}
finally
{
Monitor.Exit(resourceA); // 释放锁1
}
}
else
{
Console.WriteLine("Thread 2 failed to acquire lock1, potential deadlock detected.");
}
}
finally
{
Monitor.Exit(resourceB); // 释放锁2
}
}
else
{
Console.WriteLine("Thread 2 failed to acquire lock2, potential deadlock detected.");
}
}
方法三 减少锁的持有时间
尽量缩小锁定的范围,确保只有在修改共享资源时才持有锁。这样可以减少锁竞争,降低死锁发生的机率。
参考链接
一文搞懂C#多线程、并发、异步、同步、并行 - biubiu12138 - 博客园
C# Task详解 - 五维思考 - 博客园
同步与异步:.NET 中的 Task.WaitAll 和 Task.WhenAll-CSDN博客
死锁(Deadlock)C#_c#死锁的原因及解决方法-CSDN博客