您的位置:首页 > 编程语言 > C#

c# 任务、线程与同步

2014-06-07 16:10 155 查看
在dotNet4之前,必须直接使用Thread类和ThreadPool类编写线程。现在,dotNet对这两个类做了抽象,允许使用Parallel类和Task类。另外要分清两种主要的场景:任务并行性和数据并行性,任务并行性是把使用CPU的代码并行化,数据并行性是使用数据集合,在集合上执行的工作被划分为多个任务。

1.Thread

默认情况下,Thread类创建的线程是前台线程,该类也允许创建后台线程,以及设置线程的优先级。Thread类的构造函数可以接受ThreadStart,ThreadStart委托定义了一个返回类型为void的无参方法。

1.Thread.Join()

hread.Join()在MSDN中的解释:Blocks the calling thread until a thread terminates。

2.多线程调试

1.使用日志

2.利用断点

3.利用弹出窗口

4.利用vs自带的线程窗口

3.volatile关键字

volatile关键字仅应用于类或结构字段,用于通知编译器,将有多个线程访问该字段,因此它不应当对此成员的状态做任何优化,这样可以确保该字段在任何时间呈现的都是最新的值。

不是所有的类型都可以被定义为volatile字段,只有以下类型才可被定义为volatile:

引用类型。

指针类型(在不安全的上下文中)。

整型,如 sbyte、byte、short、ushort、int、uint、char、float 和 bool。

具有整数基类型的枚举类型。

已知为引用类型的泛型类型参数。

IntPtr 和
UIntPtr。

注意观察一下,就能发现只有值或引用的位数不超过本机整型值的位数(在32位系统中,为4个字节)的类型才能成为volatile。

参考:http://www.csharpwin.com/dotnetspace/12758r3828.shtml

3.给线程传递参数

(1)使用带ParameterizedThreadStart的构造函数,此时委托的参数类型必须是object。
(2)定义一个新类,在其中定义需要的字段,将线程的主方法定义为类的一个实例方法。
<pre name="code" class="csharp">public MyThread(string data)
{
this.data = data;
public void  ThreadMain()
{
....
}
}
var obj = new MyThread("info");
var t = new Thread(obj.ThreadMain);
t.Start();


4.线程间的同步

线程间如果要共享数据,就必须使用同步技术,确保一次只有一个线程访问和改变共享状态。lock语句、Interlocked类和Monitor类可以用于进程内部的同步。Mutex类、Event类、SemaphoreSlim类和ReaderWriterLockSlim类提供了多个进程之间的线程同步。

lock语句

lock的参数只能是引用类型,对于值类型只锁定一个副本,没有意义。lock语句块的最后,对象的锁定被解除。如果要锁定表态成员,可以把锁放在object类型上。
lock(obj)
{
// synchronized region
}

Interlocked类

提供了以线程安全的方式递增、递减、交换和读取值的方法。与其他同步技术相比,使用Interlocked类会快得多,但是,它只能用于简单的同步问题。
<pre name="code" class="csharp">lock(this)
{
retun ++state;
}
//相同的功能
return Interlocked.Increment(ref state);


Monitor类

lock语句由编译器解析为使用Monitor类,与lock语句相比,Monitor类的主要优点是:可以添加一个等待被锁定的超时值。
<pre name="code" class="csharp">lock(obj)
{
...
}
//编译器处理
Monitor.Enter(obj);
try
{
...
}
finally
{
Monitor.Exit(obj);
}
bool lockTaken = false;
Monitor.TryEnter(0bj,500,ref lockTaken);
if(lockTaken)
{
try{
....
}
finally{
Monitor.Exit(obj);
}
}
else
{
//didn't get the lock, do something else
}




SpinLock结构

SpinLock是在dotNet4开始引入的,除了体系结构上的区别之外,SpinLock结构的用法非常类似于Monitor类。获取锁定使用Enter()和TryEnter()方法,释放锁定使用Exit()方法。SpinLock结构还提供了属性IsHeld和IsHeldByCurrentThread,指定它当前是否是锁定的。

WaitHandle类

是一个抽象基类,用于等待一个信号的设置。Mutex、EventWaitHandle和Semaphore类都派生自WaitHandle。

Mutex类

是dotNet提供跨越多个进程同步访问的一个类,它非常类似于Monitor类,因为它们都只有一个线程能拥有锁定。

Semaphore类

信号量是一种计数的互斥锁定。dotNet4.5提供了两个类Semaphore和SemaphoreSlim。Semaphore类可以命名,使用系统范围内的资源,允许在不同的进程之间同步。SemaphoreSlim类是对较短等待时间进行了优化的轻型版本。

Event类

这也是一个系统范围内的资源同步方法,dotNet提供了ManualResetEvent、AutoResetEvent、ManualResetEventSlim和CountdownEvent类。
使用前面介绍的WaitHandle类,任务可以等待处于发信号状态的事件。

AutoResetEvent类

线程通过调用 AutoResetEvent 上的 WaitOne 来等待信号。如果 AutoResetEvent 处于非终止状态,则该线程阻塞,并等待当前控制资源的线程。AutoResetEven.Set()成功运行后,AutoResetEven.WaitOne()才能够获得运行机会;Set是发信号,WaitOne是等待信号,只有发了信号,等待的才会执行。如果不发的话,WaitOne后面的程序就永远不会执行。

调用 SetAutoResetEvent 发信号以释放等待线程。AutoResetEvent 将保持终止状态,直到一个正在等待的线程被释放,然后自动返回非终止状态。如果没有任何线程在等待,则状态将无限期地保持为终止状态。

可以通过将一个布尔值传递给构造函数来控制 AutoResetEvent 的初始状态,如果初始状态为终止状态,则为true;否则为 false

AutoResetEvent与ManualResetEvent的区别

他们的用法\声明都很类似,Set方法将信号置为发送状态 Reset方法将信号置为不发送状态WaitOne等待信号的发送。其实,从名字就可以看出一个手动,

一个自动,这个手动和自动实际指的是在Reset方法的处理上,如下面例子:

public AutoResetEvent autoevent=new AutoResetEvent(true);

public ManualResetEvent manualevent=new ManualResetEvent(true);

默认信号都处于发送状态,

autoevent.WaitOne();

manualevent.WaitOne();

如果 某个线程调用上面该方法,则当信号处于发送状态时,该线程会得到信号,得以继续执行。差别就在调用后,autoevent.WaitOne()每次只允许一个线程

进入,当某个线程得到信号(也就是有其他线程调用了autoevent.Set()方法后)后,autoevent会自动又将信号置为不发送状态,则其他调用WaitOne的线程只

有继续等待.也就是说,autoevent一次只唤醒一个线程。而manualevent则可以唤醒多个线程,因为当某个线程调用了set方法后,其他调用waitone的线程

获得信号得以继续执行,而manualevent不会自动将信号置为不发送.也就是说,除非手工调用了manualevent.Reset().方法,则manualevent将一直保持有信号状态,manualevent也就可以同时唤醒多个线程继续执行。如果上面的程序换成ManualResetEvent的话,就需要在waitone后面做下reset。

2.Parallel类

1.Parallel.For()

在For方法中,前两个参数定义了循环的开头和结束,第3个参数是一个Action(int)委托,其中的int参数是循环的迭代次数,该参数被传递给委托引用的方法。
For方法第3个参数还可以是Action(int,ParallelLoopState),ParallelLoopState可以调用Break和Stop方法终止For方法。
For方法的另一个泛型版本可以接受3个委托参数:
(1)Func<TLocal>,这个方法仅对用于执行迭代的每个线程调用一次,相当于对线程进行初始化。
(2)Func<int,ParallelLoopState,string,string>为循环体定义委托,前一个string接收初始化的返回值,后一个string用于自身返回值
(3)Action<TLocal>,每个线程只调用一次,是线程退出方法。

2.Parallel.Foreach()

与For方法相似,只是Foreach方法使用在实现了IEnumerable的集合上。

3.Parallel.Invoke()

Invoke方法用于提供任务的并行性,它的参数可以是一个Action委托数组。

3.Task

Task可以在单独的线程中运行,也可以以同步方式启动,这需要等待主调用线程,Task可以对底层线程进行更多的控制。

1.启动任务

可以使用TaskFactory类或Task类的构造函数和Start()方法,其中构造函数的灵活性比较大。
var tf = new TaskFactory();
Task t2 = tf.StartNew(TaskMethod,"using a task Factory");
实例化TaskFactory类,在其中把TaskMethod方法传递给StartNew方法,就会立即启动任务
Task t2 = Task.Factory.StartNew(TaskMethod,"factory via a task");
与前一个方法很相似,但使用的是Task类的静态属性Factory来访问TaskFactory,但是对厂的控制则没有那么全面。
var t3 = new Task(TaskMethod,"using a constructor and start");
t3.Start();
使用Task类的构造函数,实例化Task类只是指定了Created状态,接着调用Task类的Start()方法来启动任务。
Task t4 = Task.Run(()=>TaskMethod("using the Run method"));
这种方式是dotNet4.5新增的。

使用用Task的构造函数和TaskFactory的StartNew方法时,可以传递TaskCreationOptions枚举中的值,利用这个创建项,可以改变任务的行为。

2.连续任务

连续任务通过在任务上调用ContinueWith()方法来定义,在一个任务结束时,可以启动多个任务。

3.任务层次结构

在一个任务中启动另一个任务时,就启动了一个父子层次关系。如果父任务在子任务之前结束,其状态就会是WatingForChildrenToComplete,所有子任务也结束时,父任务的状态就变成RanToCompletion。

4.线程池

ThreadPool类托管一个线程列表,这个类会在需要的时候增加线程池中线程的数量,直到最大的线程数。如果有更多的作业要处理,线程池中线程的个数也到了极限,最新的作业就要排队,且必须等待线程完成其任务。
线程池使用起来很简单,但它有一些限制:
(1)线程池中所有线程都是后台线程,如果进程的所有前台进程都结束了,所有的后台线程就会停止。
(2)不能给入线程池的线程设置优先级或名称。
(3)入池的线程只能用于时间较短的任务,长时间的任务应用Thread
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: