Я действительно не могу понять, как волокна решат вашу проблему, поскольку они в основном не предоставляют средств для уменьшения конкуренции за общий ресурс памяти.
Я бы предпочел сосредоточиться на стратегиях уменьшения конкуренции за ресурсы, сокращения дублированных вычислений и сокращения использования ресурсов потоков при асинхронной обработке.
Использование глобальной блокировки поверх всей обработки запроса, по сути, сводит всю обработку к одному живому потоку. В качестве альтернативы вы можете попытаться уменьшить блокировку, используя блокировки только для каждого ресурса.
Отказ от ответственности: представленный здесь пример кода ни в коем случае не является производственным качеством, он здесь только для иллюстрации концепций.
Уменьшить конкуренцию
Вы можете придумать гранулированную стратегию блокировки, когда вы блокируете только некоторую область данных, о которых идет речь, для конкретной операции.
Ниже приведен пример игры на сортировку, в которой определены простые правила: каждый игрок берет элемент из списка и меняет его местами со следующим, если левый элемент меньше правого. Игра заканчивается, когда все предметы рассортированы. Никто не выигрывает, просто очень весело.
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();
}
}
Используйте асинхронный режим, а не только многопоточность
Даже если вы используете многопоточность, вы можете тратить ресурсы потока впустую, а потоки на самом деле дороги из-за памяти стека, выделенной для каждого потока, и из-за переключения контекста.
Хорошо спроектированное асинхронное приложение будет фактически использовать столько потоков, сколько ядер ЦП в вашей системе.
Подумайте о том, чтобы сделать ваше приложение асинхронным, а не только многопоточным.
person
George Polevoy
schedule
23.09.2015