【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 熊猫

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值