C#多线程学习总结

C#多线程学习总结_第1张图片
线程的基础概念
操作系统能够优先访问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的顺序获得锁。如下图所示

你可能感兴趣的:(C#,1024程序员节)