浅析.Net下的多线程编程

浅析.Net下的多线程编程

 

多线程是许多操作系统所具有的特性,它能大大提高程序的运行效率,所以多线程编程技术为编程者广泛关注。目前微软的.Net战略正进一步推进,各种相关的技术正为广大编程者所接受,同样在.Net中多线程编程技术具有相当重要的地位。本文我就向大家介绍在.Net下进行多线程编程的基本方法和步骤。
   
开始新线程

  
.Net下创建一个新线程是非常容易的,你可以通过以下的语句来开始一个新的线程:
    Thread thread = new Thread (new ThreadStart (ThreadFunc));
     thread.Start ();
   
第一条语句创建一个新的Thread对象,并指明了一个该线程的方法。当新的线程开始时,该方法也就被调用执行了。该线程对象通过一个System..Threading.ThreadStart类的一个实例以类型安全的方法来调用它要调用的线程方法。

   
第二条语句正式开始该新线程,一旦方法Start()被调用,该线程就保持在一个"alive"的状态下了,你可以通过读取它的IsAlive属性来判断它是否处于"alive"状态。下面的语句显示了如果一个线程处于"alive"状态下就将该线程挂起的方法:

    if (thread.IsAlive) {
     thread.Suspend ();
     }
   
不过请注意,线程对象的Start()方法只是启动了该线程,而并不保证其线程方法ThreadFunc()能立即得到执行。它只是保证该线程对象能被分配到CPU时间,而实际的执行还要由操作系统根据处理器时间来决定。

   
一个线程的方法不包含任何参数,同时也不返回任何值。它的命名规则和一般函数的命名规则相同。它既可以是静态的(static)也可以是非静态的(nonstatic)。当它执行完毕后,相应的线程也就结束了,其线程对象的IsAlive属性也就被置为false了。下面是一个线程方法的实例:

        public static void ThreadFunc()
     {
     for (int i = 0; i <10; i++) {
     Console.WriteLine("ThreadFunc {0}", i);
     }
     }
   
前台线程和后台线程

    .Net
的公用语言运行时(Common Language RuntimeCLR)能区分两种不同类型的线程:前台线程和后台线程。这两者的区别就是:应用程序必须运行完所有的前台线程才可以退出;而对于后台线程,应用程序则可以不考虑其是否已经运行完毕而直接退出,所有的后台线程在应用程序退出时都会自动结束。
   
一个线程是前台线程还是后台线程可由它的IsBackground属性来决定。这个属性是可读又可写的。它的默认值为false,即意味着一个线程默认为前台线程。我们可以将它的IsBackground属性设置为true,从而使之成为一个后台线程。

   
下面的例子是一个控制台程序,程序一开始便启动了10个线程,每个线程运行5秒钟时间。由于线程的IsBackground属性默认为false,即它们都是前台线程,所以尽管程序的主线程很快就运行结束了,但程序要到所有已启动的线程都运行完毕才会结束。示例代码如下:

     using System;
     using System.Threading;
     class MyApp
     {
     public static void Main ()
     {
     for (int i=0; i<10; i++) {
     Thread thread = new Thread (new ThreadStart (ThreadFunc));
     thread.Start ();
     }
     }
     private static void ThreadFunc ()
     {
     DateTime start = DateTime.Now;
     while ((DateTime.Now - start).Seconds <5)
     ;
     }
     }
   
接下来我们对上面的代码进行略微修改,将每个线程的IsBackground属性都设置为true,则每个线程都是后台线程了。那么只要程序的主线程结束了,整个程序也就结束了。示例代码如下:

        using System;
     using System.Threading;
     class MyApp
     {
     public static void Main ()
     {
     for (int i=0; i<10; i++) {
     Thread thread = new Thread (new ThreadStart (ThreadFunc));
     thread.IsBackground = true;
     thread.Start ();
     }
     }
     private static void ThreadFunc ()
     {
     DateTime start = DateTime.Now;
     while ((DateTime.Now - start).Seconds <5)
     ;
     }
     }
   
既然前台线程和后台线程有这种差别,那么我们怎么知道该如何设置一个线程的IsBackground属性呢?下面是一些基本的原则:对于一些在后台运行的线程,当程序结束时这些线程没有必要继续运行了,那么这些线程就应该设置为后台线程。比如一个程序启动了一个进行大量运算的线程,可是只要程序一旦结束,那个线程就失去了继续存在的意义,那么那个线程就该是作为后台线程的。而对于一些服务于用户界面的线程往往是要设置为前台线程的,因为即使程序的主线程结束了,其他的用户界面的线程很可能要继续存在来显示相关的信息,所以不能立即终止它们。这里我只是给出了一些原则,具体到实际的运用往往需要编程者的进一步仔细斟酌。

   
线程优先级

   
一旦一个线程开始运行,线程调度程序就可以控制其所获得的CPU时间。如果一个托管的应用程序运行在Windows机器上,则线程调度程序是由Windows所提供的。在其他的平台上,线程调度程序可能是操作系统的一部分,也自然可能是.Net框架的一部分。不过我们这里不必考虑线程的调度程序是如何产生的,我们只要知道通过设置线程的优先级我们就可以使该线程获得不同的CPU时间。
   
线程的优先级是由Thread.Priority属性控制的,其值包含:ThreadPriority.HighestThreadPriority.AboveNormalThreadPriority.NormalThreadPriority.BelowNormalThreadPriority.Lowest。从它们的名称上我们自然可以知道它们的优先程度,所以这里就不多作介绍了。

   
线程的默认优先级为ThreadPriority.Normal。理论上,具有相同优先级的线程会获得相同的CPU时间,不过在实际执行时,消息队列中的线程阻塞或是操作系统的优先级的提高等原因会导致具有相同优先级的线程会获得不同的CPU时间。不过从总体上来考虑仍可以忽略这种差异。你可以通过以下的方法来改变一个线程的优先级。

    thread.Priority = ThreadPriority.AboveNormal;
   
或是:

    thread.Priority = ThreadPriority.BelowNormal;
   
通过上面的第一句语句你可以提高一个线程的优先级,那么该线程就会相应的获得更多的CPU时间;通过第二句语句你便降低了那个线程的优先级,于是它就会被分配到比原来少的CPU时间了。你可以在一个线程开始运行前或是在它的运行过程中的任何时候改变它的优先级。理论上你还可以任意的设置每个线程的优先级,不过一个优先级过高的线程往往会影响到其他线程的运行,甚至影响到其他程序的运行,所以最好不要随意的设置线程的优先级。

   
挂起线程和重新开始线程

    Thread
类分别提供了两个方法来挂起线程和重新开始线程,也就是Thread.Suspend能暂停一个正在运行的线程,而Thread.Resume又能让那个线程继续运行。不像Windows内核,.Net框架是不记录线程的挂起次数的,所以不管你挂起线程过几次,只要一次调用Thread.Resume就可以让挂起的线程重新开始运行。
    Thread
类还提供了一个静态的Thread.Sleep方法,它能使一个线程自动的挂起一定的时间,然后自动的重新开始。一个线程能在它自身内部调用Thread.Sleep方法,也能在自身内部调用Thread.Suspend方法,可是一定要别的线程来调用它的Thread.Resume方法才可以重新开始。这一点是不是很容易想通的啊?下面的例子显示了如何运用Thread.Sleep方法:

        while (ContinueDrawing) {
     DrawNextSlide ();
     Thread.Sleep (5000);
     }
   
终止线程

   
在托管的代码中,你可以通过以下的语句在一个线程中将另一个线程终止掉:
    thread.Abort ();
   
下面我们来解释一下Abort()方法是如何工作的。因为公用语言运行时管理了所有的托管的线程,同样它能在每个线程内抛出异常。Abort()方法能在目标线程中抛出一个ThreadAbortException异常从而导致目标线程的终止。不过Abort()方法被调用后,目标线程可能并不是马上就终止了。因为只要目标线程正在调用非托管的代码而且还没有返回的话,该线程就不会立即终止。而如果目标线程在调用非托管的代码而且陷入了一个死循环的话,该目标线程就根本不会终止。不过这种情况只是一些特例,更多的情况是目标线程在调用托管的代码,一旦Abort()被调用那么该线程就立即终止了。

   
在实际应用中,一个线程终止了另一个线程,不过往往要等那个线程完全终止了它才可以继续运行,这样的话我们就应该用到它的Join()方法。示例代码如下:

    thread.Abort (); //
要求终止另一个线程

     thread.Join (); //
只到另一个线程完全终止了,它才继续运行

   
但是如果另一个线程一直不能终止的话(原因如前所述),我们就需要给Join()方法设置一个时间限制,方法如下:

    thread.Join (5000); //
暂停5

   
这样,在5秒后,不管那个线程有没有完全终止,本线程就强行运行了。该方法还返回一个布尔型的值,如果是true则表明那个线程已经完全终止了,而如果是false的话,则表明已经超过了时间限制了。

   
时钟线程

    .Net
框架中的Timer类可以让你使用时钟线程,它是包含在System.Threading名字空间中的,它的作用就是在一定的时间间隔后调用一个线程的方法。下面我给大家展示一个具体的实例,该实例以1秒为时间间隔,在控制台中输出不同的字符串,代码如下:
    using System;
     using System.Threading;
     class MyApp
     {
     private static bool TickNext = true;
     public static void Main ()
     {
     Console.WriteLine ("Press Enter to terminate...");
     TimerCallback callback = new TimerCallback (TickTock);
     Timer timer = new Timer (callback, null, 1000, 1000);
     Console.ReadLine ();
     }
     private static void TickTock (object state)
     {
     Console.WriteLine (TickNext ? "Tick" : "Tock");
     TickNext = ! TickNext;
     }
     }
   
从上面的代码中,我们知道第一个函数回调是在1000毫秒后才发生的,以后的函数回调也是在每隔1000毫秒之后发生的,这是由Timer对象的构造函数中的第三个参数所决定的。程序会在1000毫秒的时间间隔后不断的产生新线程,只到用户输入回车才结束运行。不过值得注意的是,虽然我们设置了时间间隔为1000毫秒,但是实际运行的时候往往并不能非常精确。因为Windows操作系统并不是一个实时系统,而公用语言运行时也不是实时的,所以由于线程调度的千变万化,实际的运行效果往往是不能精确到毫秒级的,但是对于一般的应用来说那已经是足够的了,所以你也不必十分苛求。

   
小结

   
本文介绍了在.Net下进行多线程编程所需要掌握的一些基本知识。从文章中我们可以知道在.Net下进行多线程编程相对以前是有了大大的简化,但是其功能并没有被削弱。使用以上的一些基本知识,读者就可以试着编写.Net下的多线程程序了。不过要编写出功能更加强大而且Bug少的多线程应用程序,读者需要掌握诸如线程同步、线程池等高级的多线程编程技术。读者不妨参考一些操作系统方面或是多线程编程方面的技术丛书。

 

ThreadJoin方法 2008-08-06 17:50

Join()方法会暂停给定的线程。
连接两个线程的意思就是调用Join()方法时,运行着的线程将进入WaitSleepJoin状态,而直到调用Join()方法的线程完成了任务,该线程才会返回到Running状态。

using System;

using System.Threading;

namespace thread

{

    public class JoiningThread

{

        public static Thread FirstThread;

        public static Thread SecondThread;

        public static void Main(string[] args)

        {

            FirstThread = new Thread(new ThreadStart(First));

            SecondThread = new Thread(new ThreadStart(Second));

            FirstThread.Start();

            SecondThread.Start();        

        }

        static void First()

        {

           

            for (int i = 1; i <= 250; i++)

                Console.Write(i + " ");

        }

        static void Second()

       {

            FirstThread.Join();

            for (int i = 251; i <= 500; i++)

                Console.Write(i + " ");

        }

    }

}

这个示例依次将数字输出到控件台,从1开始到500为止。

First()方法将输出前250个数字,Second()方法则输出从251500的数字。

如果Second()方法中没有FirstThread.Join()行,执行流就会在两个方法之间来回切换,输出结果就会很混乱。通过在Sedond()方法中调用FirstThread.Join()方法,将暂停Second()方法的执行,直到FirstThread(First()方法)中的代码执行完毕。


ThreadState成员

Aborted 线程已终止

AbortRequested 发出终止线程的要求

Background 作为后台线程执行

Running 正在执行

Suspended 已被挂起

SuspendRequeste 要求被挂起

Unstarted 尚未开始

WaitSleepJoin 被封闭在对WaitSleepJoin的调用上


int i = 0 ;
System.Threading.Interlocked.Increment(
ref
i);
Console.WriteLine(i);
System.Threading.Interlocked.Decrement(
ref
i);
Console.WriteLine(i);
System.Threading.Interlocked.Exchange(
ref i, 100
);
Console.WriteLine(i);


private static object m_monitorObject = new object ();
[STAThread]
static void Main( string
[] args)
{
Thread thread =
new Thread( new
ThreadStart(Do));
thread.Name =
" Thread1 "
;
Thread thread2 =
new Thread( new
ThreadStart(Do));
thread2.Name =
" Thread2 "
;
thread.Start();
thread2.Start();
thread.Join();
thread2.Join();
Console.Read();
}
static void
Do()
{
if ( !
Monitor.TryEnter(m_monitorObject))
{
Console.WriteLine(
" Can't visit Object " +
Thread.CurrentThread.Name);
return
;
}
try

{
Monitor.Enter(m_monitorObject);
Console.WriteLine(
" Enter Monitor " + Thread.CurrentThread.Name);
Thread.Sleep(
5000
);
}
finally

{
Monitor.Exit(m_monitorObject);
}
}


using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
namespace MonitorApplication
{
    
class Program
     {
        
private static  object m_isFull = new object();
        
static void Main(string[] args)
         {
            Thread thread = 
new Thread(HaveLunch);
            thread.Name = 
"HaveLunchThread";
            Thread thread2 = 
new Thread(SeatChange);
            thread2.Name = 
"SeatChangeThread";
            thread.Start();
            System.Threading.Thread.Sleep(
2000);
            thread2.Start();
            thread.Join();
            thread2.Join();
        }

        
private static void HaveLunch()
         {
            
lock (m_isFull)
             {
                Console.WriteLine(
"Wati for seta");
                Monitor.Wait(m_isFull);
                Console.WriteLine(
"Have a good lunch!");
            }
        }

        
private static void SeatChange()
         {
            
lock (m_isFull)
             {
                Console.WriteLine(
"Seat was changed");
                Monitor.Pulse(m_isFull);
            }
        }
    }
}



输出信息如下

 


using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
using System.IO;
using System.Diagnostics;
namespace MonitorApplication
{
    
class Program
     {
        
static void Main(string[] args)
         {
            Mutex configFileMutex = 
new Mutex(false"configFileMutex");
            Console.WriteLine(
"Wait for configFileMutex Process Id : " + Process.GetCurrentProcess().Id);
            configFileMutex.WaitOne();
            Console.WriteLine(
"Enter configFileMutex Process Id : " + Process.GetCurrentProcess().Id);
            System.Threading.Thread.Sleep(
10000);
            
if (!File.Exists(@"./config.txt"))
             {
                FileStream stream = File.Create(
@"./config.txt");
                StreamWriter writer = 
new StreamWriter(stream);
                writer.WriteLine(
"This is a Test!");
                writer.Close();
                stream.Close();
            }
            
else
             {
                String[] lines = File.ReadAllLines(
@"./config.txt");
                
for (int i = 0; i < lines.Length; i++)
                    Console.WriteLine(lines[i]);
            }
            configFileMutex.ReleaseMutex();
            configFileMutex.Close();
        }
    }
}

 

C#线程同步的几种方法()---

2008-11-28 13:11

我们在编程的时候,有时会使用多线程来解决问题,比如你的程序需要在后台处理一 大堆数据,但还要使用户界面处于可操作状态;或者你的程序需要访问一些外部资源如数据库或网络文件等。这些情况你都可以创建一个子线程去处理,然而,多线 程不可避免地会带来一个问题,就是线程同步的问题。如果这个问题处理不好,我们就会得到一些非预期的结果。

  在网上也看过一些关于线程同步的文章,其实线程同步有好几种方法,下面我就简单的做一下归纳。

  一、volatile关键字

  volatile是最简单的一种同步方法,当然简单是要付出代价的。它只能在变量一级做同步,volatile的含义就是告诉处理器, 不要将我放入工作内存, 请直接在主存操作我。(【转自www.bitsCN.com 】)因此,当多线程同时访问该变量时,都将直接操作主存,从本质上做到了变量共享。

  能够被标识为volatile的必须是以下几种类型:(摘自MSDN

·  Any reference type.

·  Any pointer type (in an unsafe context).

·  The types sbyte, byte, short, ushort, int, uint, char, float, bool.

·  An enum type with an enum base type of byte, sbyte, short, ushort, int, or uint.

  如:

Code
public class
A
{
private volatile int
_i;
public int
I
{
get { return
_i; }
set { _i =
value; }
}
}

  但volatile并不能实现真正的同步,因为它的操作级别只停留在变量级别,而不是原子级别。如果是在单处理器系统中,是没有任何问题的,变量在主存中没有机会被其他人修改,因为只有一个处理器,这就叫作processor Self-Consistency。但在多处理器系统中,可能就会有问题。 每个处理器都有自己的data cach,而且被更新的数据也不一定会立即写回到主存。所以可能会造成不同步,但这种情况很难发生,因为cach的读写速度相当快,flush的频率也相当高,只有在压力测试的时候才有可能发生,而且几率非常非常小。

  二、lock关键字

  lock是一种比较好用的简单的线程同步方式,它是通过为给定对象获取互斥锁来实现同步的。它可以保证当一个线程在关键代码段的时候,另一个线程不会进来,它只能等待,等到那个线程对象被释放,也就是说线程出了临界区。用法:

Code
public void Function()
{
object lockThis = new object
();
lock
(lockThis)
{
//
Access thread-sensitive resources.
}

}

  lock的参数必须是基于引用类型的对象,不要是基本类型像 bool,int什么的,这样根本不能同步,原因是lock的参数要求是对象,如果传入int,势必要发生装箱操作,这样每次lock的都将是一个新的不 同的对象。最好避免使用public类型或不受程序控制的对象实例,因为这样很可能导致死锁。特别是不要使用字符串作为lock的参数,因为字符串被 CLR“暂留,就是说整个应用程序中给定的字符串都只有一个实例,因此更容易造成死锁现象。建议使用不被暂留的私有或受保护成员作为参数。其实某些 类已经提供了专门用于被锁的成员,比如Array类型提供SyncRoot,许多其它集合类型也都提供了SyncRoot

  所以,使用lock应该注意以下几点: 

  1、如果一个类的实例是public的,最好不要lock(this)。因为使用你的类的人也许不知道你用了lock,如果他new了一个实例,并且对这个实例上锁,就很容易造成死锁。

  2、如果MyTypepublic的,不要lock(typeof(MyType))

  3、永远也不要lock一个字符串

  三、System.Threading.Interlocked

  对于整数数据类型的简单操作,可以用 Interlocked 类的成员来实现线程同步,存在于System.Threading命名空间。Interlocked类有以下方法:Increment , Decrement , Exchange CompareExchange 。使用Increment Decrement 可以保证对一个整数的加减为一个原子操作。Exchange 方法自动交换指定变量的值。CompareExchange 方法组合了两个操作:比较两个值以及根据比较的结果将第三个值存储在其中一个变量中。比较和交换操作也是按原子操作执行的。如:

Code
int i = 0
;
System.Threading.Interlocked.Increment(
ref
i);
Console.WriteLine(i);
System.Threading.Interlocked.Decrement(
ref
i);
Console.WriteLine(i);
System.Threading.Interlocked.Exchange(
ref i, 100
);
Console.WriteLine(i);
System.Threading.Interlocked.CompareExchange(
ref i, 10 , 100 );

Output:

 

  四、Monitor

  Monitor类提供了与lock类似的功能,不过与lock不同的是,它 能更好的控制同步块,当调用了MonitorEnter(Object o)方法时,会获取o的独占权,直到调用Exit(Object o)方法时,才会释放对o的独占权,可以多次调用Enter(Object o)方法,只需要调用同样次数的Exit(Object o)方法即可,Monitor类同时提供了TryEnter(Object o,[int])的一个重载方法,该方法尝试获取o对象的独占权,当获取独占权失败时,将返回false

  但使用 lock 通常比直接使用 Monitor 更可取,一方面是因为 lock 更简洁,另一方面是因为 lock 确保了即使受保护的代码引发异常,也可以释放基础监视器。这是通过 finally 中调用Exit来实现的。事实上,lock 就是用 Monitor 类来实现的。下面两段代码是等效的:

Code
lock
(x)
{
DoSomething();
}
等效于

object obj = ( object )x;
System.Threading.Monitor.Enter(obj);
try

{
DoSomething();
}
finally
{
System.Threading.Monitor.Exit(obj);
}

关于用法,请参考下面的代码:

Code
private static object m_monitorObject = new object
();
[STAThread]
static void Main( string
[] args)
{
Thread thread =
new Thread( new
ThreadStart(Do));
thread.Name =
" Thread1 "
;
Thread thread2 =
new Thread( new
ThreadStart(Do));
thread2.Name =
" Thread2 "
;
thread.Start();
thread2.Start();
thread.Join();
thread2.Join();
Console.Read();
}
static void
Do()
{
if ( !
Monitor.TryEnter(m_monitorObject))
{
Console.WriteLine(
" Can't visit Object " +
Thread.CurrentThread.Name);
return
;
}
try

{
Monitor.Enter(m_monitorObject);
Console.WriteLine(
" Enter Monitor " + Thread.CurrentThread.Name);
Thread.Sleep(
5000
);
}
finally

{
Monitor.Exit(m_monitorObject);
}
}

  当线程1获取了m_monitorObject对象独占权时,线程2尝试调用TryEnter(m_monitorObject),此时会由于无法获取独占权而返回false,输出信息如下:

  另外,Monitor还提供了三个静态方法 Monitor.Pulse(Object o)Monitor.PulseAll(Object o)Monitor.Wait(Object o ) ,用来实现一种唤醒机制的同步。关于这三个方法的用法,可以参考MSDN,这里就不详述了。

  五、Mutex

  在使用上,Mutex与上述的Monitor比较接近,不过Mutex不具 备WaitPulsePulseAll的功能,因此,我们不能使用Mutex实现类似的唤醒的功能。不过Mutex有一个比较大的特点,Mutex是 跨进程的,因此我们可以在同一台机器甚至远程的机器上的多个进程上使用同一个互斥体。尽管Mutex也可以实现进程内的线程同步,而且功能也更强大,但这 种情况下,还是推荐使用Monitor,因为Mutex类是win32封装的,所以它所需要的互操作转换更耗资源。 

c#中多线程同步解决方案

C#中对于多线程编程有很好的支持,常用的有以下几种解决方案:

           1. Lock(object) 对需要同步的代码块加锁;

    2. Monitor Class

    3. ReaderWriterLock Class   

           4. Mutex Class

           5. Semaphore

           6. Event

      这次我主要说一下 Lock Monitor。对于Lock 想必有一定多线程编程经验的程序员都会很熟悉,看名字就知道大概是什么意思了。Lock 就是一把锁,举个很不雅得例子。比如说我们去上WC,当你进去后就要把门锁住,后来的人只能等你方便完了才能进去。

private string lockFlag = "LOCK";
        
public void GoWC(Person p)
        {
            
lock (lockFlag)
            {
                Console.WriteLine(
"Begin");
                  Console.WriteLine(
"End");
            }
        }

 

 

   Lock使用起来非常方便,有一点需要注意就是你同步的对象最好是privateprivate static,因为如果这个同步对象是publicinternal,就有可能被其他程序中其他部分的代码获得,这样的话你的lock就失去的作用,也就是说这个WC的钥匙只能有一把,只有进了WC的人才能获得,其他等待的人是没有办法获得的。

  另一个常用的解决方案是使用Monitor class monitor class lock其实本身没有什么区别,这难免会让人觉得费解,其实不然。对于使用lock关键字来说经常会发生一种让人非常郁闷的事情“Deadlock”,personA

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值