22C#线程基础知识1

1.Thread

什么是线程Thread

线程是一个可执行路径,它可以独立于其它线程执行。

每个线程都在操作系统的进程(Process) 内执行,而操作系统的进程提供了程序运行的独立环境。

单线程应用,在进程的独立环境里只跑一个线程,所以该线程拥有独占权。

多线程应用,单个进程中会跑多个线程,它们会共享当前的执行环境(尤其是内存)。

例如,一个线程在后台读取数据,另一个线程在数据到达后进行展示。
这个数据就被称作是共享的状态。


例子:

static void Main(string[] args)
        {

            Thread thread = new Thread(WriteY);//开辟了一个新的线程 Thread
            thread.Start();//运行WriteY()

            //同时主线程也做一些工作
            for (int i = 0; i <= 1000; i++)
                Console.Write("X");

            Console.ReadKey();
        }


        public static  void WriteY()
        {
            for (int  i=0;i<=1000;i++)
                Console.Write("Y");            
        }

执行结果:
image

为啥结果回xy有重复的交替输出?

在单核计算机上,操作系统必须为每个线程分配“时间片”(在Windows中通常为20毫秒)来模拟并发,从而导致重复的x和y块。

在多核或多处理器计算机上,这两个线程可以真正地并行执行(可能受到计算机上其他活动进程的竞争)。
在本例中,由于控制台处理并发请求的机制的微妙性,您仍然会得到重复的x和y块。

图示流程:
image


术语:线程被抢占

一个线程的执行与另外一个线程上代码的执行交织的那一点。线程在这个时候就可以称为被抢占了。实际上就是操作系统夺走了线程占用着的cpu。

线程的一些属性

线程一旦开始执行,IsAlive就是true,线程结束就变成false。

线程结束的条件就是:线程构造函数传入的委托结束了执行。

线程一旦结束,就无法再重启。

每个线程都有个Name属性,通常用于调试。线程Name只能设置一次,以后更改会抛出异常。

静态的Thread.CurrentThread属性,会返回当先执行的线程。

例子:CurrentThread

static void Main(string[] args)
        {
            //设置当前线程的名字
            Thread.CurrentThread.Name = "Main Thread....";

            Thread t = new Thread(WriteY);//开辟了一个新的线程 Thread
            t.Name = "Y Thread...";
            t.Start();//运行WriteY()


            Console.WriteLine(Thread.CurrentThread.Name);

            //同时主线程也做一些工作
            for (int i = 0; i <= 1000; i++)
                Console.Write("X");

            Console.ReadKey();
        }


        public static  void WriteY()
        {
            Console.WriteLine(Thread.CurrentThread.Name);

            for (int  i=0;i<=1000;i++)
                Console.Write("Y");            
        }

输出结果:

image


Join方法和Sleep方法

Join()

调用Join()方法,可以等待另一个线程结束。类似awit进程同步。

比如,主线程调用 Join方法,主线程就会等待子线程执行结束后才继续执行。

例子:

static void Main(string[] args)
        {
            Thread t = new Thread(Go);//开辟了一个新的线程 Thread
            t.Start();//运行

            t.Join();

            Console.WriteLine("  Thread  t  has ended!");

            Console.ReadKey();
        }


        public static  void Go()
        {
            for (int  i=0;i<=1000;i++)
                Console.Write("Y");            
        }

image

Sleep()

进行线程等待。

class Program
    {
        static Thread thread1, thread2;

        static void Main(string[] args)
        {
            thread1 = new Thread(Go);
            thread1.Name = "Thread1";
            thread1.Start();

            thread2 = new Thread(Go);
            thread2.Name = "Thread2";
            thread2.Start();

            Console.ReadKey();
        }

        public static  void Go()
        {
            Console.WriteLine($"\n Current thread:{Thread.CurrentThread.Name}");
            if (Thread.CurrentThread.Name == "Thread1" && thread2.ThreadState != ThreadState.Unstarted)
                thread2.Join();

            Thread.Sleep(2000);
            Console.WriteLine($"\n Current thread:{Thread.CurrentThread.Name}");
            Console.WriteLine($"Thread1:{thread1.ThreadState}");
            Console.WriteLine($"Thread2:{thread2.ThreadState}");
        }
    }

结果

image


添加超时

调用Join的时候,可以设置一个超时,用毫秒或者TimeSpan都可以。如果返回true,那就是线程结束了;如果超时了,就返回false。

Thread.Sleep()方法会暂停当前的线程,并等一段时间。
注意:
Thread.Sleep(O)这样调用会导致线程立即放弃本身当前的时间片,自动将CPU移交给其他线程。

Thread.Yield()做同样的事,但是它只会把执行交给同一处理器上的其它线程。

当等待Sleep或Join的时候,线程处于阻塞的状态。

Sleep(O)Yield有时在高级性能调试的生产代码中很有用。它也是一个很好的诊断工具,有助于发现线程安全问题。如果在代码中的任何地方插入Sleep(O)Yield就破坏了程序,那么你工的程序几乎肯定有bug。


阻塞(Blocking)

如果线程的执行由于某种原因导致暂定,那么就认为该线程被阻塞了。例如在Sleep或者通过Join等待其他线程结束。

被阻塞的线程会立即将其处理器的时间片生成给其它线程,从此就不再消耗处理器时间,直到满足其解除阻塞条件为止。

可以通过Threadstate这个属性来判断线程是否处于被阻塞的状态:
bool blocked = (someThread.Threadstate & Threadstate.WaitSleepDoin) != 0;

Threadstate线程的状态

Threadstate的枚举值:
image

image

但是它大部分的枚举值都没什么用,下面的代码将Threadstate剥离为四个最有用的值之一:Unstarted、Running. WaitSleepJoin 和 Stopped
image

ThreadState属性可用于诊断的目的,但不适用于同步,因为线程状态可能会在测试ThreadState和对
该信息进行操作之间发生变化。


解除阻塞(UnBlocking)

当遇到下列四种情况的时候,就会解除阻塞:

阻塞条件被满足

操作超时(如果设置超时的话)

通过Thread.Interrupt()进行打断

通过Thread.Abort()进行中止


上下文切换

当线程阻塞或解除阻塞时,操作系统将执行上下文切换。
这会产生少量开销,通常为1或2微秒。


I/O-bound vs Compute-bound(或CPU-Bound)

一个花费大部分时间等待某事发生的操作称为IO-bound

IO绑定操作通常涉及输入或输出,但这不是硬性要求:Thread.Sleep()也被视为IO-bound

相反,一个花费大部分时间执行CPU密集型工作的操作称为Compute-bound。

阻塞vs忙等待(自旋)

Blocking vs Spinning

lO-bound操作的工作方式有两种:

①在当前线程上同步的等待: Console.ReadLine(),Thread.Sleep(),Thread.Join()...
②异步的操作,在稍后操作完成时触发一个回调动作。

同步等待的I/O-bound操作将大部分时间花在阻塞线程上(干等待)。

它们也可以周期性的在一个循环里进行 "打转(自旋)"

如下图两个例子:
image

在忙等待和阻塞方面有一些细微差别:

首先,如果您希望条件很快得到满足(可能在几微秒之内),则短暂自旋可能会很有效,因为它避免了上下文切换的开销和延迟。.NET Framework提供了特殊的方法和类来提供帮助SpinLock和 SpinWait。

其次,阻塞也不是零成本。这是因为每个线程在生存期间会占用大约1 MB的内存,并会给CLR和操作系统带来持续的管理开销。

因此,在需要处理成百上千个并发操作的大量I/O-bound程序的上下文中,阻塞可能会很麻烦。
所以,此类程序需要使用基于回调的方法,在等待时完全撤消其线程。

posted @ 2022-04-17 16:41  青仙  阅读(116)  评论(0编辑  收藏  举报