23C#线程基础知识2

线程安全

本地状态(Local)和共享状态(Shared)

Local本地独立:

CLR为每个线程分配自己的内存栈,以便使本地变量保持独立。

static void Main(string[] args)
        {
            new Thread(Go).Start();//在分线程上 调用Go()

            Go();//在main线程上 调用Go()

            Console.ReadKey();
        }

        public static  void Go()
        {
            //cycles是本地变量,在每个线程的内存栈上,都会创建cycles独立的副本。(线程之间不共享数据)
            for (int cycles = 0; cycles < 5; cycles++)
            {
                Console.WriteLine("?");
            }
        }

Shared共享:

如果多个线程都引用到同一个对象实例,那么它们就共享了数据。

被lambda表达式或匿名委托所捕获的本地变量,会被编译器转化为字段,所以也会被共享。

静态字段也会在线程间共享。

下面的_done字段就是共享变量

class Program
{
    private static bool _done;
    static void Main(string[] args)
    {
        new Thread(Go).Start();
        Go();
    }

    static void Go()
    {
        if (!_done)
        {
            _done = true;
            Console.WriteLine("Done");
        }
    }
}

字段共享就会引出线程安全问题,因为上述例子的输出极有可能不是固定的,如果在_done = true;前加上Thread.Sleep()结果就会不一样。这样两个线程都同时进入了里面的判断了,因为一个线程还没有来得及修改_done的值,而另一个线程也拿到_done的值进入了该判断了。主线程和分线程是同时运行的。

class Program
{
    private static bool _done;
    static void Main(string[] args)
    {
        new Thread(Go).Start();
        Go();
    }

    static void Go()
    {
        if (!_done)
        {
            Thread.Sleep(100);
            _done = true;
            Console.WriteLine("Done");
        }
    }
}

上例子就引出了线程安全这个关键概念(或者说缺乏线程安全)
上述例子的输出实际上是无法确定的:有可能(理论上)“Done”会被打印两次。如果交换Go方法里语句的顺序,那么“Done”被打印两次的几率会大大增加。因为一个线程可能正在评估 if,而另外一个线程在执行Thread.Sleep(100);语句,它还没来得及把done设置为true。所以,尽可能避免使用共享状态。


锁定与线程安全简介

Locking & Thread Safety

在读取和写入共享数据的时候,通过使用一个互斥锁( exclusive lock ),就可以修复前面例子的问题。

C#里使用lock语句来加锁

当两个线程同时竞争一个锁的时候(锁可以基于任何引用类型对象),一个线程会等待或阻塞,直到锁变成可用状态。

如:

class Program
    {
        private static bool _done;
        static readonly object _locker = new object();
        static void Main(string[] args)
        {
            Thread t = new Thread(Go);
            t.Start();//分线程调用
            Go();//main线程调用
            Console.ReadKey();
        }

        static void Go()
        {
            lock (_locker)
            {
                if (!_done)
                {
                    Thread.Sleep(100);
                    _done = true;
                    Console.WriteLine("Done");
                }
            }
        }
    }

在这里使用lock将共享代码块包括,这个lock代码块就称为临界区,每次只允许一个线程进入,在多线程上下文中,以这种方式避免不确定性的代码就叫做线程安全。

互斥锁Lock不是线程安全的最好方法,很容易忘记对字段加锁,Lock也会引起一些问题(死锁)。

线程传参

①以直接使用lambda表达式做为Thread的参数

②可以使用Thread的Start方法来传递任务的参数,因为传参委托的类型是ThreadStart,所以参数必须是object类型。

class Program
{
    static void Main(string[] args)
    {
        new Thread(G).Start("Hello World");
    }

    static void G(object str)
    {
        Console.WriteLine((string)str);
    }
}

③使用lambda表达式可以很简单地给Thread传递参数。但是线程开始后,可能会不小心修改了被捕获地变量,比如下面的例子,每一次运行都有不同的输出,而且输出有可能会有相同的值。也就是lambda默认使用引用捕获,很有可能是线程运行时(非构造时,new Thread是构造但不一定立即运行)for循环已经将i递增到10了。i被lambda表达式捕获并被编译成字段,被所有线程共享。所以会有重复值。

for (int i = 0; i < 10; i++)
{
    new Thread(() => Console.Write(i)).Start();
}

可以使用局部变量来解决这个问题,这个变量每次循环都会创建一个新的临时变量。也就是会创建10个内存地址。

for (int i = 0; i < 10; i++)
{
    int temp = i;
    new Thread(() => Console.Write(temp)).Start();
}

线程的执行是无序的,因为由操作系统的调度决定cpu运行哪条线程,所以打印也是无序的,但是绝对没有重复的值。

异常处理

创建线程时在作用范围内的 trylcatch/finally 块,在线程开始执行后就与线程无关了。

创建线程代码处的trycatch和线程里面的方法的异常捕获没关系。

static void Main(string[] args)
        {
            try
            {
                new Thread(Go).Start(); ;
            }
            catch (Exception ex)
            {
                Console.WriteLine("Exception!");
            }
            Console.ReadKey();
        }

        static void Go()
        {
            throw null;
        }

解决方法,在线程里面捕获

static void Main(string[] args)
        {
            new Thread(Go).Start(); 

            Console.ReadKey();
        }

        static void Go()
        {
            try
            {
                throw null;
            }
            catch (Exception ex)
            {
                Console.WriteLine("Exception!");
            }
        }

在WPF、WinForm里,可以订阅全局异常处理事件:

  • Application.DispatcherUnhandledException
  • Application.ThreadException

在通过消息循环调用的程序的任何部分发生未处理的异常(这相当于应用程序处于活动状态时在主线程上运行的所有代码)后,将触发这些异常。但是非U线程上的未处理异常,并不会触发它。

而任何线程有任何未处理的异常都会触发

AppDomain.CurrentDomain.UnhandledException

线程的优先级

线程的优先级(Priority属性)决定了相对于操作系统中其他活跃线程所占的执行时间。

如果想让某线程的优先级比其他进程中的线程高,那就必须提升进程的优先级,这里可以使用Process类

using (Process p = Process.GetCurrentProcess())
{
    p.PriorityClass = ProcessPriorityClass.High;
}

但是,提高线程或进程的优先级可能会导致其他线程或线程处于饥饿状态,不能随便设置。

信号

有时,你需要让某个线程一直处于等待状态,直到接收到其他线程发来的通知,才解除等待状态,这就叫做signaling。最简单的信号结构是MaunalResetEvent类对象。

调用MaunalResetEvent类对象的WaitOne方法就会阻塞当前的线程,直到另一个线程通过调用Set方法来打开信号(发送信号)。

internal class Program
{
    private static void Main(string[] args)
    {
        var signal = new ManualResetEvent(false);

        var thread = new Thread(() =>
                                {
                                    Console.WriteLine("Waiting for signal...");
                                    //等待主线程发送的信号
                                    signal.WaitOne();
                                    signal.Dispose();
                                    Console.WriteLine("Got signal!");
                                });
        thread.Start();
        Thread.Sleep(3000);
        //向thread打开信号
        signal.Set();
    }
}

信号打开后可以通过Reset方法来重新关闭信号。

posted @ 2022-04-17 18:11  青仙  阅读(95)  评论(0编辑  收藏  举报