一、线程同步概述
在多线程程序中,当存在共享变量和抢占资源的情况时,需要使用线程同步机制来防止发生这些冲突,这样才能保证得到可预见的结果,也就是线程安全的。否则就会出现不可预知的结果产生线程不安全问题。特别是在访问同一个数据的时候最为明显。主要通过以下四个方式进行:
- 简单阻塞:让一个线程等待另一个线程执行结束或者等待一段时间而阻塞执行,使用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丢弃
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;
}
}
Monitor.Enter(locker);
try
{
if (val2 != 0) Console.WriteLine(val1/val2);
val2 = 0;
}
finally
{
Monitor.Exit(locker);
}
上述使用的locker同步对象,
必须是引用类型,最好是在私有类里面定义,防止外部锁定相同对象。可使用lock(this){...}来精确控制锁的范围和粒度,同时,
锁没有阻止对同步对象本身的访问。
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
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();
}
- 一种是牺牲粒度包含大段的代码——甚至在排他锁中访问全局对象,迫使在更高的级别上实现串行化访问。这一策略也很关键,让非线程安全的对象用于线程安全代码中,避免了相同的互斥锁被用于保护对在非线程安全对象的所有的属性、方法和字段的访问。
- 另一个方式欺骗是通过最小化共享数据来最小化线程交互。这是一个很好的途径,被暗中地用于“弱状态”的中间层程序和web服务器。自多个客户端请求同时到达,每个请求来自它自己的线程(效力于ASP.NET,Web服务器或者远程体系结构),这意味着它们调用的方法一定是线程安全的。弱状态设计(因伸缩性好而流行)本质上限制了交互的能力,因此类不能够在每个请求间持久保留数据。线程交互仅限于可以被选择创建的静态字段,多半是在内存里缓存常用数据和提供基础设施服务,例如认证和审核。
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();
}
三、使用等待句柄通信
1、AutoResetEvent
WaitOne 接受一个可选的超时参数——当等待以超时结束时这个方法将返回false,WaitOne在等待整段时间里也通知离开当前的同步内容,为了避免过多的阻止发生。Reset作用是关闭旋转门,也就是无论此时是否已经set过,都将阻塞下一次WaitOne——它应该是开着的。
使用构造函数创建对象或者使用基类创建:
EventWaitHandle wh = new AutoResetEvent (false);
EventWaitHandle wh = new EventWaitHandle (false, EventResetMode.Auto);
class BasicWaitHandle
{
static EventWaitHandle _waitHandle = new AutoResetEvent (false);
static void Main()
{
new Thread (Waiter).Start();
Thread.Sleep (1000); // Pause for a second...
_waitHandle.Set(); // Wake up the Waiter.
}
static void Waiter()
{
Console.WriteLine ("Waiting...");
_waitHandle.WaitOne(); // Wait for notification
Console.WriteLine ("Notified");
}
}
![](https://img-blog.csdn.net/20140402164924937?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvdTAxMDQ4NzU2OA==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast)
class BasicWaitHandle
{
static EventWaitHandle _waitHandle = new AutoResetEvent (false);
static void Main()
{
new Thread (Waiter).Start();
Thread.Sleep (1000); // Pause for a second...
_waitHandle.Set(); // Wake up the Waiter.
}
static void Waiter()
{
Console.WriteLine ("Waiting...");
_waitHandle.WaitOne(); // Wait for notification
Console.WriteLine ("Notified");
}
}
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);
}
}
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();
}
5、ManualResetEvent
ManualResetEvent是AutoResetEvent变化的一种形式,它的不同之处在于:在线程被WaitOne的调用而通过的时候,它不会自动地Reset,这个过程就像大门一样——调用Set打开门,允许任何数量的已执行WaitOne的线程通过;调用Reset则关闭大门。此时会阻塞所有调用了WaitOne的线程,只到下次调用Set时,可能会引起一系列的“等待者”在打开门时全部释放。除了这个区别之外,其他都和AutoResetEvent的用法一样。他们的速度都在一个微妙级别。
ManualResetEvent有时被用于给一个完成的操作发送信号,又或者一个已初始化正准备执行工作的线程。
在NET4.0中出现了优化的ManualResetEventSlim类,这个可以在等待的时候有更快的响应时间,可以通过CancellationToken来取消Wait,同时这个类不是WaitHandle类的子类,而是组合了WaitHandle类的一个对象作为一个属性,返回一个WaitHandle基类对象。同时优化过的这个类的速度是原来的50倍,也就是20个纳秒级别。
ManualResetEvent是让一个线程解除一系列等待的线程的阻塞。
6、CountdownEvent
CountdownEvent是NET4.0中新增的类,对于使用之前版本的NET的时候,可以使使用Wait和Paulse自己写一个同样功能的类。这个类是用来等待一系列线程执行完毕之后通知当前等待的这个线程执行。
CountdownEvent countdown= new CountdownEvent(3); //初始化等待线程数为3个
通过调用Signal方法来减小初始化的数目count,通过调用Wait来阻塞,只到count数为0的时候就接触阻塞。
static CountdownEvent _countdown = new CountdownEvent (3);
static void Main()
{
new Thread (SaySomething).Start ("I am thread 1");
new Thread (SaySomething).Start ("I am thread 2");
new Thread (SaySomething).Start ("I am thread 3");
_countdown.Wait(); // Blocks until Signal has been called 3 times
Console.WriteLine ("All threads have finished speaking!");
}
static void SaySomething (object thing)
{
Thread.Sleep (1000);
Console.WriteLine (thing);
_countdown.Signal();
}
三个线程启动之后,先执行Sleep,在输出过程出现了CPU轮换时间片的随机性,每次的结果都是不同的。主线程等待三工作线程执行完毕之后再执行就是此处线程同步通信所演示的效果。
当count还没有达到0的时候,可以使用AddCount增加,可以使用Reset重置为初始化的count数目,但是如果达到了0之后,再调用AddCount则会引发异常,此时可以使用TryAddCount,当count为0的时候返回false。