C#多线程和异步(一)——基本概念和使用方法
一、多线程相关的基本概念
进程(Process):是系统中的一个基本概念。 一个正在运行的应用程序在操作系统中被视为一个进程,包含着一个运行程序所需要的资源,进程可以包括一个或多个线程 。进程之间是相对独立的,一个进程无法访问另一个进程的数据(除非利用分布式计算方式),一个进程运行的失败也不会影响其他进程的运行,Windows系统就是利用进程把工作划分为多个独立的区域的。进程可以理解为一个程序的基本边界。
线程(Thread):是 进程中的基本执行单元,是操作系统分配CPU时间的基本单位 ,在进程入口执行的第一个线程被视为这个进程的 主线程 。
多线程能实现的基础:
1、CPU运行速度太快,硬件处理速度跟不上,所以操作系统进行分时间片管理。这样,宏观角度来说是多线程并发 ,看起来是同一时刻执行了不同的操作。但是从微观角度来讲,同一时刻只能有一个线程在处理。
2、目前电脑都是多核多CPU的,一个CPU在同一时刻只能运行一个线程,但是 多个CPU在同一时刻就可以运行多个线程 。
多线程的优点:
可以同时完成多个任务;可以让占用大量处理时间的任务或当前没有进行处理的任务定期将处理时间让给别的任务;可以随时停止任务;可以设置每个任务的优先级以优化程序性能。
多线程的缺点:
1、 内存占用 线程也是程序,所以线程需要占用内存,线程越多,占用内存也越多(每个线程都需要开辟堆栈空间,多线程时有时需要切换时间片)。
2、 管理协调 多线程需要协调和管理,所以需要占用CPU时间以便跟踪线程,线程太多会导致控制太复杂。
3、 资源共享 线程之间对共享资源的访问会相互影响,必须解决争用共享资源的问题。
二、C#中的线程使用
2.1 基本使用
2.1.1 无参时
1 class Program 2 { 3 static void Main(string[] args) 4 { 5 ThreadTest test = new ThreadTest(); 6 //无参调用实例方法 7 Thread thread1 = new Thread(test.Func2); 8 thread1.Start(); 9 Console.ReadKey(); 10 } 11 } 12 13 class ThreadTest 14 { 15 public void Func2() 16 { 17 Console.WriteLine("这是实例方法"); 18 } 19 }
2.1.2 有参数时
1 class Program 2 { 3 static void Main(string[] args) 4 { 5 ThreadTest test = new ThreadTest(); 6 //有参调用实例方法,ParameterizedThreadStart是一个委托,input为object,返回值为void 7 Thread thread1 = new Thread(new ParameterizedThreadStart(test.Func1)); 8 thread1.Start("有参的实例方法"); 9 Console.ReadKey(); 10 } 11 } 12 class ThreadTest 13 { 14 public void Func1(object o) 15 { 16 Console.WriteLine(o); 17 } 18 }
2.2 常用的属性和方法
属性名称 | 说明 |
---|---|
CurrentThread | 获取当前正在运行的线程。 |
ExecutionContext | 获取一个 ExecutionContext 对象,该对象包含有关当前线程的各种上下文的信息。 |
IsBackground | bool,指示某个线程是否为后台线程。 |
IsThreadPoolThread | bool,指示线程是否属于托管线程池。 |
ManagedThreadId | int,获取当前托管线程的唯一标识符。 |
Name | string,获取或设置线程的名称。 |
Priority |
获取或设置一个值,该值指示线程的调度优先级 。 Lowest<BelowNormal<Normal<AboveNormal<Highest |
ThreadState |
获取一个值,该值包含当前线程的状态。 Unstarted、Sleeping、Running 等 |
方法名称 | 说明 |
---|---|
GetDomain() | 返回当前线程正在其中运行的当前域。 |
GetDomainId() | 返回当前线程正在其中运行的当前域Id。 |
Start() | 执行本线程。(不一定立即执行,只是标记为可以执行) |
Suspend() | 挂起当前线程,如果当前线程已属于挂起状态则此不起作用 |
Resume() | 继续运行已挂起的线程。 |
Interrupt() | 中断处于 WaitSleepJoin 线程状态的线程。 |
Abort() | 终结线程 |
Join() | 阻塞调用线程,直到某个线程终止。 |
Sleep() | 把正在运行的线程挂起一段时间。 |
看一个简单的演示线程方法的栗子:
namespace ThreadForm { public partial class Form1 : Form { Thread thread; int index = 0; public Form1() { InitializeComponent(); } //启动按钮 private void startBtn_Click(object sender, EventArgs e) { //创建一个线程,每秒在textbox中追加一下执行次数 if (thread==null) { thread = new Thread(() => { while (true) { index++; try { Thread.Sleep(1000); textBox1.Invoke(new Action(() => { textBox1.AppendText($"第{index}次,"); })); } catch (Exception ex) { MessageBox.Show(ex.ToString()); } } }); //启动线程 thread.Start(); } } //挂起按钮 private void suspendBtn_Click(object sender, EventArgs e) { if (thread != null && thread.ThreadState==ThreadState.Running || thread.ThreadState==ThreadState.WaitSleepJoin) { thread.Suspend(); } } //继续运行挂起的线程 private void ResumeBtn_Click(object sender, EventArgs e) { if (thread!=null && thread.ThreadState==ThreadState.Suspended) { thread.Resume(); } } //interrupt会报一个异常,并中断处于WaitSleepJoin状态的线程 private void InterruptBtn_Click(object sender, EventArgs e) { if (thread != null && thread.ThreadState==ThreadState.WaitSleepJoin) { thread.Interrupt(); } } //abort会报一个异常,并销毁线程 private void AbortBtn_Click(object sender, EventArgs e) { if (thread != null) { thread.Abort(); } } //定时器,刷新显示线程状态 private void timer1_Tick(object sender, EventArgs e) { if (thread!=null) { txtStatus.Text = thread.ThreadState.ToString(); } } //窗体加载 private void Form1_Load(object sender, EventArgs e) { timer1.Interval = 100; timer1.Enabled = true; } //窗口关闭时,关闭进程 private void Form1_FormClosing(object sender, FormClosingEventArgs e) { System.Diagnostics.Process[] processes = System.Diagnostics.Process.GetProcessesByName("ThreadForm"); foreach (var item in processes) { item.Kill(); } } } }
当点击Start按钮,线程启动文本框会开始追加【第x次】字符串;点击Suspend按钮,线程挂起,停止追加字符串;点击Resume按钮会让挂起线程继续运行;点击Interrupt按钮弹出一个异常信息,线程状态从WaitSleepJoin变为Running,线程继续运行;点击Abort按钮会弹出一个异常信息并销毁线程。
一点补充:Suspend、Resume方法已不建议使用,推荐使用AutoResetEvent和ManualResetEvent来控制线程的暂停和继续,用法也十分简单,这里不详细介绍,有兴趣的小伙伴可以研究下。
2.3 线程同步
所谓同步: 是指在某一时刻只有一个线程可以访问变量 。
c#为同步访问变量提供了一个非常简单的方式,即使用c#语言的关键字Lock,它可以把一段代码定义为互斥段,互斥段在一个时刻内只允许一个线程进入执行,实际上是Monitor.Enter(obj),Monitor.Exit(obj)的语法糖。在c#中,lock的用法如下:
lock (obj) { dosomething... }
obj代表你希望锁定的对象,注意一下几点:
1. lock不能锁定空值 ,因为Null是不需要被释放的。 2. 不能锁定string类型 ,虽然它也是引用类型的。因为字符串类型被CLR“暂留”,这意味着整个程序中任何给定字符串都只有一个实例,具有相同内容的字符串上放置了锁,就将锁定应用程序中该字符串的所有实例。 3. 值类型不能被lock ,每次装箱后的对象都不一样 ,锁定时会报错 4 避免锁定public类型 如果该实例可以被公开访问,则 lock(this) 可能会有问题,因为不受控制的代码也可能会锁定该对象。
推荐使用 private static readonly类型的对象,readonly是为了避免lock的代码块中修改对象,造成对象改变后锁失效。
以书店卖书为例
1 class Program 2 { 3 static void Main(string[] args) 4 { 5 BookShop book = new BookShop(); 6 //创建两个线程同时访问Sale方法 7 Thread t1 = new Thread(book.Sale); 8 Thread t2 = new Thread(book.Sale); 9 //启动线程 10 t1.Start(); 11 t2.Start(); 12 Console.ReadKey(); 13 } 14 } 15 class BookShop 16 { 17 //剩余图书数量 18 public int num = 1; 19 private static readonly object locker = new object(); 20 public void Sale() 21 { 22 23 lock (locker) 24 { 25 int tmp = num; 26 if (tmp > 0)//判断是否有书,如果有就可以卖 27 { 28 Thread.Sleep(1000); 29 num -= 1; 30 Console.WriteLine("售出一本图书,还剩余{0}本", num); 31 } 32 else 33 { 34 Console.WriteLine("没有了"); 35 } 36 } 37 } 38 }
代码执行结果时:
如果不添加lock则执行的结果时:
2.4 跨线程访问
例子:点击测试按钮,给文本框赋值
代码如下:
1 private void myBtn_Click(object sender, EventArgs e) 2 { 3 Thread thread1 = new Thread(SetValue); 4 thread1.Start(); 5 6 } 7 private void SetValue() 8 { 9 for (int i = 0; i < 10000; i++) 10 { 11 this.myTxtBox.Text = i.ToString(); 12 } 13 }
执行代码会出现如下错误:
出现该错误的原因是:myTxtBox是由主线程创建的,thread1线程是另外一个线程,在.NET上执行的是托管代码, C#强制要求这些代码必须是线程安全的,即不允许跨线程访问Windows窗体的控件
解决的方法:
public Form1() { InitializeComponent(); } //点击按钮开启一个新线程 private void myBtn_Click(object sender, EventArgs e) { Thread thread1 = new Thread(SetValues); thread1.IsBackground = true; thread1.Start(); } //新线程给文本框赋值 private void SetValues() { Action<int> setVal = (i) => { this.myTxtBox.Text = i.ToString(); }; for (int i = 0; i < 10000; i++) { this.myTxtBox.Invoke(setVal, i); } }
Invoke:在“拥有控件的基础窗口句柄的线程” 即在本例的主线程上执行委托,这样就不存在跨线程访问了 ,因此还是线程安全的。
参考文章: