多线程
操作系统的进程和线程,线程就是围绕进程的一个子单元。
理解多线程
例 1
界面上点击某个按钮后,需要执行一个非常耗时的操作,如果不使用多线程,就只能傻等操作返回。
使用多线程,点击按钮之后,开辟1个新线程后台去执行这个耗时操作,前台界面继续执行其他菜单目录,录入数据等。
例 2
有个操作,1个线程需要20分钟完成,现在的多核cpu,可以真正同一时刻运行多个线程。
假设是双核cpu,同一时刻运行2个线程,操作就只需要10分钟可以完成。
多线程优点:
1提高应用程序执行效率【例1】
2提高CPU利用率【例2】
线程的数量
理想的线程数量<=CPU核心数量
Thread类
Thread thread = new Thread(SayHi);
thread.Start();
线程的挂起
Thread.Sleep(1000);
线程的阻塞
示例:
for (var i = 0; i < 5; i++)
{
Thread th1 = new Thread(DoSomething);
th1.Start();
th1.Join();
}
阻塞后,各线程会依次执行。
线程终止,不推荐使用,不安全不可控。
前台线程、后台线程
Thread thread = new Thread(() =>
{
// 默认为 False
Console.WriteLine(Thread.CurrentThread.IsBackground);
});
// 可以设置为后台线程
//thread.IsBackground = true;
thread.Start();
thread.Join();
.Net的公用语言运行时(Common Language Runtime,CLR)能区分两种不同类型的线程:前台线程和后台线程。这两者的区别就是:应用程序必须运行完所有的前台线程才可以退出;而对于后台线程,应用程序则可以不考虑其是否已经运行完毕而直接退出,所有的后台线程在应用程序退出时都会自动结束。
一般后台线程用于处理时间较短的任务,如在一个Web服务器中可以利用后台线程来处理客户端发过来的请求信息。而前台线程一般用于处理需要长时间等待的任务,如在Web服务器中的监听客户端请求的程序,或是定时对某些系统资源进行扫描的程序。
重要性
(1)有前台,不关闭
如果线程为前台线程,可能导致UI线程已关闭,但实际还有前台线程暗地里运行,所以程序并没有真正关闭。
(2)无前台,全关闭
当然也要注意,如果所有前台线程都关闭,后台线程会自动关闭,后台线程的代码逻辑可能没执行完就终止了。
线程传递参数
1使用ParameterizedThreadStart委托
注意:此处只能接受object类型参数,因为Thread中只接受ThreadStart委托,而该委托是一个无参无返回值的。所以此处使用ParameterizedThreadStart。
ParameterizedThreadStart pts1 = DoSomething;
Thread thread = new Thread(pts1);
object name = "线程参数";
thread.Start(name);
在线程执行过程中,该参数需要使用需要这是一个只读属性的对象
static readonly object obj = new object();
public static void DoSomething(string name)
{
Console.WriteLine("线程{0}启动", Thread.CurrentThread.ManagedThreadId);
Stopwatch sw = new Stopwatch();
sw.Start();
long result = 0;
for (int i = 0; i < 1000000000; i++)
{
result += i;
}
Thread.Sleep(2000);
sw.Stop();
Console.WriteLine("任务名称={3},当前线程={0},计算结果是{1},总耗时:{2}", Thread.CurrentThread.ManagedThreadId, result, sw.ElapsedMilliseconds, obj.ToString());
}
2使用lambda传递参数(推荐)
string hi = "张三";
Thread thread = new Thread(() =>
{
DoSomething(hi);
});
thread.Start();
线程异常
注意:只要有线程抛出异常,整个程序就会停止。所以,异常必须处理。
对创建线程try catch没用,需要对线程中调用的函数添加异常处理代码
static void Main(string[] args)
{
try // 无用的 try catch
{
Thread thread = new Thread(SayHi);
thread.Start();
}
catch (Exception ex)
{
// 不会打印错误
Console.WriteLine(ex.ToString());
}
}
//正确
static void Main(string[] args)
{
Thread thread = new Thread(SayHi);
thread.Start();
thread.Join();
}
static void SayHi()
{
try
{
Console.WriteLine("hi");
throw new Exception("error");
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
线程安全
试试下列代码:
static void Main(string[] args)
{
Thread thread = new Thread(
() => { Check("张三"); });
Thread thread2 = new Thread(
() => { Check("李四"); });
thread.Start();
thread2.Start();
thread.Join();
thread2.Join();
}
static void Check(string name)
{
User.Name = name;
string format = @"{0} = {1}";
for (int i = 0; i < 10; i++)
{
string s = string.Format(format, name, User.Name);
Console.WriteLine(s);
Thread.Sleep(10);
}
}
public class User
{
public static string Name { get; set; }
}
张三等于了李四
原因:User.Name是静态属性,也就是共享资源,多个线程访问共享资源,需要对共享资源做同步处理。
如何解决:
创建一个只读对象对对象处理使用lock(锁操作)
基本含义: lock就是把一段代码定义为临界区,所谓临界区就是同一时刻只能有一个线程来操作临界区的代码,当一个线程位于代码的临界区时,另一个线程不能进入临界区,如果 试图进入临界区,则只能一直等待(即被阻止),直到已经进入临界区的线程访问完毕,并释放锁旗标
static readonly object obj = new object();
// -------------------------
lock (obj)
{
User.Name = name;
string format = @"{0} = {1}";
for (int i = 0; i < 10; i++)
{
string s = string.Format(format, name, User.Name);
Console.WriteLine(s);
Thread.Sleep(10);
}
}
多线程的缺点
1线程占用一定资源,比如内存,CPU;创建和销毁操作都比较昂贵
2线程调度器要管理线程
3大量的创建线程,导致内存不够用,线程调度器繁忙。
4当多个线程竞争同一个静态资源时,会造成死锁。