Just Focus on Techonology

Lead My Life

原来线程-2 (直面多线程)

 

前面一篇文章(原来线程-1)中介绍了一些线程的基本概念和一些简单的应用,现在来看看线程的高级使用多线程.

什么是多线程程序?

有很长一段时间,大多数应用应用程序(除了嵌入式的)均是单线程的,也就是说在整个应用程序中只在一个线程中运行。在这种情况下如果要执行计算单元B,必须要等到计算单元2结束,一个程序从第一步开始,接着第二步,第三步,直到最后一步,这样按步依次执行,难道各位不认为这样很浪费时间吗?相信大多数会回答是。因为人们都想在同一时间执行多个任务,从而尽量节约时间。一个多线程应用程序允许你运行多个线程,每一个线程都运行在他们所在的进程之中(线程与进程的区别请看我的前一篇文章),因此可以在同一时间在一个线程中运行第一步,在另一个线程中运行第二步,还可以让第三步,第四步都在同一时间运行,这样执行完一个任务所花去的时间便只有运行在单线程情况下所花时间的四分之一了。但是,为什么不是每一个程序都是多线程的呢?我们可以想象一下,如果A需要B的一些信息,可当AB之前便运行完毕,这时A便有可能得到错误的运行结果。

 

作个比喻

如果前面的说法显得不那么容易理解,不妨换种方式来思考,我们可以将多线程看作人的身体。人的每一个身体器官(从生物的定义来看是有一定功能的组织),比如心,肺,肝,大脑。这些都包含在一个进程中,而每一个进程都是同时运行的。想象一下,如果每一个器官都是按步骤来工作的话:先是心脏,再是大脑,接着是肝,然后是肺,这样马上人就可以say goodbye 了。所以人的身体就像一个巨大的多线程程序,所有的器官都同时工作且依赖着其他的器官。它们之间通过化学信号和血液的流动来互相联系(当然了,实际中的多线程程序的复杂程度是无论如何也不能和人体相比的)。对人体而言,如果某一些器官不能获得其他器官的一些信息,或是有些工作的快了,有些工作的慢了,我们就会感觉不适,这就是为什么器官的工作必须要是同步的,因为它们要正常地实现人体的机能。

线程用得越多越好吗

显然任何有点生活常识的人都知道,再好吃的东西,吃得太多对身体都不好。任何事物的都存在的一个度。线程也是一样。由于每一个线程在内存中都占用了一部分空间并且每多开一个线程便会给CPU 多施加一份计算负担。举一个简单的例子,相信很多人都看过马戏团中抛球的杂耍吧,如果抛一个球,相当于单线程,这会非常简单,几乎不用计算时间和把握实际,当抛两个球时,也只需要做简单的切换,当抛三个球时,这时便需要做一些计算了,因为需要控制节奏,这时对于一般人已经有点难度了,当抛四个,五个,甚至更多时,那种计算量将会十分巨大,对要求也越来越高,而且极易出错,对于线程也是这个道理,线程的多少,能够满足需要就行,CPU的运算负担太大,往往会发生很多未知错误。

 

在使用线程中存在的问题

如果程序中每一个线程都是纯粹独立排它的——也就是都不依靠其他的任何一个线程,那么多线程程序将会使用起来非常的容易而且很少有错误发生。这样,每一个线程都可以舒舒服服地,自由自在地,无拘无束地在属于自己的空间中运行而且不影响周围的兄弟朋友。可是实际上,当有超过一个的线程需要读写内存的时候,问题便发生了。举一个例子,如果有两个线程,AB,两个线程都共享一个变量X,假如这时A先往X中写入了数值6,之后BX中写入了-2,最后X的值将会是-2,反过来,B先写入了-2A后写入了6,那么X最后的值将会是6。显然,X的值和后一个使用他的线程有关。在单线程程序中,这种事是不可能发生的,因为所有的东西都是顺序进行的。在单线程程序中,由于不存在并行的线程,X的值总是先由方法1设定,然后是方法2,这里不会发生任何令人奇怪的事,因为他们是一步接着一步运行的。而在多线程程序中,两个线程同时进入一小段代码就会对结果造成影响。处理这样的问题需要对访问同一共享数据的线程进行控制,根据不同的需要赋予不同的先后顺序。这时便涉及到线程安全的问题了。

 

线程安全

在前面我举了一个抛球的例子,这里我们再来看看,想象一下如果每次我们都是抛三个球,让在空中的那个球悬停在原处,这时将左手的球抛向右手,好,这是让空中的那个球落下同时右手的球抛向空中,左手的球抛向右手,此时又让空中的球停住,这个时候,之前停在空中的球已经顺利到了你的左手。依次这般,抛球就会变的非常简单。这就是线程安全。在我们的程序之中强行的让一个线程等待另一个线程完成后再开始,这种情况就叫做线程阻塞或是线程异步。在C# 中我们会锁住内存的一部分(通常是一个对象的实例)不允许其他线程进入,直到调用这部分内存的线程结束时才可以。好了,说了那么多,还是让我们来看看一些例子吧。

在下面的例子中,将会创建两个线程,Thread 1 Thread 2,一个共享变量threadOurput.threadOutput 将会根据他所在的线程而被赋予不同的消息。下面我们就来看看例1

1

        

class Program

    
{

        
bool stopThreads = false;

        
private string threadOutput = "";

        

        
/// <summary>

           

            
/// 线程1: 显示在线程

            
/// </summary>


 

        
void DisplayThread1()

            
{

                  
while (stopThreads == false)

                  
{

                        Console.WriteLine(
"显示线程");

                        
// Assign the shared memory to a message about thread #1

                        threadOutput 
= "好啊,线程";

 

                        Thread.Sleep(
1000); // simulate a lot of processing 

                        
// tell the user what thread we are in thread #1, and display shared memory

                        Console.WriteLine(
"线程输出--> {0}", threadOutput);

 

                  }


            }


 

            
/// <summary>

            
/// Thread 2: Loop continuously,

            
/// Thread 2: Displays that we are in thread 2

            
/// </summary>


            
void DisplayThread2()

            
{

                  
while (stopThreads == false)

                  
{

                    Console.WriteLine(
"显示线程");

 

                   
// Assign the shared memory to a message about thread #2

                    threadOutput 
= "好啊,线程";

 

                    Thread.Sleep(
1000); // simulate a lot of processing

 

                   
// tell the user we are in thread #2

                    Console.WriteLine(
"线程输出--> {0}", threadOutput);

                  }


            }


 

            
void class1(){

             

                  Thread thread1 
= new Thread(new ThreadStart(this.DisplayThread1));

                  Thread thread2 
= new Thread(new ThreadStart(this.DisplayThread2));

 

                  

                  thread1.Start();

                  thread2.Start();

                  }


 

 

        
static void Main(string[] args)

        
{

 

            Program program 
= new Program();

            program.class1();

 

        }


    }

上面代码的输出结果显示在图2中。仔细地看这个结果,你会发现这段程序给出了与预料的不一致的结果。虽然我们认真地给threadOutput赋予了相应的字符串,但是输出依然不是我们所预期的那样,这是为什么呢?


1 –两个不正常的输出.

我们原本希望看到如下的输出结果。

 线程1输出 --> 好啊,线程1    线程2输出 --> 好啊,线程2,   但是绝大多数的输出都是和我们预想的是不一致的。有时我们会看到 线程2输出-->好啊,线程1   线程1输出-->好啊,线程2. threadOutput并没有和代码匹配!

解释,为什么?

以本程序为例,看到这样结果的原因是代码执行了两个方法DisplayThread1DisplayThread2,每个方法都使用变量threadOutput , 所以很有可能threadOutput虽然在线程1中被赋上了值“好啊,线程1”并且把它显示出来,但是在线程1threadOutput赋值并将其值显示的时候,这时线程2threadOutput的值赋为“好啊,线程2”。由于对同一变量的占用,出现如图1那样的奇怪结果也就完全可能了。这个很是令人头疼的线程问题在线程的编程中相当平常,通常被叫做争用条件。对于程序而言,在某些时候争用简直就是噩梦,因为它不常出现而且很难很难去再现。

在争用中获胜

 避免争用的最好方式是编写线程安全的代码。 如果你的代码是线程安全的,就能防止一些突发的线程问题。 下面是一些编写线程安全代码的技巧。首先是尽量少的去共享内存。如果你创建了一个类的实例而且他运行在线程中,而此时你又在另一个线程中创建相同类的实例,这些类是线程安全的,只要他们没有包含任何静态变量。这两个类在内存中都有自己的运行域,没有共享内存 。如果在类或实例中确实含有静态变量并对其他线程共享,你就必须要寻找到某个方式确保一个线程不能使用这部分内存知道其他的某一线程完成对其的使用。 我们管这种防止其他线程影响被占用内存的方法叫做锁定C#中允许我们上锁代码通过Monitor类或lock{}结构 。对lock的使用,让我们看看下面这段代码吧。

2

 

         class Program

    
{

        
bool stopThreads = false;

        
private string threadOutput = "";

        
/// <summary>

        
/// 线程

        
/// </summary>


        
void DisplayThread1()

        
{

            
while (stopThreads == false)

            
{

                
//对线程上锁

                
lock (this)

                
{

                    Console.WriteLine(
"显示线程1");

                    threadOutput 
= "好啊,线程";

                    Thread.Sleep(
1000); 

                   

                    Console.WriteLine(
"线程输出--> {0}", threadOutput);

                }
 

            }


        }


 

        
/// <summary>

        
/// 线程

        
/// </summary>


        
void DisplayThread2()

        
{

            
while (stopThreads == false)

            
{

                
// 对线程上锁

                
lock (this)

                
{

                    Console.WriteLine(
"显示线程2");

                    threadOutput 
= "好啊,线程";

                    Thread.Sleep(
1000); // simulate a lot of processing

                    
// tell the user what thread we are in thread #1

                    Console.WriteLine(
"线程输出--> {0}", threadOutput);

                }
 // lock released for thread #2 here

            }


        }


 

            
void class1(){

             

                  Thread thread1 
= new Thread(new ThreadStart(this.DisplayThread1));

                  Thread thread2 
= new Thread(new ThreadStart(this.DisplayThread2));

 

                  

                  thread1.Start();

                  thread2.Start();

                  }


 

 

        
static void Main(string[] args)

        
{

 

            Program program 
= new Program();

            program.class1();

 

        }
 

对线程上锁后的运行结果如图3所示注意他们都同步正常显示了,正如我们所需要的结果一样: 线程1输出 --> 好啊,线程1    线程2输出 --> 好啊,线程2.   但是需要注意的是对线程的锁定是要付出一定代价的。当你锁定了一个线程的时候,是强行让其他的线程等待知道线程锁释放。实际上,程序的速度已经被减慢了,因为当其他线程在等待使用共享内存的时候,第一个线程什么事也没有做。因此,我们应该很节约地使用lock不要对每一个在代码中的方法都上锁,然而他们并不是在共享的内存中。要小心使用locks , 因为谁也不想让线程1去等线程2的锁释放,让线程2去等待线程1的锁释放,你等我,我又等你,勇无终日, 造成了死锁。这是谁也不希望发生的。

 

 2 –对双线程上锁的异步调用

简单的总结

对多线程的使用,是一个较为高级的技术,然而他又是一把双刃剑,使用得好,可以有效的提高性能,反之,则会使性能下降,甚至造成死机。线程还有很多的应用,我在后面的文章中会一一提及。

posted on 2006-07-07 05:06  ColinYang  阅读(2228)  评论(7编辑  收藏  举报

导航