C#多线程(三)

一、线程同步概述

在多线程程序中,当存在共享变量和抢占资源的情况时,需要使用线程同步机制来防止发生这些冲突,这样才能保证得到可预见的结果,也就是线程安全的。否则就会出现不可预知的结果产生线程不安全问题。特别是在访问同一个数据的时候最为明显。主要通过以下四个方式进行:

  • 简单阻塞:让一个线程等待另一个线程执行结束或者等待一段时间而阻塞执行,使用Sleep、Join、Task.Wait这几个方式

构成

目的

Sleep

阻止给定的时间周期

Join

等待另一个线程完成

  • 锁:排他锁是最常见的锁机制,对于共享数据在每个线程内访问前判断锁的情况,保证每次只能有一个线程访问,使得相互不会干扰结果。排他锁使用lock关键字、Mutex类和SpinLock,对于共享锁使用Semaphore、SemaphoreSlim和读写锁。

构成

目的

是否跨进程

速度

lock

确保只有一个线程访问某个资源或某段代码。

Mutex

确保只有一个线程访问某个资源或某段代码。可被用于防止一个程序的多个实例同时运行。

中等

Semaphore

确保不超过指定数目的线程访问某个资源或某段代码。

中等

  • 信号量:这种机制使得线程一直阻塞知道接收到其他线程的通知才开始执行,避免了无效的轮询需求。最常见的方法是使用event wait handles和Monitors类的Wait/Pulse方法,.NET4.0 使用的是CountdownEvent和Barrier类。

构成

目的

跨进程?

速度

EventWaitHandle

允许线程等待直到它受到了另一个线程发出信号。

中等

Wait 和 Pulse*

允许一个线程等待直到自定义阻止条件得到满足。

中等

  • 非阻塞的同步机制:这个机制使用处理器的原语操作来实现。C#提供了非阻塞的原语:Thread.MemoryBarrier、Thread.VolatileRead、Thread.VolatileWrite、volatile关键字和Interlocked类。

构成

目的

跨进程?

速度

Interlocked*

完成简单的非阻止原子操作。

是(内存共享情况下)

非常快

volatile*

允许安全的非阻止在锁之外使用个别字段。

非常快

 
阻塞的线程占用处理器阻塞当时的时间片后,一直就不会占用处理器,知道发生阻塞的条件满足后才会运行。可以使用下面的方式来测试一个线程是否被阻塞:
     bool  blocked = (Thread.ThreadState & ThreadState.WaitSleepJoin) != 0;
解除阻塞发生的情况:(其中对于使用Suspend方法挂起的线程并不视为阻塞状态)
  • 阻塞条件满足
  • 执行时间完毕
  • 使用Thread.Interrupt打断
  • 使用Thread.Abort丢弃
阻止和轮询:使用轮询非常消耗CPU资源,但是可以使用轮询休眠组合使用的方式。
二、锁和线程安全
1、lock
线程安全就是为了任何时刻只有一个线程进入临界区代码。下面是使用lock关键字来实现线程安全,这种互斥锁如果有大于一个线程竞争这个锁,他们会形成一个就绪队列,以先到先得的方式获得锁,他们被阻止在ThreadState的WaitSleepJoin状态,可以使用Interrupt和Abort来强制线程释放锁。
       static object locker = new object();
        static int val1, val2;
        static void Main(string[] args)
        {
            val1 = 100;
            val2 = 10;
            Thread t1 = new Thread(Go);
            Thread t2 = new Thread(Go);

            t1.Start();
            t2.Start();

            Console.ReadKey();
        }
        static void Go()
        {
            lock (locker)
            {
                if (val2 != 0) Console.WriteLine(val1 / val2);
                val2 = 0;
            }
        }

C#多线程(三)
这里得到的是线程安全的结果,只有一个线程执行输出结果,否则可能会出现val2为0 而被作为除数的错误。lock语句实际上是调用了Monitor.Enter和Monitor.Exit方法,同时与try-finally语句配合。上述lock语句的等价方式如下:
Monitor.Enter(locker);
try
{
    if (val2 != 0)  Console.WriteLine(val1/val2);
    val2 = 0;
}
finally
{
    Monitor.Exit(locker);
}
上述使用的locker同步对象,必须是引用类型,最好是在私有类里面定义,防止外部锁定相同对象。可使用lock(this){...}来精确控制锁的范围和粒度,同时,锁没有阻止对同步对象本身的访问
2、嵌套
可以重复锁定相同的对象,多次调用Monitor.Enter和Monitor.Exit或者是lock来实现,但是线程只能在最开始或者最外面的锁时被阻止。
3、使用情形和性能
任何与多线程有关的会进行读和写的字段都应当加锁。
锁本身是非常快的,一般只需几十纳秒,发生阻塞的情况下也只会接近数微妙范围。对于太多同步对象死锁是非常容易出现的,因此好的原则是开始使用较少的锁。
4、Mutex
Mutex非常类似lock关键字,但是可以在多个进程间工作。使用Mutex耗时几个微秒级时间,大约是使用lock关键字耗时的50倍。使用Mutex的WaitOne来阻塞,使用ReleaseMutex方法来接触阻止。
        static void Main(string[] args)
        {
            using (var mutex = new Mutex(false, "mutex"))
            {
                if (!mutex.WaitOne(TimeSpan.FromSeconds(3), false))
                {
                    Console.WriteLine("Another app instance is running. Bye!");
                    return;
                }
                RunProgram();
            }
        }

        static void RunProgram()
        {
            Console.WriteLine("Running. Press Enter to exit");
            Console.ReadLine();
        }
5、Semaphore
类似一个房间,有最大可以容纳的人数,当人数达到最大可容纳数之后就不能进入只能排队,这时每次有一个人出房间之后,排队的一个才能进入房间。构造函数使用至少两个必须参数:一个是当前可容纳的数目,另一个是可容纳的最大总数。
当可容纳的最大总数为1的时候就和lock和Mutex是类似的,只是没有“主人”,任何在Semaphorenn内的线程都可以调用Relase方法退出,但是Mutex和lock只有获得了锁的线程才能释放它。
C#中有两个版本的类:Semaphore和SemaphoreSlim类,后者是.NET 4.0中的类,是经过优化了可以满足很低延迟的并行编程的需求。当然也可在传统多线程编程中处理归功于在等待状态时有一个cancellation token。
Semaphore在限制并发方面非常有用,可以阻止过多的线程在一段代码中一次执行,Semaphore的容量就是限制过多线程的上限数。
        static SemaphoreSlim sem = new SemaphoreSlim(3);
        static void Main(string[] args)
        {
            for (int i = 1; i < 10; i++)
                new Thread(Enter).Start(i);
            Console.ReadKey();
        }

        static void Enter(object id)
        {
            Console.WriteLine(id + "wants to enter");
            sem.Wait();
            Console.WriteLine(id + "enter in!");
            Thread.Sleep(1000 * (int)id);
            Console.WriteLine(id + "is leaving");
            sem.Release();
        }

C#多线程(三)
上述代码中Thread.Sleep如果替换为某些紧急的磁盘I/O读写操作,通过这样限制线程最大数目,限制并发的执行磁盘操作数目,可以大大提高程序的整体性能。

6、线程安全
线程安全的代码是指在面对任何多线程的情况下,都没有不可预知的因素或结果,首先是完成锁,其次是减少线程间交互的可能性。线程安全的开发是重要的,同时线程安全会带来性能损失。因此线程安全在需要实现的地方来实现。
  • 一种是牺牲粒度包含大段的代码——甚至在排他锁中访问全局对象,迫使在更高的级别上实现串行化访问。这一策略也很关键,让非线程安全的对象用于线程安全代码中,避免了相同的互斥锁被用于保护对在非线程安全对象的所有的属性、方法和字段的访问。
  • 另一个方式欺骗是通过最小化共享数据来最小化线程交互。这是一个很好的途径,被暗中地用于“弱状态”的中间层程序和web服务器。自多个客户端请求同时到达,每个请求来自它自己的线程(效力于ASP.NET,Web服务器或者远程体系结构),这意味着它们调用的方法一定是线程安全的。弱状态设计(因伸缩性好而流行)本质上限制了交互的能力,因此类不能够在每个请求间持久保留数据。线程交互仅限于可以被选择创建的静态字段,多半是在内存里缓存常用数据和提供基础设施服务,例如认证和审核。
7、释放
一个被阻止的线程可以通过Interrupt和Abort两个方法提前释放,但是这个操作必须通过别的活动的线程实现,等待的线程没有能力对其被阻止的状态做任何事情。使用Interrupt后会继续执行知道下一次被阻止时,抛出ThreadInterruptedException异常。但是使用Abort并不会继续执行,会在线程当前所执行的位置抛出异常。
        static SemaphoreSlim sem = new SemaphoreSlim(3);
        static void Main(string[] args)
        {
            Thread t = new Thread(delegate()
            {
                try
                {
                    Thread.Sleep(Timeout.Infinite);
                }
                catch (ThreadInterruptedException)
                {
                    Console.WriteLine("Exception!");
                }
                Console.WriteLine("Woken");
            });
            t.Start();
            t.Interrupt();
            Console.ReadKey();
        }
C#多线程(三)
三、使用等待句柄通信
EventWaitHandle有两个子类AutoResetEvent和ManualResetEvent,这些类在线程间进程信号传输是非常容易的。
1、AutoResetEvent
使用WaitOne方法等待或阻止一个线程,调用Set方法来检测是否让该线程通过执行,如果有许多线程调用WaitOne,则会形成一个队列,其他未阻塞的线程通过调用Set方法来释放一个阻塞。如果调用Set时没有线程处于等待状态,那么句柄保持打开知道某个线程调用了WaitOne方法,但是在没有等待的时候重复调用Set方法不会让多个线程通过。

WaitOne 接受一个可选的超时参数——当等待以超时结束时这个方法将返回false,WaitOne在等待整段时间里也通知离开当前的同步内容,为了避免过多的阻止发生。Reset作用是关闭旋转门,也就是无论此时是否已经set过,都将阻塞下一次WaitOne——它应该是开着的。

使用构造函数创建对象或者使用基类创建:

EventWaitHandle wh = new AutoResetEvent (false);
EventWaitHandle wh = new EventWaitHandle (false, EventResetMode.Auto);
2、跨进程的EventWaitHandle

EventWaitHandle的构造器允许以“命名”的方式进行创建,它有能力跨多个进程。名称是个简单的字符串,可能会无意地与别的冲突!如果名字使用了,你将引用相同潜在的EventWaitHandle,除非操作系统创建一个新的。

EventWaitHandle wh = new EventWaitHandle (false, EventResetMode.Auto,
  "MyCompany.MyApp.SomeName");
如果两个程序都运行上述代码,他们就可以彼此发送信号,等待句柄可以跨越两个进程中的所有线程。
3、任务确认

假设希望在后台完成任务,但又不在每次得到任务时再创建一个新的线程。可以通过一个轮询的线程来完成:等待一个任务,执行它,然后等待下一个任务。这是一个普遍的多线程方案。也就是在创建线程上切分内务操作,任务执行被序列化,在多个工作线程和过多的资源消耗间排除潜在的不想要的操作。

        static EventWaitHandle ready = new AutoResetEvent(false);
        static EventWaitHandle go = new AutoResetEvent(false);
        static volatile string task;
        static void Main(string[] args)
        {
            new Thread(work).Start();

            for (int i = 1; i <= 5; i++)
            {
                ready.WaitOne();
                task = "#".PadRight(i, ‘@‘);
                go.Set();
            }
            ready.WaitOne();
            task = null;
            go.Set();
            Console.ReadKey();
        }

        static void work()
        {
            while (true)
            {
                ready.Set();
                go.WaitOne();
                if (task == null) return;
                Console.WriteLine(task);
            }
        }
C#多线程(三)

4、生成者消费者模型

还有一个普遍的线程方案是在后台工作进程从队列中分配任务,称为生产者/消费者队列:在工作线程中生产者入列任务,消费者出列任务。下面例子中,AutoResetEvent用来通知工作线程,只有在用完任务事等待。集合类队列来表示,通过锁来控制访问确保线程安全,队列为Null时结束任务。

     class ProducerConsumerQueue : IDisposable
    {
        EventWaitHandle wh = new AutoResetEvent(false);
        Thread worker;
        object locker = new object();
        Queue<string> tasks = new Queue<string>();

        public ProducerConsumerQueue()
        {
            worker = new Thread(Work);
            worker.Start();
        }

        public void EnqueueTask(string task)
        {
            lock (locker) tasks.Enqueue(task);
            wh.Set();
        }

        public void Dispose()
        {
            EnqueueTask(null);     // Signal the consumer to exit.
            worker.Join();          // Wait for the consumer‘s thread to finish.
            wh.Close();             // Release any OS resources.
        }

        void Work()
        {
            while (true)
            {
                string task = null;
                lock (locker)
                    if (tasks.Count > 0)
                    {
                        task = tasks.Dequeue();
                        if (task == null) return;
                    }
                if (task != null)
                {
                    Console.WriteLine("Performing task: " + task);
                    Thread.Sleep(1000);
                }
                else
                    wh.WaitOne();
            }
        }
    }

        static void Main(string[] args)
        {
            using (ProducerConsumerQueue q = new ProducerConsumerQueue())
            {
                q.EnqueueTask("Hello");
                for (int i = 0; i < 10; i++) q.EnqueueTask("Say " + i);
                q.EnqueueTask("Goodbye!");
            }
            Console.ReadKey();
        }

C#多线程(三)

5、ManualResetEvent

ManualResetEvent是AutoResetEvent变化的一种形式,它的不同之处在于:在线程被WaitOne的调用而通过的时候,它不会自动地reset,这个过程就像大门一样——调用Set打开门,允许任何数量的已执行WaitOne的线程通过;调用Reset关闭大门,可能会引起一系列的“等待者”直到下次门打开。

ManualResetEvent有时被用于给一个完成的操作发送信号,又或者一个已初始化正准备执行工作的线程。

参考:http://www.albahari.com/threading/part2.aspx

C#多线程(三),布布扣,bubuko.com

C#多线程(三)

上一篇:Python之easy_install安装出错


下一篇:spring的aop实现