延申阅读:
1) 多线程之Thread
2) 多线程之线程池
3) 多线程之Task
一、什么是线程
线程是程序能够独立运行的最小单位,线程具有进程所具有的特征,所以线程又叫轻型进程。
1)一个进程可以有若干个线程,默认只有一个线程。
2)进程入口的第一个线程就是主线程,也就是默认的线程,主线程一般就是UI线程。
3)进程本身不执行代码, 执行代码的是线程。
4)线程可以认为是受系统自主运行,不被人为控制的一个执行过程。
5)线程一定会从头到尾执行完,所以它有许多状态。
6)线程创建的默认都是前台线程,前台线程全部执行完成,程序才会退出。
7)后台线程,当所有前台线程执行完成,立即退出。因此非关键操作都可以用后台线程。
8)线程的并发,除CPU多核是真实并发外,都是CPU的运算切换,只是切换时间非常短,让人感觉是并发。
二、创建线程
最基本的创建方式
public delegate void ThreadStart(); //ThreadStart 是一个委托
public Thread(ThreadStart start); //public Thread(委托);
//简化写法
由于ThreadStart是一个委托,那么我们就可以用Lambda表达式
Thread thread=new Thread(()=>method());
1、静态方法、实例方法
和平时调用的方法一样,实例需要new 类名().实例方法(),静态方法可以直接调用类名.静态方法();
2、无参方法
调用无参委托 ThreadStart
private static void MethodNoParam()
{
Console.WriteLine("无参方法");
}
var thread1 = new Thread(new ThreadStart(MethodNoParam));
//简写
var thread1 = new Thread(()=>{ Console.WriteLine("无参方法"); });
3、有参方法
调用有参委托 ParameterizedThreadStart
private static void MethodWithParam(object obj)
{
Console.WriteLine($"有参方法,参数是:{obj}");
}
var thread2 = new Thread(new ParameterizedThreadStart(MethodWithParam));
//简写
var thread2 = new Thread((param)=> { Console.WriteLine($"有参方法,参数是:{param}"); });
4、线程属性
ManagedThreadId 获取当前托管线程的唯一标识符。
Name 获取或设置线程的名称。
ThreadState 获取一个值,该值包含当前线程的状态。
Priority 获取或设置指示线程的调度优先级的值。
IsAlive 获取指示当前线程的执行状态的值。
IsBackground 获取或设置一个值,该值指示某个线程是否为后台线程。
IsThreadPoolThread 获取指示线程是否属于托管线程池的值。
ThreadState 线程状态
Unstarted //未启动
Running //正在运行 任务正在执行中
WaitSleepJoin //阻塞等待中
Aborted //已终止
Stopped //已停止 任务已完成
Priority 优化级。优化级越高拥有越多的迭代机会
Highest //最高
Normal //正常
Lowest //最低
三、前台线程和后台线程
主功能用前台线程,副功能用后台线程,设置后台线程thread.IsBackground = true;
1)前台线程,前台线程必须全部跑完,进程才完成并退出
2)后台线程,是指在程序运行的时候在后台提供一种通用服务的线程,并且这种线程并不属于程序中不可或缺的部分。
前台线程跑完后,后台线程同时也退出(即使未完成也退出)
private void Form1_Load(object sender, EventArgs e)
{
CreateThread();
}
private void CreateThread()
{
var thread = new Thread(x=> {
while(true) //while(true)保持任务永不退出,因此线程常驻
{
System.IO.File.AppendAllText(@"D:\Study\ForegroundThreadTest.txt", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") "\r\n");
Thread.Sleep(1000); //每次遍历都挂起1秒,即每1秒阻塞一次线程
}
});
//thread.IsBackground = true; //设置为后台线程,才会在前台线程退出后,同时退出
thread.Start();
}
可以看出,当程序退出后,线程没有退出,这是因为线程的任务并没有完成。
要让前台线程也同时退出,在编写Winform/Wpf程序时,可以加上这段代码以退出程序。
System.Environment.Exit(0);
四、线程方法
Sleep 线程挂起,阻塞当前线程,使当前线程处在延时状态
Thread.Sleep(100); //毫秒
Join 两个线程间同步。因为线程是并发的,所以会同时执行,而Join方法会阻塞后一个线程,直到当前线程完成。
t1.Start();
t1.Join();
t2.Start(); //线程t1完成后才执行
Abort 终止线程。抛出线程异常达到终止的效果,可能引发不可预知后果,不建议使用
五、线程同步
线程同步即资源同时仅允许一个线程进入,防止多个线程同时访问资源,造成数据混乱。
实现线程同步有3个方法。
1、lock
静态方法lock(静态变量),实例方法lock(this)
private static readonly object obj = new object();
private static void GetMoney(string threadName)
{
lock (obj)
{
if (TotalMoney - 100 < 0)
Console.WriteLine("余额不足了!");
else
{
Console.WriteLine($"{threadName}取钱{100}");
TotalMoney -= 100;
Console.WriteLine($"余额:{TotalMoney}");
}
}
}
经典的银行取钱操作,当不加锁时会造成数据错误
只有线程同步才能得到正确的结果
2、特性 [MethodImpl(MethodImplOptions.Synchronized)]
[MethodImpl(MethodImplOptions.Synchronized)]
private static void GetMoney(string threadName)
{
if (TotalMoney - 100 < 0)
Console.WriteLine("余额不足了!");
else
{
Console.WriteLine($"{threadName}取钱{100}");
TotalMoney -= 100;
Console.WriteLine($"余额:{TotalMoney}");
}
}
3、Monitor.TryEnter
区别于lock在于不会因为资源被占用,而一直阻塞。
详情参考:https://docs.microsoft.com/zh-cn/dotnet/api/system.threading.monitor.tryenter?redirectedfrom=MSDN&view=net-6.0
private static readonly object obj = new object();
private static void GetMoney(string threadName)
{
if (Monitor.TryEnter(obj, 500)) //如果资源被占用,那么等待500毫秒
{
try
{
if (TotalMoney - 100 < 0)
Console.WriteLine("余额不足了!");
else
{
Console.WriteLine($"{threadName}取钱{100}");
TotalMoney -= 100;
Console.WriteLine($"余额:{TotalMoney}");
}
}
finally
{
Monitor.Exit(obj);//释放对象的锁
}
}
}
4、互拆锁 Mutex
Mutex是全局的,比较消耗资源
const string MutexName = "CSharpThreadingCookbook";
using (var m = new Mutex(false, MutexName))
{
if (!m.WaitOne(TimeSpan.FromSeconds(5), false))
{
WriteLine("Second instance is running!");
}
else
{
WriteLine("Running!");
ReadLine();
m.ReleaseMutex();
}
}
六、死锁 deadlock
死锁是由于两个或以上的线程互相持有对方需要的资源,导致这些线程处于等待状态,无法执行。线程同步会造成死锁,要尽量避免!
互斥性:线程对资源的占有是排他性的,一个资源只能被一个线程占有,直到释放。
请求和保持条件:一个线程对请求被占有资源发生阻塞时,对已经获得的资源不释放。
不剥夺:一个线程在释放资源之前,其他的线程无法剥夺占用。
循环等待:发生死锁时,线程进入死循环,永久阻塞。说白了死锁就是两个或以上线程交替访问,形成死循环
static void LockTooMuch(object lock1, object lock2)
{
lock (lock1)
{
Console.WriteLine("LockTooMuch-lock1");
Console.WriteLine($"LockTooMuch-lock1的状态:{Thread.CurrentThread.ThreadState}");
lock (lock2)
{
Console.WriteLine("LockTooMuch-lock2");
Console.WriteLine($"LockTooMuch-lock2的状态:{Thread.CurrentThread.ThreadState}");
}
}
}
static void Main(string[] args)
{
object lock1 = new object();
object lock2 = new object();
var t1 = new Thread(() => LockTooMuch(lock1, lock2)); //t1 线程
t1.Start();
//t1.Join(); //阻塞主线程,先让t1线程完成,这样不会死锁,但没有达到并发效果
/*
主线程和子线程是并发的,因此lock2还没有被t1线程锁定,可以访问,打印t1状态、会死锁等信息
此时t1线程要访问lock2的资源,但lock2被锁定,t1处在阻塞等待状态
然后主线程要访问lock1的资源,但lock1被t1线程锁定,主线程片在阻塞等待状态
双方互相在等待对方释放资源,处在死锁状态
*/
lock(lock2) //主线程
{
Console.WriteLine($"主线程的状态:{Thread.CurrentThread.ThreadState}");
Console.WriteLine("会死锁");
lock(lock1)
{
Console.WriteLine("读取资源");
}
/*可以防止死锁,当超过3秒未能访问lock1的资源时,释放资源。t1线程得以继续下去
if(Monitor.TryEnter(lock1,3000))
{
Console.WriteLine("访问lock1成功");
}
else
{
Console.WriteLine("访问lock1超时,主线程退出");
}
*/
}
}
七、跨线程访问
.net禁止跨线程访问,因为这是不安全的。一般UI控件都是UI主线程中,但可以委托调用
textBox1.Invoke(new EventHandler((x,y)=> {
textBox1.Text = i.ToString();
}));
简写
Invoke((EventHandler) delegate { textBox1.Text = DateTime.Now.ToString(); });
或者
Action act = () =>
{
textBox1.Text = i.ToString();
};
textBox1.Invoke(act);
八、线程通讯
1)AutoResetEvent 自动信号
var autoReset=new AutoResetEvent(false); //false表示初始化无信息 true表示初始化有信息
WaitOne 表示设置接收信息,当没有信号时,阻塞线程
Set 表示发送信息,当前WaitOne标记的地方,释放阻塞,线程得以继续运行
此类的Reset不起作用,和ManualResetEvent最大的区别就是,发送信息号,立即复位到无信息状态
private static AutoResetEvent autoReset = new AutoResetEvent(false); //初始化无信息号状态,按键位1通知线程往下执行
static void Main(string[] args)
{
var t1 = new Thread(() =>
{
while(true)
{
autoReset.WaitOne();
Console.WriteLine("这是输出消息");
}
});
t1.Start();
while (true)
{
switch (Console.ReadLine())
{
case "1":
autoReset.Set(); //AutoResetEvent自动复位到无信号状态
Console.WriteLine("开始运行");
break;
case "2":
autoReset.Reset(); //ManualResetEvent有效
Console.WriteLine("暂停运行");
break;
default:
break;
}
}
}
2)ManualResetEvent 手动信号
WaitOne 表示设置接收信息,当没有信号时,阻塞线程
Set 表示发送信息,当前WaitOne标记的地方,释放阻塞,线程得以继续运行
Reset 表示重置信息,即无信息状态
和AutoResetEvent最大的区别是,不会自动复位,需要手动设置。
3)CountdownEvent 计数器
Signal //计一次线程运行
Wait //阻塞当前线程,只到计数完成