多线程的使⽤(Thread)
上篇
中篇多线程的使⽤(Thread)
下篇 net 任务⼯⼚实现异步多线程
Thread多线程概述
上⼀篇我们介绍了net 的同步与异步,我们异步演⽰的时候使⽤的是委托多线程来实现的。今天我们来细细的剖析下多线程。
多线程的优点:可以同时完成多个任务;可以使程序的响应速度更快;可以让占⽤⼤量处理时间的任务或当前没有进⾏处理的任务定期将处理时间让给别的任务;可以随时停⽌任务;可以设置每个任务的优先级以优化程序性能。
然⽽,多线程虽然有很多优点,但是也必须认识到多线程可能存在影响系统性能的不利⽅⾯,才能正确使⽤线程。弊端主要有如下⼏点:
(1)线程也是程序,所以线程需要占⽤内存,线程越多,占⽤内存也越多。
(2)多线程需要协调和管理,所以需要占⽤CPU时间以便跟踪线程[时间空间转换,简称时空转换]。
(3)线程之间对共享资源的访问会相互影响,必须解决争⽤共享资源的问题。
(4)线程太多会导致控制太复杂,最终可能造成很多程序缺陷。
当启动⼀个可执⾏程序时,将创建⼀个主线程。在默认的情况下,C#程序具有⼀个线程,此线程执⾏程序中以Main⽅法开始和结束的代码,Main()⽅法直接或间接执⾏的每⼀个命令都有默认线程(主线程)执⾏,当Main()⽅法返回时此线程也将终⽌。
⼀个进程可以创建⼀个或多个线程以执⾏与该进程关联的部分程序代码。在C#中,线程是使⽤Thread类处理的,该类在System.Threading命名空间中。使⽤Thread类创建线程时,只需要提供线程⼊⼝,线程⼊⼝告诉程序让这个线程做什么。通过实例化⼀个Thread类的对象就可以创建⼀个线程。
多线程Thread
上⼀节我们通过了线程的异步了解了线程的等待和线程回调的区别、学习了不卡主线程的原理。这⾥我们简单学习下 Thread 类的使⽤.我们分别让主线程和⼦线程循环输出 1-100,我们来看下结果。
Thread类接收⼀个ThreadStart委托或ParameterizedThreadStart委托的构造函数,该委托包装了调⽤Start⽅法时由新线程调⽤的⽅法,⽰例代码如下:
Thread thread=new Thread(new ThreadStart(method));//创建线程
thread.Start();                                                          //启动线程
1.线程的⽆序性质
Thread t = new Thread(()=> {
for (int i = 0; i < 100; i++)
{
Console.WriteLine($"我是⼦线程({i})~~~~");
}
});// 参数 ThreadStart 是⼀个委托类型的。凡是委托,我们都可以使⽤lambda 表达式代替
t.Start();//Start 通过Start开启⼀个线程
for (int i = 0; i < 100; i++)
{
Console.WriteLine($"我是主线程【{i}】");
}
输出结果:
通过执⾏结果我们会看到,主线程和⼦线程不是⼀味的执⾏,是兼续的。也就是说主线程和⼦线程在执⾏过程中是互相抢CPU资源进⾏计算的。
这⾥我们可以总结下,异步多线程三⼤特点:
(1)同步卡界⾯,UI线程被占⽤;异步多线程不卡界⾯,UI线程空闲,计算任务交给⼦线程。
(2)同步⽅法慢,因为只有⼀个线程⼲活;异步多线程⽅法快,因为多个线程并发计算(空间换时间), 这⾥也会消耗更多的资源,不是线程的线性关系(倍数关系),不是线程越多越好(1资源有限 2线程调度耗资源 3不稳定)
(3)※异步多线程是⽆序的,不可预测的:启动顺序不确定、消耗时间不确定、结束顺序不确定(这⾥特别提醒下,我们不要试图控制执⾏的顺序)
异步与多线程的区别: 异步是主线程⽴即启动,启动完成以后,在执⾏⼦线程。但是多线程不⼀定是谁先启动。如果看不懂,请看本⼈。
关于线程的⽆续,这⾥就不过多的解释了继续往下看。
2.前台线程与后台线程
在上⼀篇⽂章我们简单的说到了,线程本⾝并不是任何⾼级语⾔的概念,本⾝是计算机的概念,只是⾼级语⾔给予封装了⼀层。
前台线程:窗体Ui主线程退出(销毁)以后,⼦线程必须计算完成才能退出。请下载demo ⾃⾏查看,还是上⾯的案列。
后台线程:窗体Ui主线程退出(销毁)以后,⼦线程就会退出。关闭窗体以后,控制台就退出了。我们通过Thread 类的 IsBackground属性进⾏设置;设置为true 的时候为后台线程,默认为false前台线程。看下边案列
private void button1_Click(object sender, EventArgs e)
{
Thread t = new Thread(()=> {
for (int i = 0; i < 100; i++)
{
Thread.Sleep(100);//演⽰前台线程和后台线程,演⽰线程⽆序性质,请注释掉
Console.WriteLine($"我是⼦线程({i})~~~~");
}
});// 参数 ThreadStart 是⼀个委托类型的。凡是委托,我们都可以使⽤lambda 表达式代替
t.IsBackground=true;//设置为true 的时候为后台线程,默认为false前台线程。关闭窗体会⽴即停⽌计算。
t.Start();//Start 通过Start开启⼀个线程
for (int i = 0; i < 100; i++)
{
Console.WriteLine($"我是主线程【{i}】");
}
}
后台线程
后台线程⼀般⽤于处理不重要的事情,应⽤程序结束时,后台线程是否执⾏完成对整个应⽤程序没有影响。如果要执⾏的事情很重要,需要将线程设置为前台线程。
3.线程的状态及属性⽅法的使⽤
这⾥简单列下线程的常⽤属性
属性名称说明
CurrentContext获取线程正在其中执⾏的当前上下⽂。
CurrentThread获取当前正在运⾏的线程。
ExecutionContext获取⼀个 ExecutionContext 对象,该对象包含有关当前线程的各种上下⽂的信息。
IsAlive获取⼀个值,该值指⽰当前线程的执⾏状态。
IsBackground获取或设置⼀个值,该值指⽰某个线程是否为后台线程。
IsThreadPoolThread获取⼀个值,该值指⽰线程是否属于托管线程池。
ManagedThreadId获取当前托管线程的唯⼀标识符。
Name获取或设置线程的名称。
Priority获取或设置⼀个值,该值指⽰线程的调度优先级。
ThreadState获取⼀个值,该值包含当前线程的状态。
通过ThreadState可以检测线程是处于Unstarted、Sleeping、Running 等等状态,它⽐ IsAlive 属性能提供更多的特定信息。
前⾯说过,⼀个应⽤程序域中可能包括多个上下⽂,⽽通过CurrentContext可以获取线程当前的上下⽂。
CurrentThread是最常⽤的⼀个属性,它是⽤于获取当前运⾏的线程。
Thread.CurrentThread.ManagedThreadId 获取线程的ID ,我们尽量不要使⽤  Thread.CurrentThread.Name,因为name 并不是唯⼀的
线程状态值
Thread 中包括了多个⽅法来控制线程的创建、挂起、停⽌、销毁,以后来的例⼦中会经常使⽤。
⽅法名称说明
Abort()    终⽌本线程。
GetDomain()返回当前线程正在其中运⾏的当前域。
GetDomainId()返回当前线程正在其中运⾏的当前域Id。
Interrupt()中断处于 WaitSleepJoin 线程状态的线程。
Join()已重载。阻塞调⽤线程,直到某个线程终⽌时为⽌。
Resume()继续运⾏已挂起的线程。
Start()  执⾏本线程。
Suspend()挂起当前线程,如果当前线程已属于挂起状态则此不起作
Sleep()  把正在运⾏的线程挂起⼀段时间。
关于线程状态操作的⽅法,我们尽量不要使⽤,这⾥以销毁线程的⽅法为列⼦去演⽰。本图表除了第⼀个以外,都可以使⽤。
注意:
在我们net 中,语⾔分为托管代码和⾮托管代码。托管代码是可以控制的,⾮托管代码是不可控制的.我们线程销毁实际上是抛出了⼀个异常,当我们程序捕获到这个异常以后,线程因为异常⽽退出。所以销毁线程在某些(值⾮托管调⽤)时候会出现问题的,我们后续在任务⼯⼚ FacTask 的时候,再去详细讲解销毁线程。如果⾮要"停⽌线程靠的不是外部⼒量⽽是线程⾃⾝,外部修改信号量,线程检测信号量,当线程检测到信号量以后,我们抛出异常,在主线程中捕获异常即可".
4.线程等待
我们在学习异步的时候,知道UI主线程在等待⼦线程的时候,窗体是不可以移动的。并且我们可以有实时返回(有损耗)的等待(阻塞)和⼀直阻塞主线程(⽆损耗)等待⼦线程完事,再去执⾏主线程。下⾯我们来看看⼀个案列,案列以5名学⽣写作业为例⼦。
public void WriteJob(string name) {
Console.WriteLine("********************** "+ name + " Start【" + Thread.CurrentThread.ManagedThreadId + "】等待............... ***");
Stopwatch watch = new Stopwatch();
watch.Start();
for (int i = 0; i < 10; i++)
{
Thread.Sleep(32);
Console.WriteLine($"学⽣:{name}在做第{i+1}题作业");
}
watch.Stop();
Console.WriteLine("********************** " + name + " End【" + Thread.CurrentThread.ManagedThreadId + "】⽤时"+ watch.ElapsedMilliseconds + "毫秒............... ***");
}
///<summary>
///线程等待
///</summary>
///<param name="sender"></param>
///<param name="e"></param>
private void button2_Click(object sender, EventArgs e)
{
Console.WriteLine("**********************button2_Click Start【" + Thread.CurrentThread.ManagedThreadId + "】**********************************************");
List<Thread> threadList = new List<Thread>();
for (int i = 0; i < 5; i++)
{
string studentName = "甲" + i + "同学";
Thread thread = new Thread(() => WriteJob(studentName));
Console.WriteLine($"{studentName}开始写作业");
thread.Start();
threadList.Add(thread);
}
//⽆损耗阻塞主线程
//foreach (var thread in threadList)
//{
//    thread.Join();//表⽰把thread线程任务join到当前线程,也就是当前线程等着thread任务完成
//}
//带返回,有损耗阻塞主线程
while (threadList.Count(t => t.ThreadState != System.Threading.ThreadState.Stopped) > 0)
{
Thread.Sleep(100);
Console.WriteLine("请等待....");
}
Console.WriteLine("**********************button2_Click end【" + Thread.CurrentThread.ManagedThreadId + "】**********************************************");
}
看了上⾯的代码,我们都发现⽆损耗阻塞使⽤的是  thread.Join(); ,有损耗阻塞,我们是需要⾃⼰去写计算逻辑的。
5.线程回调
在多线程中是不存在回调的,我们只能通过⾃⼰去包装,实现线程的回调。在包装之前,我们先回顾
下异步回调的特点:
#region异步回调回顾
{
//回调
AsyncCallback callBack = param =>
{
Console.WriteLine("当前状态:"+param.AsyncState);
Console.WriteLine("你睡吧,我去吃好吃的去了");
};
Func<string, int> func = t => {
Console.WriteLine(t);
Thread.Sleep(5000);
Console.WriteLine("等等吧,我在睡⼀会");
return DateTime.Now.Millisecond;//返回当前毫秒数
};
///第⼀个参数是我们⾃定义的参数,第⼆个参数是我们回调的参数,第三个参数是状态参数
IAsyncResult iAsyncResult= func.BeginInvoke("张四⽕,起床吧", callBack, "runState");
int second= func.EndInvoke(iAsyncResult);
Console.WriteLine("当前毫秒数:"+second);
}
{
//回调
AsyncCallback callBack = param =>
{
Console.WriteLine("当前状态:" + param.AsyncState);
Console.WriteLine("妈妈说:孩⼦太⼩,睡会吧");
Action<string> act = t =>
{
Console.WriteLine(t);
Thread.Sleep(5000);
Console.WriteLine("等等吧,我在睡⼀会");
};
IAsyncResult iAsyncResult = act.BeginInvoke("梓烨,起床吧", callBack, "runState");
act.EndInvoke(iAsyncResult);
}
#endregion
异步回调:是分为有返回和⽆返回两种,返回值类型取决于我们委托的返回值类型,回调就是在⼦线程执⾏完成以后,执⾏⼀个代码块。多线程下如何使⽤回调呢?在Thread下是没有回调的。我们可以根据异步回调的特点⾃⼰封装⼀个线程回调或异步返回值。
#region回调封装
///<summary>
///回调封装⽆返回值
///</summary>
///<param name="start"></param>
///<param name="callback">回调</param>
private void ThreadWithCallback(ThreadStart start, Action callback)
{
Thread thread = new Thread(() =>
{
start.Invoke();
callback.Invoke();
});
thread.Start();
}
///<summary>
///有返回值封装(请根据本案例⾃⾏封装回调)
/
//</summary>
///<typeparam name="T">返回值类型</typeparam>
///<param name="func">需要⼦线程执⾏的⽅法</param>
///<returns></returns>
private Func<T> ThreadWithReturn<T>(Func<T> func)
{
T t = default(T);//初始化⼀个泛型
ThreadStart newStart = () =>
{
t = func.Invoke();
};
Thread thread = new Thread(newStart);
thread.Start();
return new Func<T>(() =>
{
thread.Join();
return t;
});
}
#endregion
代码调⽤
#region多线程回调封装调⽤
{
//⽆返回
ThreadWithCallback(()=>{
Console.WriteLine("梓烨,起床吧");
Thread.Sleep(5000);
Console.WriteLine("等等吧,我在睡⼀会");
},()=> {
Console.WriteLine("妈妈说:梓烨太⼩,睡会吧");
});
}
{
/
/有返回
int secound=  ThreadWithReturn<int>(()=>{
Console.WriteLine("张四⽕,起床吧");
Thread.Sleep(5000);
Console.WriteLine("等等吧,我在睡⼀会");
return DateTime.Now.Millisecond;//返回当前毫秒数
}).Invoke();
Console.WriteLine(secound);
}
#endregion
以上都是 C#1.0时代的多线程,这些现在基本已经没有⼈在使⽤了。注意:本⽂并没有介绍信号量,在没有介绍信号量退出线程的时候,我们还是使⽤net ⾃带的终⽌线程。下边扩展点技术:
6.线程同步
所谓同步:是指在某⼀时刻只有⼀个线程可以访问变量。
如果不能确保对变量的访问是同步的,就会产⽣错误。
c#为同步访问变量提供了⼀个⾮常简单的⽅式,即使⽤c#语⾔的关键字Lock,它可以把⼀段代码定义为互斥段,互斥段在⼀个时刻内只允许⼀个线程进⼊执⾏,⽽其他线程必须等待。在c#中,关键字Lock定义如下:
Lock(expression)
{
statement_block
}
expression代表你希望跟踪的对象:
如果你想保护⼀个类的实例,⼀般地,你可以使⽤this;
如果你想保护⼀个静态变量(如互斥代码段在⼀个静态⽅法内部),⼀般使⽤类名就可以了
⽽statement_block就算互斥段的代码,这段代码在⼀个时刻内只可能被⼀个线程执⾏。
以书店卖书为例
static void Main(string[] args)
{
BookShop book = new BookShop();
//创建两个线程同时访问Sale⽅法
Thread t1 = new Thread(new ThreadStart(book.Sale));//因为使⽤的同⼀个引⽤,所以书店库存量始终是⼀个地址的引⽤
Thread t2 = new Thread(new ThreadStart(book.Sale));
//启动线程
Console.ReadKey();
}
class BookShop
{
public int num = 1;//库存量
public void Sale()
{
int tmp = num;
if (tmp > 0)//判断是否有书,如果有就可以卖
{
Thread.Sleep(1000);
num -= 1;
Console.WriteLine("售出⼀本图书,还剩余{0}本", num);
}
else
{
Console.WriteLine("没有了");
}
}
}
从运⾏结果可以看出,两个线程同步访问共享资源,没有考虑同步的问题,结果不正确(结果出现“-1”)。
如何做到线程的同步呢?我们需要使⽤线程锁 lock
class BookShop
{
public int num = 1;//库存量
public void Sale()
{
// 使⽤lock关键字解决线程同步问题。锁住当前对象
lock (this)
{
int tmp = num;
if (tmp > 0)//判断是否有书,如果有就可以卖
{
Thread.Sleep(1000);
num -= 1;
Console.WriteLine("售出⼀本图书,还剩余{0}本", num);
writeline特点
}
else
{
Console.WriteLine("没有了");
}
}
}
}
7.跨线程访问
在很多实际应⽤中,⼦线程的计算百分⽐要时刻返回给主线程,列⼊进度条。我们以winform 的 textbox 输出1-100为列。
产⽣错误的原因:textBox1是由主线程创建的,thread线程是另外创建的⼀个线程,在.NET上执⾏的是托管代码,C#强制要求这些代码必须是线程安全的,即不允许跨线程访问Windows窗体的控件。
解决⽅案:
1、在窗体的加载事件中,将C#内置控件(Control)类的CheckForIllegalCrossThreadCalls属性设置为false,屏蔽掉C#编译器对跨线程调⽤的检查。如下:
System.Windows.Forms.Control.CheckForIllegalCrossThreadCalls = false;
使⽤上述的⽅法虽然可以保证程序正常运⾏并实现应⽤的功能,但是在实际的软件开发中,做如此设置是不安全的(不符合.NET的安全规范),在产品软件的开发中,此类情况是不允许的。如果要在遵守.NET安全标准的前提下,实现从⼀个线程成功地访问另⼀个线程创建的空间,要使⽤C#的⽅法回调机制。
2、使⽤回调函数
回调前边有讲解,这⾥直接上代码,注意,这⾥的回调,使⽤的是控件⾃带的回调哦。
private void button1_Click(object sender, EventArgs e)
{
Action<int> act = t=> { Box1.Text = t.ToString(); };
//创建⼀个线程去执⾏这个⽅法:创建的线程默认是前台线程
Thread thread = new Thread(()=> {
for (int i = 0; i < 100; i++)
{
Thread.Sleep(100);
// Box1.Invoke(t => { Box1.Text = t.ToString(); }, i);这⾥不允许使⽤ lambda 表达式,因为⾥⾯传递的是⼀个委托类型,不是委托
}

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。