个人对委托与线程的见解(下)

线程相关

        关于线程的概念很多,简单的说,线程是程序执行流的最小单元,如果把进程比作一条河流,那么线程就是河流的一条小支流。他是独立执行,却能对主进程有影响。

常识

1. 前台线程和后台线程:通过Thread类新建线程 thread1 默认为前台线程。当所有前台线程关闭时,所有的后台线程也会被直接终止,不会抛出异常。将前台线程转后台线程,只需thread1.IsBackground = true
2. 挂起(Suspend)和唤醒(Resume):由于线程的执行顺序和程序的执行情况不可预知,所以使用挂起和唤醒容易发生死锁的情况,在实际应用中应该尽量少用。
3. 阻塞线程:Join,阻塞调用线程,直到该线程终止。
4. 终止线程:Abort:抛出 ThreadAbortException 异常让线程终止,终止后的线程不可唤醒。Interrupt:抛出 ThreadInterruptException 异常让线程终止,通过捕获异常可以继续执行。
5. 线程优先级:AboveNormal BelowNormal Highest Lowest Normal,默认为Normal。


单线程与多线程 

        单线程就是,任务一个一个地做,必须做完一个任务后,再去做另一个任务。多线程就是一会做这个任务,一会做那个任务,每个任务做一会,不停的切换。显然,最后把所有的任务做完,多线程必定比单线程更耗费时间。为什么?因为,多线程要在不同的任务之间切换,切换肯定是要耗费时间的。那么问题来了,既然多线程比单线程更耗费时间,为什么还要多线程? 单线程有一个致命的问题,就是一个线程运行的整个过程中,其他线程必须等待,不能响应用户的命令,用户体验太差,好像电脑死机一样。



线程池:线程池线程默认为后台线程(IsBackground)

由于线程的创建和销毁需要耗费一定的开销,过多的使用线程会造成内存资源的浪费,出于对性能的考虑,于是引入了线程池的概念。线程池维护一个请求队列,线程池的代码从队列提取任务,然后委派给线程池的一个线程执行,线程执行完不会被立即销毁,这样既可以在后台执行任务,又可以减少线程创建和销毁所带来的开销。


同步与异步

         同步的使用场景:多个线程同时访问一块数据,也叫共享区。对于多个线程同时访问一块数据的时候,必须使用同步,否则可能会出现不安全的情况。比如数据库中的脏读。但是,多个线程同时访问一块数据,有一种情况不需要同步技术,那就是原子操作,也就是说操作系统在底层保证了操作要么全部做完,要么不做。

        异步的使用场景:只有一个线程访问当前的数据。比如,观察者模式,没有共享区,主题发生变化,通知观察者更新,主题继续做自己的事情,不需要等待观察者更新完成后再工作。

创建线程实例

创建一个最简单的单线程:
 
 
//实例化线程
new System.Threading.Thread(Function).Start();

private void Function()
{
     MessageBox.Show("线程");
}

这种线程调用方式,仅适用于无参方法调用,同时,可以限制线程使用的堆栈大小,只需在Thread(Function,int)增加字节为单位的整形参数。同时,这种方式,不需要去关闭线程,垃圾回收机制会将其自动回收,可好比这就是一个参数对象。

实现同样效果还可以这样创建线程:

Thread thread1 = new Thread(Function);
thread1.Start();

但是这个需要注意的是,一般在线程不用时,需要用线程的Abort()方法结束线程。

此外,还可以这样写

//利用委托
Thread thread2 = new Thread(delegate () { MessageBox.Show("线程委托"); });
thread2.Start();

//Lambda表达式的形式
Thread thread3 = new Thread(() => { MessageBox.Show("Lambda表达式"); });
thread3.Start();

本着学习的态度来找知识的话,现在应该会提出,如果要传参数,那应该怎么写呢,下面就说一下创建有参数的线程方法。

线程传参

首先,最简单的,就是利用线程的公共API传参(也就是Start()方法)

/// <summary>
/// 调用方法
/// </summary>
/// <param name="e">参数对象</param>
private void Function(object e)
{
     MessageBox.Show(e.ToString());  
}

//调用
Thread thread = new Thread(Function);
thread.Start("线程传参");

这种传参方式有时可能不太实用(参数只有一个),那么我们还可以自己定义一个类对象,来实现传参

public class ThreadEntry
{
	private int intPara;
	private string strPara;
	public ThreadEntry(int intPara, string strPara)
	{
		this.intPara = intPara;
		this.strPara = strPara;
	}

	/// <summary>
	/// 调用方法
	/// </summary>
	public void Method()
	{
		MessageBox.Show(string.Format(strPara, intPara));
	}
}

//调用
Thread thread = new Thread(new ThreadEntry(1,"第{0}种类传参").Method);
thread.Start();

除此之外,还可以提前定义一个线程的抽象类,来实现参数传递方法,这样调用起来更方便。尽管两种方法的可拓展性都挺高的,但是用抽象类的办法,还是要好一点,因为在实例化对象时,会少一个层级。

首先,我们需要定义一个自己的线程抽象类

abstract class MyThread
{
	Thread thread = null;

	abstract public void run();

	public void start()
	{
		if (thread == null)
			thread = new Thread(run);
		thread.Start();
	}
}

之后再定义一个它的子类

class ParaThread : MyThread
{
	private int intPara;
	private string strPara;
	public ParaThread(int intPara, string strPara)
	{
		this.intPara = intPara;
		this.strPara = strPara;
	}

	override public void run()
	{
		MessageBox.Show(string.Format(strPara, intPara));
	}
} 

这里只用了两个参数的构造函数,所以只能传两个参数,如果说你想传更多的参数,只需要对构造函数重写就行了,之前的对象类,需要更多的传参,也是重写就行了。

核心东西写完后,简单调用一下就好了

ParaThread thread = new ParaThread(2, "第{0}种类传参");
thread.start();

线程传参方式很多,这里只是对常用的进行了举例。


异步线程

 对于异步线程,前面已做过简要的讲解,这里便举一个实例,供参考学习。后面还有一个task任务也是属于异步线程的范畴。

private Control _control;
private Thread beginInvokeThread;
public delegate void beginInvokeDelegate();
public event beginInvokeDelegate Run;//委托事件

/// <summary>
/// 执行事件
/// </summary>
/// <returns>返回执行结果:true</returns>
private bool Do()
{
	if (this.Run != null)
		RaiseEvent(Run);
	return true;
}

/// <summary>
/// 事件处理函数
/// </summary>
/// <param name="handler">处理</param>
private void RaiseEvent(beginInvokeDelegate handler)
{
	if (handler != null)
	{
		beginInvokeDelegate beginInvoke = handler;
		beginInvoke();
	}
}

/// <summary>
/// 异步线程入口
/// </summary>
/// <param name="control">作用控件</param>
/// <param name="run">执行函数</param>
public void DoBeginInvoke(Control control, beginInvokeDelegate run)
{
	Run = run;
	_control = control;
	//开启异步线程
	System.Threading.ThreadStart s = new System.Threading.ThreadStart(new System.Threading.ThreadStart(Result));
	beginInvokeThread = new System.Threading.Thread(s);
	beginInvokeThread.Name = "异步线程";
	beginInvokeThread.Start();
}

/// <summary>
/// 中断执行
/// </summary>
private void EndProcess()
{
	if (beginInvokeThread.IsAlive) beginInvokeThread.Abort();//结束线程
}

/// <summary>
/// 执行结果处理
/// </summary>
private void Result()
{
	bool ok = Do();
	_control.BeginInvoke(new System.Threading.ThreadStart(delegate()
	{
		if (ok)
		{
			EndProcess();
		}
	}));
}

这是一个异步线程类,结构比较清晰,很容易就能明白,只要知道了其中的原理,其实可以写的更加简化。这里需要注意的是,这是一个UI异步线程,如果在异步线程还未执行完成时,强制关掉了窗体,可能会触发异常,所以关闭窗体时调用EndProcess()方法,也可尝试将线处理为后台线程,即IsBackground 设为true。

同步线程

同步与异步在程序上的差别不是很大,基本上就是将上面的异步线程实例中的BeginInvoke更换成Invoke便可以实现同步线程的效果。

多线程

线程池[ThreadPool]

这是一种相对较简单的方法,适应于一些需要多个线程而又较短任务(如一些常处于阻塞状态的线程) ,明显缺点就是对创建的线程不能加以控制及设置其优先级。由于每个进程只有一个线程池,所以ThreadPool类的成员函数都为static。

核心函数介绍:

//调用成功则返回true,它的另一个重载函数类似,只是委托不带参数而已
public static bool QueueUserWorkItem( 
    WaitCallback callBack,//要创建的线程调用的委托
    object state //传递给委托的参数
);
public static RegisteredWaitHandle RegisterWaitForSingleObject(
   WaitHandle waitObject,// 要注册的 WaitHandle
   WaitOrTimerCallback callBack,// 线程调用的委托
   object state,//传递给委托的参数
   int TimeOut,//超时,单位为毫秒,
   bool executeOnlyOnce file://是否只执行一次
);
public delegate void WaitOrTimerCallback(
   object state,//也即传递给委托的参数
   bool timedOut//true表示由于超时调用,反之则因为waitObject
);

使用实例:

首先定义一个多线程操作类

public class Multithreading
{
	public static int iCount = 0;
	public static int iMaxCount = 0;
	public ManualResetEvent rEvent;
	
	public Multithreading(int iMaxCount, ManualResetEvent rEvent)
	{
		this.iMaxCount = count;
		this.rEvent = rEvent;
	}

	public void DoProcess(object i)
	{
		Console.WriteLine("Thread操作[" + i.ToString() + "]");
		Thread.Sleep(1000);
		//Interlocked.Increment()操作是一个原子操作,作用是:iCount++ 具体请看下面说明 
		//原子操作,就是不能被更高等级中断抢夺优先的操作。你既然提这个问题,我就说深一点。
		//由于操作系统大部分时间处于开中断状态,
		//所以,一个程序在执行的时候可能被优先级更高的线程中断。
		//而有些操作是不能被中断的,不然会出现无法还原的后果,这时候,这些操作就需要原子操作。
		//就是不能被中断的操作。
		Interlocked.Increment(ref iCount);
		if (iCount == iMaxCount)
		{
			Console.WriteLine("发出结束信号!");
			//将事件状态设置为终止状态,允许一个或多个等待线程继续。
			eventX.Set();
		}
	}
}

有了这个操作类,我们就可以将想要传的参数通过操作类的构造函数传递过去,在方法中使用。有了操作类,接下来就可以创建线程池,实现简单的多线程处理效果了。

//新建ManualResetEvent对象并且初始化为无信号状态
ManualResetEvent rEvent = new ManualResetEvent(false);
ThreadPool.SetMaxThreads(3, 3);
int executeCount = 10;
Multithreading t = new Multithreading(executeCount, rEvent);
for (int i = 0; i < executeCount; i++)
{
	ThreadPool.QueueUserWorkItem(new WaitCallback(t.DoProcess), i);
}

//WaitOne  阻止当前线程,直到当前 WaitHandle 收到信号为止。 
rEvent.WaitOne(Timeout.Infinite, true);

task

相对于线程池,task的可控制性就更高了,值得注意的是,task线程是异步执行的,也就是说task任务从作用上说是异步线程,从模式上说是属于多线程。这里只对task的用法做一个简单的介绍,想要深入理解,可以去百度一下专门讲解task的文章。

调用方法

private static void StartTask(object e)
{
	Console.WriteLine("执行Task任务【{0}】", e);
	Thread.Sleep(1000);
}

创建task任务

Console.WriteLine("创建Task任务");
new Task(StartCode, 1).Start();
Console.WriteLine("创建Task任务完成");
Thread.Sleep(1000);
task任务可以用Cancel()方法取消,但是这是个异步请求,Task可能已经完成了。

再举一个计算的实例

简单的计算方法

private static Int Sum(Int i)
{
	Int sum = 0;
	for (; i > 0; i--)
		checked { sum += i; }
	return sum;
}

task任务创建与使用

Task<Int> t = new Task<Int>(i => Sum((Int)i), 10000000);
t.Start();
//Wait显式的等待一个线程完成
t.Wait();

Console.WriteLine("计算结果:" + t.Result);

结果输出还可以这么写

Task cwt =  t.ContinueWith(task=>Console.WriteLine("计算结果:{0}",task.Result));
cwt.Wait();

task创建调用的其他写法

//using task factory
TaskFactory tf = new TaskFactory();
Task t1 = tf.StartNew(TaskMethod);

//using the task factory via a task
Task t2 = Task.TaskFactory.StartNew(TaskMethod);

补充(Timer)

适用于需周期性调用的方法,它不在创建计时器的线程中运行,它在由系统自动分配的单独线程中运行。这和Win32中的SetTimer方法类似。它的构造为:

public Timer(
   TimerCallback callback,//所需调用的方法
   object state,//传递给callback的参数
   int dueTime,//多久后开始调用callback
   int period//调用此方法的时间间隔
); 
// 如果 dueTime 为0,则 callback 立即执行它的首次调用。
// 如果 dueTime 为 Infinite,则 callback 不调用它的方法,计时器被禁用。
// 但使用 Change 方法可以重新启用它。 
// 如果period 为0或 Infinite,并且 dueTime 不为 Infinite,则 callback 调用它的方法一次。计时器的定期行为被禁用
// 但使用 Change 方法可以重新启用它。
// 如果 period 为零0或 Infinite,并且 dueTime 不为 Infinite,则 callback 调用它的方法一次。计时器的定期行为被禁用。
// 但使用 Change 方法可以重新启用它。

改变它的period和dueTime,我们可以通过调用Timer的Change方法来改变:

public bool Change(
   int dueTime,
   int period
);

调用写法

Timer tm=new Timer (new TimerCallback (CallBack),"Test",2000,2000);
tm.Change (0,500);
以上内容皆属个人对线程的理解,不一定正确,供参考学习,如有问题,欢迎指出交流。


猜你喜欢

转载自blog.csdn.net/qwerdfcv/article/details/80292840