1. C#高级内容
1.1. 多线程
1.1.1. 线程基础操作
1.1.1.1. new Thread和Start()
C#中新建一个线程的方法是创建一Thread实例,构造参数接收一个方法指针(委托),调用Start()
让线程开始执行:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace TestThread
{
public delegate int TakesAWhileDelegate(int data, int ms);
class Program
{
/*
多线程测试
*/
static void Main(string[] args)
{
Thread t = new Thread(WriteB);
t.Name = "t1";
t.Start();
//Console.WriteLine(t.IsAlive);
Thread.CurrentThread.Name = "主线程";
WriteA();
//Console.WriteLine(t.IsAlive);
Console.ReadLine();
}
static void WriteA()
{
Console.WriteLine($"我是线程[{Thread.CurrentThread.Name}],我执行任务:打印字符A");
for (int i = 0; i < 100; i++)
{
Console.Write("A");
}
}
static void WriteB()
{
Console.WriteLine($"我是线程[{Thread.CurrentThread.Name},我执行任务:打印字符B");
for (int i = 0; i < 100; i++)
{
Console.Write("B");
}
}
}
}
运行结果:
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Thread的两个构造方法:
- public Thread(ThreadStart start):start为无参的委托类型,适用于目标任务方法无参数的情况。
- public Thread(ParameterizedThreadStart start):start是有参的委托类型,参数为
object
类型,执行时通过Start(object param)
传递实参。
1.1.1.2. Join()和Sleep()
Sleep(int ms)
:当前线程休眠ms毫秒t.Join()
:当前线程等待线程t运行结束
线程在Sleep和Join期间的状态是WaitJoinSleep,Thread.CurrentThread.State
获取当前线程的状态
1.1.1.3. 线程安全问题
C#提供lock
语句块控制线程间同步。
语法:
lock(lock对象){
...
}
示例:
namespace TestThread
{
class Program3
{
bool _done;
object _lock = new object();
static void Main(string[] args)
{
Program3 p = new Program3();
/*
* 线程不安全
new Thread(p.Go).Start();
p.Go();
*/
/*线程安全*/
new Thread(p.GoWithLock).Start();
p.GoWithLock();
Console.ReadKey();
}
private void Go()
{
if (!_done)
{
Console.WriteLine("Done.");
_done = true;
}
}
//使用lock解决线程安全问题
private void GoWithLock()
{
lock (this)
{
if (!_done)
{
Console.WriteLine("Done.");
_done = true;
}
}
}
}
}
运行结果:
- 使用lock
Done.
- 不使用lock
Done. Done.
1.1.1.4. 多线程异常处理
一段示例:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace TestThreadEx
{
class Program
{
static void Main(string[] args)
{
try
{
new Thread(Go).Start();
}
catch (Exception e)
{
Console.WriteLine("异常被抛出:" + e.Message);
}
}
private static void Go()
{
throw null;
}
}
}
在上面的程序中,Go
方法抛出异常,但Main()
方法中的catch并不会捕获到这个异常,这个异常直接被抛给了运行时环境。
正确的解决方法:在Go方法中捕获可能抛出的异常。 改正:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace TestThreadEx
{
class Program2
{
static void Main(string[] args)
{
new Thread(Go).Start();
Console.ReadKey();
}
private static void Go()
{
try
{
throw null;
}
catch (Exception e)
{
Console.WriteLine("异常被抛出:" + e.Message);
}
}
}
}
运行结果:
异常被抛出:未将对象引用设置到对象的实例。
1.1.1.5. background线程和foreground线程
- 后台线程:即background线程,后台线程不会阻止程序的终止,当所有前台线程结束,所有后台线程都会马上结束
- 前台线程:即foreground线程,只要有一个前台线程还在运行,程序就不会终止。
1.1.2. Signaling
即信号量 示例:
using log4net;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace TestSignal
{
class Program
{
private static readonly ILog logger = LogManager.GetLogger(typeof(Program));
static void Main(string[] args)
{
var signal= new ManualResetEvent(false);
new Thread(() =>
{
logger.Info("Waiting for signal");
signal.WaitOne();
signal.Dispose();
logger.Info("Got the signal");
}).Start();
Thread.Sleep(2000);
signal.Set();
}
}
}
输出结果:
2022-07-22 18:19:27,665 [4] INFO TestSignal.Program - Waiting for signal
2022-07-22 18:19:29,665 [4] INFO TestSignal.Program - Got the signal
1.1.3. 富客户端中的多线程
富客户端包括WPF、Metro、WinForm等应用。
1.1.3.1. 问题和解决方法
在富客户端应用中,控件和UI元素只能由创建它们的线程去控制(一般是main UI线程),如果在非UI线程中想要更新控件的内容,需要把请求转发给UI线程,技术术语叫做marshal
,解决方法如下:
1.1.3.1.1. 低级方法
- 在WPF应用中,在UI元素的
Dispather
对象上调用BeginInvoke()
或者Invoke()
方法 - 在WinForm应用中,在控件上调用
BeginInvoke()
或者Invoke()
BeginInvoke()
和Invoke()
功能是一样的,区别在于Invoke()
会阻塞当前线程,而BeginInvoke()
不会
1.1.3.1.2. 高级方法
除了调用BeginInvoke()和Invoke()方法,还可以使用SynchronizationContext来解决非UI线程更新控件内容的问题:示例代码:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace TestMarshal
{
public partial class Form1 : Form
{
SynchronizationContext _uiSyncContext;
public Form1()
{
InitializeComponent();
this.lbTime_1.Text = this.lbTime_1.Name+":"+DateTime.Now.ToString();
_uiSyncContext = SynchronizationContext.Current;
new Thread(Work).Start();
}
private void Work()
{
Thread.Sleep(5000);
UpdateMessage("The answer");
}
private void UpdateMessage(string msg)
{
// Marshal the delegate to the UI thread:
_uiSyncContext.Post((m) => lbTime_2.Text = (string)m,this.lbTime_2.Name+":"+DateTime.Now.ToString());
}
}
}
1.1.4. 线程池
需要注意的几个点:
- 线程池中的线程不能设置Name
- 线程池中的线程总是background线程
- 阻塞的池中线程可能会降低性能
使用线程池的好处有:
- 线程预先创建,任务到达时立即执行,提高效率
- 避免手动创建过多的活跃线程(数量超过CPU核心数)导致上下文切换带来的时间浪费
1.1.4.1. 使用
使用线程池最简单的方法是调用Task.Run(),如下:
using log4net;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace TestThreadPool
{
class Program
{
private static readonly ILog logger = LogManager.GetLogger(typeof(Program));
static void Main(string[] args)
{
Thread.CurrentThread.Name = "Main";
logger.Info($"线程[{Thread.CurrentThread.Name}]开始执行");
Task.Run(() =>
{
logger.Info($"线程[{Thread.CurrentThread.Name}]开始执行");
Thread.Sleep(2000);
logger.Info($"线程[{Thread.CurrentThread.Name}]结束执行");
});
Thread.Sleep(2000);
logger.Info($"线程[{Thread.CurrentThread.Name}]结束执行");
}
}
}
执行结果:
2022-07-22 20:15:40,599 [Main] INFO TestThreadPool.Program - 线程[Main]开始执行
2022-07-22 20:15:40,615 [4] INFO TestThreadPool.Program - 线程[]开始执行
2022-07-22 20:15:42,626 [4] INFO TestThreadPool.Program - 线程[]结束执行
2022-07-22 20:15:42,626 [Main] INFO TestThreadPool.Program - 线程[Main]结束执行
1.1.5. Task
Task作为并发库的一部分,是在.NET framework 4.0中发布的。
Task默认使用线程池中的线程执行任务。
Task
的几个主要方法如下:
Task Run()
:运行任务,有多个重载形式- 应用场景
默认情况下,CLR在池中线程运行task,因此适合较短运行时间的任务,如果任务时间长且是阻塞操作,可以调用下面的方法:
Task task = Task.Factory.StartNew (() => ..., TaskCreationOptions.Longrunning);
- 返回值
```csharp
private static void TestTaskResult()
{
Task
task = Task.Run( () => { logger.Debug("start..."); Thread.Sleep(3000); logger.Debug("end..."); return 3; }); //task.Wait(); logger.Debug($"task执行结果:{task.Result}"); }
运行结果: 2022-07-23 08:42:37,689 [4] DEBUG TestTask.Program - start… 2022-07-23 08:42:40,697 [4] DEBUG TestTask.Program - end… 2022-07-23 08:42:40,697 [1] DEBUG TestTask.Program - task执行结果:3 ```
当task未完成时,
task.Result
会阻塞当前线程,因此task.Wait()不是必要的- 应用场景
默认情况下,CLR在池中线程运行task,因此适合较短运行时间的任务,如果任务时间长且是阻塞操作,可以调用下面的方法:
void Wait()
:当前线程等待task线程执行完 示例:using log4net; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; namespace TestTask { class Program { private static readonly ILog logger = LogManager.GetLogger(typeof(Program)); static void Main(string[] args) { /*Task应用*/ TestWait(); } private static void TestWait() { Task task = Task.Run(() => { Thread.Sleep(2000); logger.Debug("Foo"); }); logger.Debug(task.IsCompleted); task.Wait(); logger.Debug(task.IsCompleted); } } }
运行结果:
2022-07-23 08:25:20,747 [1] DEBUG TestTask.Program - False 2022-07-23 08:25:22,758 [4] DEBUG TestTask.Program - Foo 2022-07-23 08:25:30,935 [1] DEBUG TestTask.Program - True
1.1.5.1. Task和异常
不同于直接使用Thread,Task可以方便地传播异常。 Task在执行时发生异常会将异常抛给调用task.Wati()方法或访问task.Result属性的线程。 示例:
private static void TestTaskException()
{
Task task = Task.Run(() => {
logger.Debug("开始执行.");
Thread.Sleep(2000);
logger.Debug("抛出了一个异常.");
throw null;
});
try
{
task.Wait();
}
catch (Exception e)
{
logger.Debug($"捕获到一个异常{e.Message}");
}
}
运行结果:
2022-07-23 08:59:33,010 [4] DEBUG TestTask.Program - 开始执行.
2022-07-23 08:59:35,031 [4] DEBUG TestTask.Program - 抛出了一个异常.
2022-07-23 08:59:35,041 [1] DEBUG TestTask.Program - 捕获到一个异常发生一个或多个错误。