独占锁与线程纤程

Exclusive lock versus thread fiber

我目前正在开发一个 c# 应用程序,该应用程序将用作多人游戏的服务器端,但我不太确定应该如何处理多线程问题。在我继续之前,可能值得一提的是我对这个话题还很陌生。

问题

服务器端应用程序的要求之一是它应该包含特定于应用程序的数据,例如已连接到服务器的对等点的信息,以及它们的位置等。问题是,如果没有某种形式的线程安全机制,两个请求可能会同时读取和写入同一块数据,这显然是有问题的。

解决问题

到目前为止,为了解决这个问题,我只是将每个请求包装在一个锁块中,确保每个请求都按顺序发生,这样数据一次只能由一个对等方操作.

最近,在对该主题进行一些研究后,我了解到 fibers 的想法,以及一种设置 "fiber pool" 的方法,允许将操作排队到单个光纤上作为尝试确保请求按顺序发生的另一种方式。

问题

我对线程和这些类型的主题的了解非常有限。我很想知道更多关于这个话题的信息,特别是我很想知道这两种解决方案的优缺点,以及我最终应该采取哪条路线。

如有任何帮助,我们将不胜感激。

我真的不知道纤程如何解决您的问题,因为它们基本上不提供减少共享内存资源争用的方法。

我宁愿专注于减少资源争用、减少重复计算和减少线程的策略异步处理的资源 使用情况。

在所有请求处理之上使用全局锁基本上将所有处理减少到单个活动线程。作为替代方案,您可以尝试仅针对每个资源使用锁来减少锁定。

免责声明:这里提供的示例代码绝不是生产质量,它只是为了说明概念。

减少争用

当您针对特定操作只锁定某些有问题的数据区域时,您可以想出一个粒度锁定策略。

以下是一个排序游戏示例,它定义了简单的规则: 每个玩家抓取列表中的一个项目,如果左边的项目少于右边的项目,则将其与下一个交换。 当所有项目都排序后,游戏结束。 没有人赢,只是很有趣。

using System;
using System.Threading;
using System.Collections.Generic;
using System.Linq;

public class Program
{
    public static void Main()
    {
        var game = new SortingGame();
        var random = new Random(234);

        // Simulate few concurrent players.
        for (var i = 0; i < 3; i++)
        {
            ThreadPool.QueueUserWorkItem(o =>
            {
                while (!game.IsSorted())
                {
                    var x = random.Next(game.Count() - 1);
                    game.PlayAt(x);
                    DumpGame(game);
                };
            });
        }

        Thread.Sleep(4000);

        DumpGame(game); 
    }

    static void DumpGame(SortingGame game)
    {
        var items = game.GetBoardSnapshot();

        Console.WriteLine(string.Join(",", items));
    }
}


class SortingGame
{
    List<int> items;
    List<object> lockers;

    // this lock is taken for the entire board to guard from inconsistent reads.
    object entireBoardLock = new object();

    public SortingGame()
    {
        const int N = 10;

        // Initialize a game with items in random order
        var random = new Random(1235678);
        var setup = Enumerable.Range(0, N).Select(i => new { x = i, position = random.Next(0, 100)}).ToList();
        items = setup.OrderBy(i => i.position).Select(i => i.x).ToList();
        lockers = Enumerable.Range(0, N).Select(i => new object()).ToList();
    }

    public int Count()
    {
        return items.Count;
    }

    public bool IsSorted()
    {
        var currentBoard = GetBoardSnapshot();
        var pairs = currentBoard.Zip(currentBoard.Skip(1), (a, b) => new { a, b});
        return pairs.All(p => p.a <= p.b);
    }

    public IEnumerable<int> GetBoardSnapshot()
    {
        lock (entireBoardLock)
            return new List<int>(items);
    }

    public void PlayAt(int x)
    {
        // Find the resource lockers for the two adjacent cells in question
        var locker1 = GetLockForCell(x);
        var locker2 = GetLockForCell(x + 1);

        // It's important to lock the resources in a particular order, same for all the contending writers and readers.
        // These can last for a long time, but are granular,
        // so the contention is greatly reduced.
        // Try to remove one of the following locks, and notice the duplicate items in the result
        lock (locker1)
        lock (locker2)
            {
                var a = items[x];
                var b = items[x + 1];
                if (a > b)
                {
                    // Simulate expensive computation
                    Thread.Sleep(100);
                    // Following is a lock to protect from incorrect game state read
                    // The lock lasts for a very short time.
                    lock (entireBoardLock)
                    {
                        items[x] = b;
                        items[x + 1] = a;
                    }
                }           
            }
    }

    object GetLockForCell(int x)
    {
        return lockers[x];
    }
}

消除重复计算

如果您需要一些昂贵的计算来保持最新,但又不依赖于特定的请求,那么尝试为每个请求计算它只会浪费资源。

如果已经为另一个请求开始计算,则以下方法允许跳过重复计算。

它与缓存不同,因为您实际上可以通过这种方式在时间范围内获得可能的最佳计算结果:

void Main()
{
    for (var i = 0; i < 100; i++)
    {
        Thread.Sleep(100);
        var j = i;
        ThreadPool.QueueUserWorkItem((o) => {
            // In this example, the call is blocking becase of the Result property access.
            // In a real async method you would be awaiting the result.
            var result = computation.Get().Result;

            Console.WriteLine("{0} {1}", j, result);
        });
    }
}

static ParticularSharedComputation computation = new ParticularSharedComputation();

abstract class SharedComputation
{
    volatile Task<string> currentWork;
    object resourceLock = new object();
    public async Task<string> Get()
    {
        Task<string> current;
        // We are taking a lock here, but all the operations inside a lock are instant.
        // Actually we are just scheduling a task to run.
        lock (resourceLock)
        {
            if (currentWork == null)
            {
                Console.WriteLine("Looks like we have to do the job...");
                currentWork = Compute();
                currentWork.ContinueWith(t => {
                    lock (resourceLock)
                        currentWork = null;
                });
            }
            else
                Console.WriteLine("Someone is already computing. Ok, will wait a bit...");
            current = currentWork;
        }

        return await current;
    }

    protected abstract Task<string> Compute();
}

class ParticularSharedComputation : SharedComputation
{
    protected override async Task<string> Compute()
    {
        // This method is thread safe if it accesses only it's instance data,
        // as the base class allows only one simultaneous entrance for each instance.
        // Here you can safely access any data, local for the instance of this class.
        Console.WriteLine("Computing...");

        // Simulate a long computation.
        await Task.Delay(2000);

        Console.WriteLine("Computed.");
        return DateTime.Now.ToString();
    }
}

走向异步,而不仅仅是多线程

即使您正在执行多线程,也可能会浪费线程资源,并且由于为每个线程分配的堆栈内存以及上下文切换,线程实际上很昂贵。

一个设计良好的异步应用程序实际上会使用与系统中 CPU 个内核一样多的线程。

考虑让您的应用程序异步,而不仅仅是多线程。