【C#】线程同步--《C#本质论》

目录

 一、使用Monitor来同步

二、使用lock来同步

三、避免锁定 this、typeof(type)和 string

四、Interlocked 类

五、避免死锁

六、更多同步类型

(1)Mutex

(2) WaitHandle

(3)重置事件类:ManualResetEvent和ManualResetEventSlim

(4)Semaphore / SemaphoreSlim 和 CountdownEvent

(5)并发集合类

七、线程本地存储

(1)ThreadLocal

 (2)ThreadStaticAttribute

(3) GetData() 和 SetData()

(4)AsyncLocal


线程同步的作用是避免死锁的同时防止出现竞态条件。若能同步多个线程对代码或数据的并发访问,就说这些代码和数据是线程安全的。

关于变量读写的原子性,有一个重点需要注意:假如类型的大小不超过一个本机(指针大小的)整数,“运行时”就保证该类型不会被部分性地读取或写入。所以,64位操作系统保证能够原子性地读写一个long (64位)。然而,128位变量(比如decimal)的读写就不保证是原子性的。所以,通过写操作来更改一个decimal变量时,可能会在仅仅复制了32位之后被打断,造成以后读取一个不正确的值,这称为一次torn read (被撕裂的读取)。

局部变量没有必要同步。局部变量加载到栈上,而每个线程都有自己的逻辑栈。所以,针对每个方法调用,每个局部变量都有自己的实例。在不同的方法调用之间,局部变量默认是不共享的。因此,它们在多个线程之间也是不共享的。然而,这并不是说局部变量完全没有并发性问题,因为代码可能轻易向多个线程公开局部变量”。例如,在迭代之间共享局部变量的并行for循环就会公开变量,使其可能被并发访问,从而造成一个竞态条件。s

    public static void Main()
    {
        int x = 0;
        Parallel.For(0, 10000, i => { x++; x--; });
        Console.WriteLine($"Count={x}");
    }

使用await/async模式重构代码

    public static async void CountAsync()
    {
        int count = 0;
        Task task = Task.Run(() =>
        {
            for(int i=0;i<1000; i++)
            {
                count++;
                count--;
            }
        });
        await task;
        Console.WriteLine($"Count2={count}");
    }

Count1=17
Count2=0

Count1=108
Count2=0 

 一、使用Monitor来同步

为了同步多个线程,防止它们同时执行特定的代码段,需要用监视器(monitor)来阻止第二个线程进入受保护的代码段,直到第一个线程退出那个代码段。监视器功能由System.Threading.Monitor类提供。为了标识受保护代码段的开始和结束位置,需要分别调用静态方法Monitor.Enter()和Monitor.Exit()。

    readonly static object sync=new object();
    const int total=1000;
    static long count=0;
    public static void Main()
    {
        Task task = Task.Run(() => Decrement());

        for (int i = 0; i < total; i++)
        {
            bool lockTaken = false;
            try
            {
                Monitor.Enter(sync,ref lockTaken);
                count++;
            }
            finally
            {
                if(lockTaken)
                {
                    Monitor.Exit(sync);
                }
            }
        }

        task.Wait();
        Console.WriteLine($"Count={count}");
    }

    private static void Decrement()
    {
        for(int i = 0; i < total; i++)
        {
            bool lockToken=false;
            try
            {
                Monitor.Enter(sync,ref lockToken);
                count--;
            }
            finally
            {
                if(lockToken)
                {
                    Monitor.Exit(sync);
                }
            }
        }
    }

Monitor还支持Pulse()方法,允许线程进入“就绪队列” (ready queue),指出下一个就轮到它获得锁。这是同步生产者一消费者模式的一种常见方式,目的是保证除非有“生产”,否则就没有“消费”。拥有监视器的生产者线程调用 Monitor.Pulse() 通知消费者线程一个项(item)已生产好,请“准备” (get ready)消费。

一个Pulse()调用只允许一个线程(消费者)进入就绪队列。生产者线程调用Monitor.Exit() 时,消费者线程将取得锁并进入关键执行区域以开始“消费”。消费者处理好等待处理的项以后,就调用Exit(),从而允许生产者(当前正由Monitor.Enter()阻塞)再次生产其他项。在本例中,一次只有一个线程进入就绪队列,确保没有“生产”就没有“消费”,反之亦然。

二、使用lock来同步

    readonly static object sync=new object();
    const int total=1000;
    static long count=0;
    public static void Main()
    {
        Task task = Task.Run(() => Decrement());

        for (int i = 0; i < total; i++)
        {
            lock (sync)
            {
                count++;
            }
        }
        task.Wait();
        Console.WriteLine($"Count={count}");
    }

    private static
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值