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丢弃
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、SemaphoreSemaphore在限制并发方面非常有用,可以阻止过多的线程在一段代码中一次执行,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();
}三、使用等待句柄通信
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);
}
}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关闭大门,可能会引起一系列的“等待者”直到下次门打开。
ManualResetEvent有时被用于给一个完成的操作发送信号,又或者一个已初始化正准备执行工作的线程。
原文:http://blog.csdn.net/u010487568/article/details/22309551