C#多线程总结
1.几个重要的概念。
(1)进程:当一个程序开始运行时,它就是一个进程,进程包括运行中的程序和程序所使用到的内存和系统资源。 而一个进程又是由多个线程所组成的。
(2)线程:线程是程序中的一个执行流,每个线程都有自己的专有寄存器(栈指针、程序计数器等),但代码区是共享的,即不同的线程可以执行同样的函数。
(3)多线程:多线程是指程序中包含多个执行流,即在一个程序中可以同时运行多个不同的线程来执行不同的任务,也就是说允许单个程序创建多个并行执行的线程来完成各自的任务。
(4)任何程序在执行时,至少有一个主线程。
多线程的好处:可以提高CPU的利用率。在多线程程序中,一个线程必须等待的时候,CPU可以运行其它的线程而不是等待,这样就大大提高了程序的效率。
多线程的缺点:线程也是程序,所以线程需要占用内存,线程越多占用内存也越多; 多线程需要协调和管理,所以需要CPU时间跟踪线程; 线程之间对共享资源的访问会相互影响,必须解决竞用共享资源的问题; 线程太多会导致控制太复杂,最终可能造成很多Bug。
2.在.net framework class library中,所有与多线程机制应用相关的类都是放在System.Threading命名空间中的。如果你想在你的应用程序中使用多线程,就必须包含这个类。
Thread类有几个重要的方法,如下:
Start():启动线程;
Sleep(int):静态方法,暂停当前线程指定的毫秒数;
Abort():通常使用该方法来终止一个线程;
Suspend():该方法并不终止未完成的线程,它仅仅挂起线程,以后还可恢复;
Resume():恢复被Suspend()方法挂起的线程的执行。
一个例子:
public static int Main(string[] args){
Console.WriteLine("Thread Start/Stop/Join Sample");
Thread nThread = new Thread(new ThreadStart(Thread1));
nThread.Start();
while (!nThread.IsAlive)
Thread.Sleep(1);
nThread.Abort();
nThread.Join();//Join:阻塞调用线程,直到某个线程终止或经过了指定时间为止。Join代码写在哪,哪个就是调用线程;哪个线程执行了Join方法,则这个线程为某个线程。
Console.WriteLine();
Console.WriteLine("Thread1 has finished");
try
{
Console.WriteLine("Try to restart the Thread1 thread");
nThread.Start();
}
catch (ThreadStateException)
{
Console.Write("ThreadStateException trying to restart Thread1. ");
Console.WriteLine("Expected since aborted threads cannot be restarted.");
Console.ReadLine();
}
return 0;
}
public static void Thread1()
{
while (true)
{
Console.WriteLine("Thread1 is running in its own thread.");
}
}
在Main()函数的While循环中,我们使用静态方法Thread.Sleep()让主线程停了1ms,这段时间CPU转向执行线程nThread。然后我们试图用Abort()方法终止线程nThread,后面的Join()方法使主线程等待,直到nThread线程结束。之后我们试图将nThread重新启动,但前面的Abort()方法结束线程是不可恢复的,最后程序抛出异常。
3.生产者和消费者
每个线程都有自己的资源 ,但是代码区是共享的,即每个线程都可以执行相同的函数。这可能带来的问题就是几个线程同时执行一个函数,导致数据的混乱,产生不可预料的结果,因些必须避免这种情况的发生。
(1)C#提供了关键字lock,它可以把一段代码定义为互斥段,互斥段在一个时刻内只允许一个线程进入执行,而其他线程必须等待。在C#中,关键字lock定义:lock(expression) statement_block。expression代表你希望跟踪的对象,通常是对象引用。如果你想保护一个类的实例,一般地使用this;如果你想保护一个静态变量,一般使用类名就可以了。 statement_block就是互斥段的代码,这段代码一个时刻内只可能被一个线程执行。
(2)当线程公用一个对象时,也会出现和公用代码类似的问题。这种问题就不应该使用lock关键字了,这里需要用Monitor,我们称之为监视器,Monitor提供了使线程共享资源的方案。Monitor可以锁定一个对象,一个线程只有得到这把锁才可以对该对象进行操作。Monitor必须和一个具体的对象相关联。当一个线程调用Monitor.Enter()方法锁定一个对象时,这个对象就归它所有了,其他线程想要访问这个对象,只有等待它使用Monitor.Exit()方法释放锁。
对于任何一个被Monitor锁定的对象,内存中都保存着与它相关的一些信息:
1>是现在持有锁的线程的引用;
2>是一个预备队列,队列中保存了已经准备好获取锁的线程;
3>是一个等待队列,队列中保存着当前正在等待这个对象状态改变的队列的引用。
当拥有对象锁的线程准备释放锁时,它使用Monitor.Pulse()方法通知等待队列中的第一个线程,于是该线程被转移到预备队列中,当对象锁被释放时,在预备队列中的线程可以立即获得对象锁。
下面是如何使用lock和Monitor来实现线程的同步和通讯的例子。这个例子中,生产者线程和消费者线程是交替进行的,生产者写入一个数,消费者立即读取并显示。
首先,定义一个被操作的对象的类Cell,在这个类里,有两个方法:ReadFromCell()和WriteToCell。消费者线程将调用ReadFromCell()读取cellContents的内容并且显示出来,生产者进程将调用WriteToCell()方法向cellContents写入数据。
public class Cell{
int cellContents; // Cell对象里边的内容
bool readerFlag = false; // 状态标志,为true时可以读取,为false则正在写入
public int ReadFromCell()
{
lock (this)
{
if (!readerFlag)//如果现在不可读取
{
try
{
//等待WriteToCell方法中调用Monitor.Pulse()方法
Monitor.Wait(this);
}
catch (SynchronizationLockException e)
{
Console.WriteLine(e);
}
catch (ThreadInterruptedException e)
{
Console.WriteLine(e);
}
}
Console.WriteLine("Consume: {0}", cellContents);
readerFlag = false;//重置readerFlag标志,表示消费行为已经完成
Monitor.Pulse(this);//通知WriteToCell()方法(该方法在另外一个线程中执行,等待中)
}
return cellContents;
}
public void WriteToCell(int n)
{
lock (this)
{
if (readerFlag)
{
try
{
Monitor.Wait(this);
}
catch (SynchronizationLockException e)
{
//当同步方法(指Monitor类除Enter之外的方法)在非同步的代码区被调用
Console.WriteLine(e);
}
catch (ThreadInterruptedException e)
{
//当线程在等待状态的时候中止
Console.WriteLine(e);
}
}
cellContents = n;
Console.WriteLine("Produce: {0}", cellContents);
readerFlag = true;
Monitor.Pulse(this);//通知另外一个线程中正在等待的ReadFromCell()方法
}
}
}
//下面定义生产者类 CellProd 和消费者类 CellCons ,它们都只有一个方法ThreadRun(),以便在Main()函数中提供给线程的ThreadStart代理对象,作为线程的入口。
public class CellProd
{
Cell cell; // 被操作的Cell对象
int quantity = 1; // 生产者生产次数,初始化为1
public CellProd(Cell box, int request)
{
//构造函数
cell = box;
quantity = request;
}
public void ThreadRun()
{
for (int looper = 1; looper <= quantity; looper++)
cell.WriteToCell(looper); //生产者向操作对象写入信息
}
}
public class CellCons
{
Cell cell;
int quantity = 1;
public CellCons(Cell box, int request)
{
//构造函数
cell = box;
quantity = request;
}
public void ThreadRun()
{
int valReturned;
for (int looper = 1; looper <= quantity; looper++)
valReturned = cell.ReadFromCell();//消费者从操作对象中读取信息
}
}
然后在下面这个类MonitorSample的Main()函数中,我们要做的就是创建两个线程分别作为生产者和消费者,使用CellProd.ThreadRun()方法和CellCons.ThreadRun()方法对同一个Cell对象进行操作。
{
int result = 0; //一个标志位,如果是0表示程序没有出错,如果是1表明有错误发生
Cell cell = new Cell();
//下面使用cell初始化CellProd和CellCons两个类,生产和消费次数均为20次
CellProd prod = new CellProd(cell, 20);
CellCons cons = new CellCons(cell, 20);
Thread producer = new Thread(new ThreadStart(prod.ThreadRun));
Thread consumer = new Thread(new ThreadStart(cons.ThreadRun));
//生产者线程和消费者线程都已经被创建,但是没有开始执行
try
{
producer.Start();
consumer.Start();
producer.Join();
consumer.Join();
Console.ReadLine();
}
catch (ThreadStateException e)
{
//当线程因为所处状态的原因而不能执行被请求的操作
Console.WriteLine(e);
result = 1;
}
catch (ThreadInterruptedException e)
{
//当线程在等待状态的时候中止
Console.WriteLine(e);
result = 1;
}
//尽管Main()函数没有返回值,但下面这条语句可以向父进程返回执行结果
Environment.ExitCode = result;
}
在这个例子中,同步是通过等待Monitor.Pulse()来完成的。首先生产者生产了一个值,而同一时刻消费者处于等待状态,直到收到生产者的“脉冲(Pulse)”通知它生产已经完成,此后消费者进入消费状态,而生产者开始等待消费者完成操作后将调用Monitor.Pulese()发出的“脉冲”。
4.多线程的自动管理(线程池)
在多线程的程序中,经常会出现两种情况:
(1)应用程序中,线程把大部分的时间花费在等待状态,等待某个事件发生,然后才能给予响应这一般使用ThreadPool(线程池)来解决;
(2)另一种情况:线程平时都处于休眠状态,只是周期性地被唤醒这一般使用Timer(定时器)来解决;
ThreadPool类提供一个由系统维护的线程池(可以看作一个线程的容器),该容器需要 Windows 2000 以上系统支持,因为其中某些方法调用了只有高版本的Windows才有的API函数。
将线程安放在线程池里,需使用ThreadPool.QueueUserWorkItem()方法,该方法的原型如下:
//将一个线程放进线程池,该线程的Start()方法将调用WaitCallback代理对象代表的函数
public static bool QueueUserWorkItem(WaitCallback);
//重载的方法如下,参数object将传递给WaitCallback所代表的方法
public static bool QueueUserWorkItem(WaitCallback, object);
ThreadPool类是一个静态类,你不能也不必要生成它的对象。而且一旦使用该方法在线程池中添加了一个项目,那么该项目将是无法取消的。
在这里你无需自己建立线程,只需把你要做的工作写成函数,然后作为参数传递给ThreadPool.QueueUserWorkItem()方法就行了,传递的方法就是依靠WaitCallback代理对象,而线程的建立、管理、运行等工作都是由系统自动完成的,你无须考虑那些复杂的细节问题。
如果应用程序需要对线程进行特定的控制,则不适合使用线程池,需要创建并管理自己的线程。不适合使用线程池的情形包括:
— 如果需要使一个任务具有特定的优先级。
— 如果具有可能会长时间运行(并因此阻塞其他任务)的任务。
— 如果需要将线程放置到单线程单元中(线程池中的线程均处于多线程单元中)。
— 如果需要用永久标识来标识和控制线程,比如想使用专用线程来中止该线程,将其挂起或按名称发现它。
用法:
首先程序创建了一个ManualResetEvent对象,该对象就像一个信号灯,可以利用它的信号来通知其它线程。例1中,当线程池中所有线程工作都完成以后,ManualResetEvent对象将被设置为有信号,从而通知主线程继续运行。
ManualResetEvent对象有几个重要的方法:
初始化该对象时,用户可以指定其默认的状态(有信号/无信号);在初始化以后,该对象将保持原来的状态不变,直到它的Reset()或者Set()方法被调用:
Reset()方法:将其设置为无信号状态;
Set()方法:将其设置为有信号状态。
WaitOne()方法:使当前线程挂起,直到ManualResetEvent对象处于有信号状态,此时该线程将被激活。
然后,程序将向线程池中添加工作项,这些以函数形式提供的工作项被系统用来初始化自动建立的线程。当所有的线程都运行完了以后,ManualResetEvent.Set()方法被调用,因为调用了ManualResetEvent.WaitOne()方法而处在等待状态的主线程将接收到这个信号,于是它接着往下执行,完成后边的工作。
例1:
public static int Main(string[] args){
Console.WriteLine("Thread Pool Sample:");
bool W2K = false;
int MaxCount = 10;//允许线程池中运行最多10个线程
//新建ManualResetEvent对象并且初始化为无信号状态
ManualResetEvent eventX = new ManualResetEvent(false);
Console.WriteLine("Queuing {0} items to Thread Pool", MaxCount);
Alpha oAlpha = new Alpha(MaxCount);
//创建工作项
//注意初始化oAlpha对象的eventX属性
oAlpha.eventX = eventX;
Console.WriteLine("Queue to Thread Pool 0");
try
{
//将工作项装入线程池
//这里要用到Windows 2000以上版本才有的API,所以可能出现NotSupportException异常
ThreadPool.QueueUserWorkItem(new WaitCallback(oAlpha.Beta), new SomeState(0));
W2K = true;
}
catch (NotSupportedException)
{
Console.WriteLine("These API's may fail when called on a non-Windows 2000 system.");
W2K = false;
}
if (W2K)//如果当前系统支持ThreadPool的方法.
{
for (int iItem = 1; iItem < MaxCount; iItem++)
{
//插入队列元素
Console.WriteLine("Queue to Thread Pool {0}", iItem);
ThreadPool.QueueUserWorkItem(new WaitCallback(oAlpha.Beta), new SomeState(iItem));
}
Console.WriteLine("Waiting for Thread Pool to drain");
//等待事件的完成,即线程调用ManualResetEvent.Set()方法
eventX.WaitOne(Timeout.Infinite, true);
//WaitOne()方法使调用它的线程等待直到eventX.Set()方法被调用
Console.WriteLine("Thread Pool has been drained (Event fired)");
Console.WriteLine();
Console.WriteLine("Load across threads");
foreach (object o in oAlpha.HashCount.Keys)
Console.WriteLine("{0} {1}", o, oAlpha.HashCount[o]);
}
Console.ReadLine();
return 0;
}
//这是用来保存信息的数据结构,SomeState类是一个保存信息的数据结构,它在程序中作为参数被传递给每一个线程,因为你需要把一些有用的信息封装起来提供给线程,而这种方式是非常有效的。public class SomeState
{
public int Cookie;
public SomeState(int iCookie)
{
Cookie = iCookie;
}
}
public class Alpha
{
public Hashtable HashCount;
public ManualResetEvent eventX;
public static int iCount = 0;
public static int iMaxCount = 0;
public Alpha(int MaxCount)
{
HashCount = new Hashtable(MaxCount);
iMaxCount = MaxCount;
}
//线程池里的线程将调用Beta()方法
public void Beta(Object state)
{
//输出当前线程的hash编码值和Cookie的值
Console.WriteLine(" {0} {1} :", Thread.CurrentThread.GetHashCode(), ((SomeState)state).Cookie);
Console.WriteLine("HashCount.Count=={0}, Thread.CurrentThread.GetHashCode()=={1}", HashCount.Count, Thread.CurrentThread.GetHashCode());
lock (HashCount)
{
//如果当前的Hash表中没有当前线程的Hash值,则添加之
if (!HashCount.ContainsKey(Thread.CurrentThread.GetHashCode()))
HashCount.Add(Thread.CurrentThread.GetHashCode(), 0);
HashCount[Thread.CurrentThread.GetHashCode()] =
((int)HashCount[Thread.CurrentThread.GetHashCode()]) + 1;
}
int iX = 2000;
Thread.Sleep(iX);
//Interlocked.Increment()操作是一个原子操作,InterLocked类也是专为多线程程序而存在的,它提供了一些有用的原子操作。原子操作:就是在多线程程序中,如果这个线程调用这个操作修改一个变量,那么其他线程就不能修改这个变量了,这跟lock关键字在本质上是一样的。
Interlocked.Increment(ref iCount);
if (iCount == iMaxCount)
{
Console.WriteLine();
Console.WriteLine("Setting eventX ");
eventX.Set();
}
}
}
例2:
// 存放要计算的数值的字段static double number1 = -1;
static double number2 = -1;
public static void Main()
{
// 获取线程池的最大线程数和维护的最小空闲线程数
int maxThreadNum, portThreadNum;
int minThreadNum;
ThreadPool.GetMaxThreads(out maxThreadNum, out portThreadNum);
ThreadPool.GetMinThreads(out minThreadNum, out portThreadNum);
Console.WriteLine("最大线程数:{0}", maxThreadNum);
Console.WriteLine("最小空闲线程数:{0}", minThreadNum);
// 函数变量值
int x = 256;
// 启动第一个任务:计算x的8次方
Console.WriteLine("启动第一个任务:计算{0}的8次方。", x);
ThreadPool.QueueUserWorkItem(new WaitCallback(TaskProc1), x);
// 启动第二个任务:计算x的8次方根
Console.WriteLine("启动第二个任务:计算{0}的8次方根。", x);
ThreadPool.QueueUserWorkItem(new WaitCallback(TaskProc2), x);
// 等待,直到两个数值都完成计算
while (number1 == -1 || number2 == -1) ;
// 打印计算结果
Console.WriteLine("{0}的8次方 = {1}", x, number1);
Console.WriteLine("{0}的8次方根 = {1}", x, number2);
Console.ReadLine();
}
// 启动第一个任务:计算x的8次方
static void TaskProc1(object o)
{
number1 = Math.Pow(Convert.ToDouble(o), 8);
}
// 启动第二个任务:计算x的8次方根
static void TaskProc2(object o)
{
number2 = Math.Pow(Convert.ToDouble(o), 1.0 / 8.0);
}
5.多线程自动管理(定时器)
Timer类:设置一个定时器,定时执行用户指定的函数。
定时器启动后,系统将自动建立一个新的线程,执行用户指定的函数。
初始化一个Timer对象:
Timer timer = new Timer(timerDelegate, s,1000, 1000);
第一个参数:指定了TimerCallback 委托,表示要执行的方法;
第二个参数:一个包含回调方法要使用的信息的对象,或者为空引用;
第三个参数:延迟时间——计时开始的时刻距现在的时间,单位是毫秒,指定为“0”表示立即启动计时器;
第四个参数:定时器的时间间隔——计时开始以后,每隔这么长的一段时间,TimerCallback所代表的方法将被调用一次,单位也是毫秒。指定 Timeout.Infinite 可以禁用定期终止。
Timer.Change()方法:修改定时器的设置。
class TimerExampleState{
public int counter = 0;
public Timer tmr;
}
public static void Main()
{
TimerExampleState s = new TimerExampleState();
//创建代理对象TimerCallback,该代理将被定时调用
TimerCallback timerDelegate = new TimerCallback(CheckStatus);
//创建一个时间间隔为1s的定时器
Timer timer = new Timer(timerDelegate, s, 1000, 1000);
s.tmr = timer;
//主线程停下来等待Timer对象的终止
while (s.tmr != null)
Thread.Sleep(0);
Console.WriteLine("Timer example done.");
Console.ReadLine();
}
//下面是被定时调用的方法
static void CheckStatus(Object state)
{
TimerExampleState s = (TimerExampleState)state;
s.counter++;
Console.WriteLine("{0} Checking Status {1}.", DateTime.Now.TimeOfDay, s.counter);
if (s.counter == 5)
{
//使用Change方法改变了时间间隔
(s.tmr).Change(10000, 2000);
Console.WriteLine("changed");
}
if (s.counter == 10)
{
Console.WriteLine("disposing of timer");
s.tmr.Dispose();
s.tmr = null;
}
}
程序首先创建了一个定时器,它将在创建1秒之后开始每隔1秒调用一次CheckStatus()方法,当调用5次以后,在CheckStatus()方法中修改了时间间隔为2秒,并且指定在10秒后重新开始。当计数达到10次,调用Timer.Dispose()方法删除了timer对象,主线程于是跳出循环,终止程序。
6.互斥对象
如何控制好多个线程相互之间的联系,不产生冲突和重复,这需要用到互斥对象,即:System.Threading 命名空间中的 Mutex 类。
我们可以把Mutex看作一个出租车,乘客看作线程。乘客首先等车,然后上车,最后下车。当一个乘客在车上时,其他乘客就只有等他下车以后才可以上车。而线程与Mutex对象的关系也正是如此,线程使用Mutex.WaitOne()方法等待Mutex对象被释放,如果它等待的Mutex对象被释放了,它就自动拥有这个对象,直到它调用Mutex.ReleaseMutex()方法释放这个对象,而在此期间,其他想要获取这个Mutex对象的线程都只有等待。
下面这个例子使用了Mutex对象来同步四个线程,主线程等待四个线程的结束,而这四个线程的运行又是与两个Mutex对象相关联的。
其中还用到AutoResetEvent类的对象,可以把它理解为一个信号灯。这里用它的有信号状态来表示一个线程的结束。
// AutoResetEvent.Set()方法设置它为有信号状态
// AutoResetEvent.Reset()方法设置它为无信号状态
例:
using System;using System.Threading;
namespace ThreadExample
{
public class MutexSample
{
static Mutex gM1;
static Mutex gM2;
const int ITERS = 100;
static AutoResetEvent Event1 = new AutoResetEvent(false);
static AutoResetEvent Event2 = new AutoResetEvent(false);
static AutoResetEvent Event3 = new AutoResetEvent(false);
static AutoResetEvent Event4 = new AutoResetEvent(false);
public static void Main(String[] args)
{
Console.WriteLine("Mutex Sample ");
//创建一个Mutex对象,并且命名为MyMutex
gM1 = new Mutex(true, "MyMutex");
//创建一个未命名的Mutex 对象.
gM2 = new Mutex(true);
Console.WriteLine(" - Main Owns gM1 and gM2");
AutoResetEvent[] evs = new AutoResetEvent[4];
evs[0] = Event1; //为后面的线程t1,t2,t3,t4定义AutoResetEvent对象
evs[1] = Event2;
evs[2] = Event3;
evs[3] = Event4;
MutexSample tm = new MutexSample();
Thread t1 = new Thread(new ThreadStart(tm.t1Start));
Thread t2 = new Thread(new ThreadStart(tm.t2Start));
Thread t3 = new Thread(new ThreadStart(tm.t3Start));
Thread t4 = new Thread(new ThreadStart(tm.t4Start));
t1.Start();// 使用Mutex.WaitAll()方法等待一个Mutex数组中的对象全部被释放
t2.Start();// 使用Mutex.WaitOne()方法等待gM1的释放
t3.Start();// 使用Mutex.WaitAny()方法等待一个Mutex数组中任意一个对象被释放
t4.Start();// 使用Mutex.WaitOne()方法等待gM2的释放
Thread.Sleep(2000);
Console.WriteLine(" - Main releases gM1");
gM1.ReleaseMutex(); //线程t2,t3结束条件满足
Thread.Sleep(1000);
Console.WriteLine(" - Main releases gM2");
gM2.ReleaseMutex(); //线程t1,t4结束条件满足
//等待所有四个线程结束
WaitHandle.WaitAll(evs);
Console.WriteLine(" Mutex Sample");
Console.ReadLine();
}
public void t1Start()
{
Console.WriteLine("t1Start started, Mutex.WaitAll(Mutex[])");
Mutex[] gMs = new Mutex[2];
gMs[0] = gM1;//创建一个Mutex数组作为Mutex.WaitAll()方法的参数
gMs[1] = gM2;
Mutex.WaitAll(gMs);//等待gM1和gM2都被释放
Thread.Sleep(2000);
Console.WriteLine("t1Start finished, Mutex.WaitAll(Mutex[]) satisfied");
Event1.Set(); //线程结束,将Event1设置为有信号状态
gM1.ReleaseMutex();
gM2.ReleaseMutex();
}
public void t2Start()
{
Console.WriteLine("t2Start started, gM1.WaitOne( )");
gM1.WaitOne();//等待gM1的释放
Console.WriteLine("t2Start finished, gM1.WaitOne( ) satisfied");
Event2.Set();//线程结束,将Event2设置为有信号状态
gM1.ReleaseMutex();
}
public void t3Start()
{
Console.WriteLine("t3Start started, Mutex.WaitAny(Mutex[])");
Mutex[] gMs = new Mutex[2];
gMs[0] = gM1;//创建一个Mutex数组作为Mutex.WaitAny()方法的参数
gMs[1] = gM2;
int i=Mutex.WaitAny(gMs);//等待数组中任意一个Mutex对象被释放
Console.WriteLine("t3Start finished, Mutex.WaitAny(Mutex[])");
Event3.Set();//线程结束,将Event3设置为有信号状态
gMs[i].ReleaseMutex();
}
public void t4Start()
{
Console.WriteLine("t4Start started, gM2.WaitOne( )");
gM2.WaitOne();//等待gM2被释放
Console.WriteLine("t4Start finished, gM2.WaitOne( )");
Event4.Set();//线程结束,将Event4设置为有信号状态
}
}
}
参考:http://kb.cnblogs.com/page/42528/
作者:sy
出处:http://www.cnblogs.com/sydeveloper
QQ:522733724
本页版权归作者和博客园所有,欢迎转载,但未经作者同意必须保留此段声明,
且在文章页面明显位置给出原文链接,否则保留追究法律责任的权利