我目前正在使用一个c#应用程序,它将作为多人游戏的服务器端工作,我有点不确定我应该如何处理多线程问题。在我继续之前,可能值得一提的是我对这个话题很陌生。
问题
服务器端应用程序的一个要求是它应该包含特定于应用程序的数据,例如已连接到服务器的对等方的信息,以及它们的位置等。问题是如果没有某种形式的线程安全机制,两个请求可能会读取和写入同一条数据,这显然是个问题。
解决问题
到目前为止,为了解决这个问题,我只是简单地将每个请求包装在一个锁定块内,确保每个请求都按顺序发生,这样数据一次只能被一个对等体操纵。 / p>
最近,在对该主题进行了一些研究之后,我了解了fibers的概念,以及一种设置“光纤池”的方法,允许将操作排队到单根光纤上的另一种方式试图确保请求按顺序发生。
问题
我对线程和这些类型主题的了解非常有限。我很想知道更多关于这个话题的内容,特别是我想知道任何一个解决方案的优缺点,最终我应该采取哪种方式。
非常感谢任何帮助。
答案 0 :(得分:1)
我真的无法弄清楚光纤如何解决您的问题,因为它们基本上没有提供减少共享内存资源争用的方法。
我宁愿专注于减少资源上的争用,减少重复计算以及通过异步处理减少线程资源使用的策略。
在所有请求处理之上使用全局锁定基本上减少了对单个活动线程的所有处理。作为替代方案,您可以尝试仅使用每个资源的锁来减少锁定。
Disclamer:此处提供的示例代码绝不是生产质量,它仅用于说明概念。
减少争用
当您仅针对特定操作锁定某些相关数据区域时,您可以采用粒度锁定策略。
以下是一个排序游戏示例,它定义了简单的规则: 如果左侧项目小于右侧,则每个玩家抓取列表中的项目,并将其与下一个项目交换。 所有项目都排序后游戏结束。 没有人会赢,只是很有趣。
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核心一样多的线程。
考虑使您的应用程序异步,而不仅仅是多线程。