线程的基础概念
操作系统能够优先访问CPU,并能够调整不同程序访问CPU的优先级,避免某一个程序独占CPU的情况发生。
可以认为线程是一个虚拟进程,用于独立运行一个特定的程序。
线程会消耗大量的操作系统资源,多个线程共享一个物理处理器将导致操作系统忙于管理这些线程,而无法运行程序
在单核cpu上并行执行计算任务是没有意义的。
在多核cpu上可以使用多线程有效的利用多个cpu的计算能力。这需要组织多个线程间的通信和相互同步。
线程的基本操作
线程的生命周期包括:创建线程 、挂起线程、线程等待、终止线程
创建线程
通过new 一个Thread对象并指定线程执行函数创建线程。调用Start方法开启线程
///
/// 线程启动函数
///
static void PrintNums()
{
Console.WriteLine(“starting …”);
for (int i = 0; i < 10; i++)
{
Console.WriteLine(i);
}
Console.WriteLine(“end…”);
}
/// 创建一个线程
public static void Main()
{
Thread thread = new Thread(PrintNums);
thread.Start();
PrintNums();//主线程调用
}
暂停当前线程
通过在线程函数中调用Thread.Sleep()暂停当前线程,使线程进入休眠状态。此时线程会占用尽可能少的CPU时间。
//暂停线程
static void PrintNumsWithDelay()
{
Console.WriteLine(“starting …”);
//Log(“当前线程状态 :” + Thread.CurrentThread.ThreadState.ToString());
for (int i = 0; i < 10; i++)
{
Thread.Sleep(TimeSpan.FromSeconds(1)); //每次暂停一秒
Console.WriteLine(i);
}
Console.WriteLine(“end…”);
}
//创建一个线程并暂停
public static void Test1()
{
Thread t = new Thread(PrintNumsWithDelay);
t.Start();
PrintNums();//主线程调用
}
线程等待
假设有线程t,在主程序中调用了t.Join()方法,该方法允许我们等待直到线程t完成。当线程t完成 时,主程序会继续运行。借助该技术可以实现在两个线程间同步执行步骤。第一个线程会等待另一个线程完成后再继续执行。第一个线程等待时是处于阻塞状态(正如暂停线程中调用 Thread.Sleep方法一样)
///
/// 在主线程中等待线程执行玩
///
public static void Main()
{
Thread t1 = new Thread(PrintNumsWithDelay);
t1.Start();
t1.Join(); //等待线程t1执行完成,程序会在这里阻塞
Console.WriteLine(“Thread t1 finished”);
}
终止线程
通过调用Thread.Abort()方法强制终止线程。这会给线程注入ThreadAbortExeception方法,导致线程被终结。这是一个非常危险的操作, 任何时刻发生并可能彻底摧毁应用程序。另外,使用该技术也不一定总能终止线程。目标线程可以通过处理该异常并调用Thread.ResetAbort方法来拒绝被终止。因此并不推荐使用,Abort方法来关闭线程 。
///
/// 终止线程 非常危险,不推荐使用,也不一定能够终止线程
///
public static void Main()
{
Thread t = new Thread(PrintNumsWithDelay);
t.Start();
//5s之后终止线程t
Thread.Sleep(5000);
t.Abort();
Console.WriteLine(“Thread t has been Abort”);
}
获取线程状态
线程状态位于Thread对象的ThreadState属性中。ThreadState属性是一个C#枚举对象。刚开始线程状态为ThreadState.Unstarted,然后我们启动线程,线程状态会从ThreadState.Running变为ThreadState. WaitSleepJoin。 其中:请注意始终可以通过Thread.CurrentThread静态属性获得当前Thread对象。
///
/// 线程状态
///
public static void Test5()
{
Thread t1 = new Thread(PrintNumsWithDelay);
Log(“t1线程状态 :” + t1.ThreadState.ToString());
t1.Start();
Log(“t1线程状态 :” + t1.ThreadState.ToString());
t1.Join(); //等待线程t1执行完成,程序会在这里阻塞
Log(“t1线程状态 :” + t1.ThreadState.ToString());
Console.WriteLine(“Thread t1 finished”);
Log(“t1线程状态 :” + t1.ThreadState.ToString());
}
线程优先级
通过设置Thread.Priority属性给线程对象设置优先级 ThreadPriority.Highest (最高优先级)、 ThreadPriority.Lowest(最低优先级)。通常优先级更高的线程将获取到更多cpu时间。
///
/// 线程优先级
///
class ThreadSample
{
private bool isStop = false;
public void Stop()
{
isStop = true;
}
public void CountNums()
{
long counter = 0;
while (!isStop)
{
counter++;
}
Console.WriteLine("{0} with {1,11} priority has a count = {2,13}"
,Thread.CurrentThread.Name,Thread.CurrentThread.Priority,
counter.ToString());
}
}
static void RunThreads()
{
//启动两个线程t1 t2
var sample = new ThreadSample();
Thread t1 = new Thread(sample.CountNums);
t1.Name = "Thread1";
Thread t2 = new Thread(sample.CountNums);
t2.Name = "Thread2";
//设置线程的优先级
t1.Priority = ThreadPriority.Highest; //t1为最高优先级
t2.Priority = ThreadPriority.Lowest; //t2为最低优先级
//启动
t1.Start();
t2.Start();
//主线程阻塞2s
Thread.Sleep(TimeSpan.FromSeconds(2));
sample.Stop(); //停止计数
//等待按键按下
Console.ReadKey();
}
///
/// 线程优先级,决定该线程可以占用多少cpu时间
///
public static void Test6()
{
Log("当前线程的优先级 priority = " + Thread.CurrentThread.Priority.ToString());
Log("在所有核上运行");
RunThreads();
Thread.Sleep(TimeSpan.FromSeconds(2));
Log("在单个核上运行");
//在该进程下的线程只能在一个核上运行
Process.GetCurrentProcess().ProcessorAffinity = new IntPtr(1);
//再次执行
RunThreads();
/*
结果:多核时高优先级的线程通常会比低优先级的线程多执行次数 但是大体接近
单核的时候竞争就更激烈了,用有高优先级的线程会占用更多的cpu时间,而留给低优先级的线程的
cpu时间就更少了。
* 在所有核上运行
Thread1 with Highest priority has a count = 583771892
Thread2 with Lowest priority has a count = 444097830
在单个核上运行
Thread2 with Lowest priority has a count = 32457242
Thread1 with Highest priority has a count = 6534967709
*
*/
}
前台线程和后台线程
当主程序启动时定义了两个不同的线程。默认情况下,显式创建的线程是前台线程。通过手动的设置Thread对象的IsBackground属性为ture来创建一个后台线程。
前台线程与后台线程的主要区别: 进程会等待所有的前台线程完成后再结束工作,但是如果只剩下后台线程,则会直接结束工作。
如果程序定义了一个不会完成的前台线程,主程序并不会正常结束。
///
/// 前台线程和后台线程
///
class ThreadSample2
{
private int iterCount = 0;
public ThreadSample2(int count)
{
this.iterCount = count;
}
public void CountNum()
{
for (int i = 0; i <= iterCount; i++)
{
Thread.Sleep(500); //挂起0.5s
//输出次数
Console.WriteLine("{0} prints {1}",Thread.CurrentThread.Name,i);
}
Log("Thread Finished : " + Thread.CurrentThread.Name);
}
}
public static void Test7()
{
ThreadSample2 samp1 = new ThreadSample2(10);
ThreadSample2 samp2 = new ThreadSample2(20);
//启动两个线程t1,t2,并讲其中一个设置为后台线程 默认是前台线程
Thread t1 = new Thread(samp1.CountNum);
t1.Name = "Foreground";
Thread t2 = new Thread(samp2.CountNum);
t2.Name = "Background";
t2.IsBackground = true; //设置为后台线程
//启动
t1.Start();
t2.Start();
//进程会等所有的前台程序结束完之后才结束;如果只剩下后台程序,则进程会直接结束
}
向线程中传递参数
启动线程的时候需要向线程函数中传递参数,一般有三种方式。
将线程函数声明为一个类的成员函数,通过类的成员变量来传递参数。
声明一个静态函数当作线程的执行函数,该函数接受一个object类型的参数param,这个参数可以通过Thread.Start(param)传递到线程中。
通过lambda表达式的闭包机制传递参数,原理等同于1。C#编辑器会帮我们实现这个类。
///
/// 向线程中传递参数
///
public static void CountNum(int iterCount)
{
for (int i = 0; i <= iterCount; i++)
{
Thread.Sleep(500); //挂起0.5s
//输出次数
Console.WriteLine("{0} prints {1}",Thread.CurrentThread.Name,i);
}
Log("Thread Finished : " + Thread.CurrentThread.Name);
}
//方法3:通过object类型的参数传递,参数parma在Start()函数中传递
public static void Count(object param)
{
CountNum((int) param);
}
public static void Test8()
{
Thread t1 = new Thread(Count);
t1.Start(6); //传递参数
//方法3 通过lamda表达式
int num = 8;
Thread t2 = new Thread(()=>{ CountNum(num);});
num = 12;
Thread t3 = new Thread(() => { CountNum(num);});
t2.Start();
t3.Start();
}
线程锁的使用
当多个线程同时访问一个资源的时候,容易形成竞争条件,导致错误的产生。为了确保不会发生这种情况,需要保证当前线程1在访问变量A的时候,其他线程必须等待直到线程1完成当前操作。
如果锁定了一个对象,需要访问该对象的所有其他线程则会处于阻塞状态,并等待直到该对象解除锁定。这,可能会导致严重的性能问题。
可以使用lock关键字来进行加锁操作
public abstract class BaseCounter
{
public abstract void Add();
public abstract void Del();
public abstract long GetRes();
}
///
/// 线程不安全的计数器
///
public class Counter : BaseCounter
{
private long counter;
public override void Add()
{
counter++;
}
public override void Del()
{
counter--;
}
public override long GetRes()
{
return counter;
}
}
///
/// 线程安全的计数器
///
public class ThreadSafeCounter : BaseCounter
{
///
/// 线程锁对象
///
private readonly object lockObj = new object();
private long counter;
public override void Add()
{
lock (lockObj) //锁定一个对象,需要访问该对象的所有其他线程就会处于阻塞状态,并等待直到该对象接触锁定
{
counter++;
}
}
public override void Del()
{
lock (lockObj)
{
counter--;
}
}
public override long GetRes()
{
return counter;
}
}
///
/// 线程函数 测试计数器
///
///
static void TestCounter(BaseCounter c)
{
for (int i = 0; i < 10000000; i++)
{
c.Add();
c.Del();
}
}
public static void TestLock()
{
//测试不安全的计数器
//创建3个线程同时对计数器进行加减
Counter c = new Counter();
Thread t1 = new Thread(() => { TestCounter(c);});
Thread t2 = new Thread(() => { TestCounter(c);});
Thread t3 = new Thread(() => { TestCounter(c);});
t1.Start();
t2.Start();
t3.Start();
t1.Join();
t2.Join();
t3.Join();
//等三个线程都执行完之后打印结果 看是否为0
Console.WriteLine("count = " + c.GetRes());
//测试线程安全的计数器
//创建3个线程同时对计数器进行加减
ThreadSafeCounter c1 = new ThreadSafeCounter();
t1 = new Thread(() => { TestCounter(c1);});
t2 = new Thread(() => { TestCounter(c1);});
t3 = new Thread(() => { TestCounter(c1);});
t1.Start();
t2.Start();
t3.Start();
t1.Join();
t2.Join();
t3.Join();
//等三个线程都执行完之后打印结果 看是否为0
Console.WriteLine("count = " + c1.GetRes());
}
死锁的产生
什么是死锁:
所谓死锁,是指多个进程在运行过程中因争夺资源而造成的一种僵局,当进程处于这种僵持状态时,若无外力作用,它们都将无法再向前推进。 因此我们举个例子来描述,如果此时有一个线程A,按照先锁a再获得锁b的的顺序获得锁,而在此同时又有另外一个线程B,按照先锁b再锁a的顺序获得锁。如下图所示