【ET6.0】C#多线程
一个简单的多线程例子
public class Program1_1
{
public static void Main1_1()
{
Console.WriteLine($"main thread1 {Thread.CurrentThread.ManagedThreadId}");
Thread thread = new Thread(_ =>
{
Console.WriteLine($"thread start {Thread.CurrentThread.ManagedThreadId}");
});
Console.WriteLine($"main thread2 {Thread.CurrentThread.ManagedThreadId}");
thread.Start();
thread.Join(); //等待多线程执行完成
Console.WriteLine($"main thread3 {Thread.CurrentThread.ManagedThreadId}");
}
}
main thread1 1
main thread2 1
thread start 4
main thread3 1
线程结果回调
假设一个需求:服务器端接收到一个寻路请求,并在寻路结束后返回结果给客户端。
因为寻路计算量大,主线程新开一个进程执行寻路逻辑,并传入一个发送消息的回调。新线程执行完寻路逻辑之后,执行主线程传入的回调。
public class Program1_2
{
public static Object obj = new object();
public static void Main1_2()
{
int a = 0;
Console.WriteLine($"Main thread {Thread.CurrentThread.ManagedThreadId}");
StartThread(() => { FindPath(() => { SendMsg(ref a); }); });
StartThread(() => { FindPath(() => { SendMsg(ref a); }); });
while (true)
{
}
}
public static void StartThread(Action action)
{
new Thread(_ =>
{
Thread.Sleep(new Random().Next(0, 10));
action.Invoke();
}).Start();
}
public static void FindPath(Action ThreadFinishCallback)
{
Console.WriteLine($"FindPath {Thread.CurrentThread.ManagedThreadId}");
ThreadFinishCallback.Invoke();
}
public static void SendMsg(ref int a)
{
lock (obj)
{
a++;
Thread.Sleep(10000);
}
Console.WriteLine($"SendMsg: {a} {Thread.CurrentThread.ManagedThreadId}");
}
}
Main thread 1
FindPath 4
FindPath 5
SendMsg: 1 4
SendMsg: 2 5
多线程SendMsg中竞争公共资源变量a。因为有对公共资源的写入,应该对其加锁。加锁不仅性能低(并发度降低,多个线程需要互相等待才能访问共享资源。这样会增加线程的等待时间),又有可能导致死锁问题,举一个计算发生死锁的最大资源数的问题: 假设单个线程执行任务需要n个资源,此时总共有(n-1)*m个资源,m个线程。如果每个线程占有n-1个资源,并且等待其他线程释放资源,所有线程都阻塞了,这时需要系统强制释放资源或者杀掉进程才能重新运行,简直是灾难性事故。
如果多线程sendMsg可以改成单线程的就好了。即SendMsg方法统一在主线程调用,异步变同步,也不用担心数据不一致的问题。具体看下面。
跨线程操作
public class Program1_3
{
public static void Main1_3()
{
int a = 0;
ThreadSynchronizationContext.Instance.Post(()=>{Console.WriteLine($"Main thread {Thread.CurrentThread.ManagedThreadId}");});//设置主线程
StartThread(() => { FindPath(() => { SendMsg(ref a); }); });
StartThread(() => { FindPath(() => { SendMsg(ref a); }); });
while (true)
{
Thread.Sleep(1); //避免cpu占用率太高
ThreadSynchronizationContext.Instance.Update();
}
}
public static void StartThread(Action action)
{
new Thread(_ => { action.Invoke(); }).Start();
}
public static void FindPath(Action ThreadFinishCallback)
{
Console.WriteLine($"FindPath {Thread.CurrentThread.ManagedThreadId}");
ThreadSynchronizationContext.Instance.Post(ThreadFinishCallback); //将回调扔回主线程执行
}
public static void SendMsg(ref int a)
{
a++;
Console.WriteLine($"SendMsg: {a} {Thread.CurrentThread.ManagedThreadId}");
}
}
Main thread 1
FindPath 4
FindPath 5
SendMsg: 1 1
SendMsg: 2 1
在上面的例子中,新线程执行完寻路操作后,将SendMsg回调传入ThreadSynchronizationContext的回调队列中,并在Update中不断从队列取出回调执行,可以看到SendMsg编程在主线程执行了。
ThreadSynchronizationContext实现如下:
namespace ET
{
public class ThreadSynchronizationContext : SynchronizationContext
{
//单例---主线程
public static ThreadSynchronizationContext Instance { get; } = new ThreadSynchronizationContext(Thread.CurrentThread.ManagedThreadId);
//主线程id
private readonly int threadId;
//线程同步队列,发送 接受socket回调都放回该队列,由poll线程同一执行
private readonly ConcurrentQueue<Action> queue = new ConcurrentQueue<Action>();
//ConCurrentQueue是一个线程安全的队列 https://learn.microsoft.com/en-us/dotnet/api/system.collections.concurrent.concurrentqueue-1?view=net-7.0
private Action a;
public ThreadSynchronizationContext(int threadId)
{
//传入主线程id
this.threadId = threadId;
}
public void Update()
{
while (true)
{
//尝试将队列中第一个委托出队,如果成功出队,则执行委托,否则结束这一委托
if (!this.queue.TryDequeue(out a))
{
return;
}
try
{
a();
}
catch (Exception e)
{
Log.Error(e);
}
}
}
public override void Post(SendOrPostCallback callback, object state)
{
this.Post(()=>callback(state));
}
public void Post(Action action)
{
//如果当前线程就是主线程,那么不需要执行入队这一操作,直接执行委托
if (Thread.CurrentThread.ManagedThreadId == this.threadId)
{
try
{
action();
}
catch (Exception e)
{
Log.Error(e);
}
return;
}
this.queue.Enqueue(action);
}
public void PostNext(Action action)
{
this.queue.Enqueue(action);
}
}
}
Task.Run的简单例子
注意,Task.Run是从线程池中取出一个线程(不一定新建进程,也有可能从池中取出空闲的)。
public class Program1_4
{
public static void Main1_4()
{
//设置主线程
ThreadSynchronizationContext.Instance.Post(()=>{Console.WriteLine($"Main thread {Thread.CurrentThread.ManagedThreadId}");});
Task.Run(() =>{FindPath(SendMsg);});
Task.Run(() =>{FindPath(SendMsg);});
while (true)
{
Thread.Sleep(1);
ThreadSynchronizationContext.Instance.Update();
}
}
public static void FindPath(Action ThreadFinishCallback)
{
Console.WriteLine($"FindPath {Thread.CurrentThread.ManagedThreadId}");
ThreadSynchronizationContext threadSynchronizationContext = ThreadSynchronizationContext.Instance;
threadSynchronizationContext.Post(ThreadFinishCallback); //这里先主动回调扔回主线程执行
}
public static void SendMsg()
{
Console.WriteLine($"SendMsg: {Thread.CurrentThread.ManagedThreadId}");
}
}
Main thread 1
FindPath 6
FindPath 4
SendMsg: 1
SendMsg: 1
await的使用
public class Program1_5
{
public static void Main1_5()
{
//设置主线程
Console.WriteLine($"Main thread {Thread.CurrentThread.ManagedThreadId}");
StartTask();
while (true)
{
}
}
public static async void StartTask()
{
await Task.Run(FindPath);
SendMsg();
}
public static void FindPath()
{
Console.WriteLine($"FindPath {Thread.CurrentThread.ManagedThreadId}");
}
public static void SendMsg()
{
Console.WriteLine($"SendMsg: {Thread.CurrentThread.ManagedThreadId}");
}
}
Main thread 1
FindPath 4
SendMsg: 4
同步上下文
可以看到,这个例子中的SendMsg又变成多线程的了。每次执行完异步操作都要手动同步上下文,这实在太麻烦了,有没有什么方法能让编译器帮我们搞定同步的问题呢?
我们先修改ThreadSynchronizationContext构造函数。
public ThreadSynchronizationContext(int threadId)
{
//传入主线程id
this.threadId = threadId;
SynchronizationContext.SetSynchronizationContext(this); //设置要同步到当前线程。
}
public class Program1_5
{
public static void Main1_5()
{
//设置主线程
Game.ThreadSynchronizationContext.Post(()=>{Console.WriteLine($"Main thread {Thread.CurrentThread.ManagedThreadId}");});
StartTask();
StartTask();
while (true)
{
Thread.Sleep(1);
Game.ThreadSynchronizationContext.Update();
}
}
public static async void StartTask()
{
await Task.Run(FindPath);
SendMsg();
}
public static void FindPath()
{
Console.WriteLine($"FindPath {Thread.CurrentThread.ManagedThreadId}");
}
public static void SendMsg()
{
Console.WriteLine($"SendMsg: {Thread.CurrentThread.ManagedThreadId}");
}
}
Main thread 1
FindPath 4
FindPath 6
SendMsg: 1
SendMsg: 1
可以看到,此时我们并没有调用Post方法,但是SendMsg还是回到主线程了。编译器在编译之后为我们生成了一些额外的代码,也就是说实际的代码可能长下面这个样子。
public static async void StartTask()
{
SynchronizationContext synchronizationContext = SynchronizationContext.Current;
await Task.Run(FindPath);
//没有同步上下文,可能送入一个新线程执行,也有可能在之前的线程上执行
if (synchronizationContext == null)
{
// SendMsg();
ThreadPool.QueueUserWorkItem((_) => { SendMsg(); });
}
//扔回同步上下文里
else
{
synchronizationContext.Post((_) =>
{
SendMsg();
},null);
}
}
在上面ThreadSynchronizationContext构造函数我们设置了SynchronizationContext.Current。因此会自动调用post将await之后的操作回调到主线程执行。
将回调改成await的形式
//把一个回调转成task
public static async Task Run(Action action)
{
TaskCompletionSource tcs = new TaskCompletionSource();
//线程池执行任务
ThreadPool.QueueUserWorkItem((_) =>
{
action.Invoke();
Console.WriteLine($"set result: {Thread.CurrentThread.ManagedThreadId}");
tcs.SetResult();
});
await tcs.Task;
}
public static async void StartTask()
{
await Run(FindPath);
SendMsg();
}
把Task修改成ETTask
public class Program1_6
{
public static void Main1_6()
{
//设置主线程
Game.ThreadSynchronizationContext.Post(()=>{Console.WriteLine($"Main thread {Thread.CurrentThread.ManagedThreadId}");});
StartTask2().Coroutine();
while (true)
{
Thread.Sleep(1);
Game.ThreadSynchronizationContext.Update();
}
}
public static async ETTask ETRun(Action action)
{
ETTask tcs = ETTask.Create();
SynchronizationContext synchronizationContext = SynchronizationContext.Current;
ThreadPool.QueueUserWorkItem((_) =>
{
action.Invoke();
if (synchronizationContext == null)
{
Console.WriteLine($"thread finish callback:{Thread.CurrentThread.ManagedThreadId}");
tcs.SetResult();
}
else
{
synchronizationContext.Post((_) => { tcs.SetResult();},null);
}
});
await tcs;
}
public static async ETTask StartTask2()
{
await ETRun(FindPath);
SendMsg();
}
public static void FindPath()
{
Console.WriteLine($"FindPath {Thread.CurrentThread.ManagedThreadId}");
}
public static void SendMsg()
{
Console.WriteLine($"SendMsg: {Thread.CurrentThread.ManagedThreadId}");
}
}
Task.Factory.StartNew实现线程的调度。
Task本身不会执行任何代码,需要使用线程来执行Task的代码。Task.Factory.StartNew方法中可以传入一个TaskScheduler对象来调度Task。可以直接在当前线程执行task,也可以从线程池中分配线程执行。
public class Program1_7
{
public static void Main1_7()
{
//设置主线程
Game.ThreadSynchronizationContext.Post(()=>{Console.WriteLine($"Main thread {Thread.CurrentThread.ManagedThreadId}");});
StartTask();
while (true)
{
Thread.Sleep(1);
Game.ThreadSynchronizationContext.Update();
}
}
public static async void StartTask()
{
await Task.Factory.StartNew(FindPath, new CancellationToken(), TaskCreationOptions.None, MyTaskSchedule.Instance);
SendMsg(); //此时依然会同步到主线程
}
class MyTaskSchedule : TaskScheduler
{
public static MyTaskSchedule Instance { get; } = new MyTaskSchedule();
protected override IEnumerable<Task> GetScheduledTasks()
{
return null;
}
//调度task(执行StartNew时会调用此方法)
protected override void QueueTask(Task task)
{
// TryExecuteTask(task); //直接在主线程执行task
//线程池的调度
ThreadPool.QueueUserWorkItem((_) => TryExecuteTask(task));
}
protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued)
{
throw new NotImplementedException();
}
}
public static void FindPath()
{
Console.WriteLine($"FindPath {Thread.CurrentThread.ManagedThreadId}");
}
public static void SendMsg()
{
Console.WriteLine($"SendMsg: {Thread.CurrentThread.ManagedThreadId}");
}
}
参考链接
[1] C# 异步编程TaskScheduler
[2] 网络游戏架构设计 by 熊猫