Многопоточность в С#
Есть класс Bank. Нужно построить класс так чтобы при изменении одного из его свойств создавался поток, который запишет данные о свойствах в текстовый файл. Класс должен инкапсулировать в себе всю логику многопоточности. Помогите дальше сделать.
class Bank
{
private int money;
private string name;
private int percent;
public event PropertyChangedEventHandler Changed;
public int Money
{
get { return money; }
set
{
money = value;
Update();
}
}
public string Name
{
get { return name; }
set
{
name = value;
Update();
}
}
public int Percent
{
get { return percent; }
set
{
percent = value;
Update();
}
}
protected virtual void Update([CallerMemberName] string names = null)
{
PropertyChangedEventHandler handler = Changed;
if (handler != null) handler(this, new PropertyChangedEventArgs(names));
}
}
class Program
{
static void Main(string[] args)
{
}
}
Ответы (2 шт):
Если вам нужно правильно реализовать PropertyChanged, то не надо выдумывать велосипедов, а просто использовать интерфейс INotifyPropertyChanged.
public class Bank : INotifyPropertyChanged
{
private int money;
private string name;
private int percent;
public event PropertyChangedEventHandler PropertyChanged;
public int Money
{
get { return money; }
set
{
money = value;
OnPropertyChanged();
}
}
public string Name
{
get { return name; }
set
{
name = value;
OnPropertyChanged();
}
}
public int Percent
{
get { return percent; }
set
{
percent = value;
OnPropertyChanged();
}
}
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
Но ваше задание вообще не про это.
Как вы указали, вам требуется реализовать сохранение данных класса в файл с использованием стороннего потока каждый раз, когда данные поменялись. На помощь приходит шаблон проектирования Producer/Consumer, BlockingCollection и ReaderWriterLock.
Насмотря на на принцип SRP из SOLID, который говорит нам, что класс должен делать что-то одно, я реализовал вам пример монстра, который может всё, PropertyChanged тоже оставил. По хорошему, класс с данными должен выглядеть как тот, что я выше показал, а все остальные должны быть реализованы в других внешних классах.
public class Bank : INotifyPropertyChanged, IDisposable
{
private static readonly JsonSerializerOptions options = new JsonSerializerOptions { WriteIndented = true, Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping };
private readonly BlockingCollection<string> queue;
private readonly ReaderWriterLock locker;
private readonly string fileName;
private Thread writerThread;
private int money;
private string name;
private int percent;
public event PropertyChangedEventHandler PropertyChanged;
public Bank(string fileName)
{
this.fileName = fileName;
queue = new BlockingCollection<string>();
locker = new ReaderWriterLock();
writerThread = new Thread(WriterLoop);
writerThread.IsBackground = true;
writerThread.Start();
}
public int Money
{
get
{
locker.AcquireReaderLock(Timeout.InfiniteTimeSpan);
try
{
return money;
}
finally
{
locker.ReleaseReaderLock();
}
}
set
{
CheckDisposed();
locker.AcquireWriterLock(Timeout.InfiniteTimeSpan);
money = value;
locker.ReleaseWriterLock();
OnPropertyChanged();
}
}
public string Name
{
get
{
locker.AcquireReaderLock(Timeout.InfiniteTimeSpan);
try
{
return name;
}
finally
{
locker.ReleaseReaderLock();
}
}
set
{
CheckDisposed();
locker.AcquireWriterLock(Timeout.InfiniteTimeSpan);
name = value;
locker.ReleaseWriterLock();
OnPropertyChanged();
}
}
public int Percent
{
get
{
locker.AcquireReaderLock(Timeout.InfiniteTimeSpan);
try
{
return percent;
}
finally
{
locker.ReleaseReaderLock();
}
}
set
{
CheckDisposed();
locker.AcquireWriterLock(Timeout.InfiniteTimeSpan);
percent = value;
locker.ReleaseWriterLock();
OnPropertyChanged();
}
}
private void WriterLoop()
{
foreach(string json in queue.GetConsumingEnumerable())
{
File.WriteAllText(fileName, json);
}
}
private void SaveData()
{
locker.AcquireReaderLock(Timeout.InfiniteTimeSpan);
try
{
queue.Add(JsonSerializer.Serialize(this, options));
}
finally
{
locker.ReleaseReaderLock();
}
}
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
SaveData();
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
private void CheckDisposed()
{
if (disposed)
throw new ObjectDisposedException(nameof(Bank));
}
bool disposed;
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (disposing)
CheckDisposed();
if (!disposed)
{
queue.CompleteAdding();
PropertyChanged = null;
writerThread.Join();
disposed = true;
}
}
~Bank() => Dispose(false);
}
Почему IDisposable, потому что поток надо остановить, когда мы перестаем классом пользоваться, иначе он будет выполняться до тех пор, пока приложение не будет закрыто. Можно было и обычный метод public void Stop(), но тогда бы в случае, если вы забыли задиспозить класс, он бы продолжал при изменениях свойств писать в файл.
Потестируем и запись и INPC
static void Main(string[] args)
{
string fileName = "bank.json";
using (Bank bank = new Bank(fileName))
{
bank.PropertyChanged += (s, e) => Console.WriteLine($"Value '{e.PropertyName}' was changed to '{typeof(Bank).GetProperty(e.PropertyName).GetValue(bank)}'");
bank.Name = "Bank 1";
bank.Percent = 5;
bank.Money = 100;
}
Console.WriteLine(File.ReadAllText(fileName));
}
Вывод в консоль
Value 'Name' was changed to 'Bank 1'
Value 'Percent' was changed to '5'
Value 'Money' was changed to '100'
{
"Money": 100,
"Name": "Bank 1",
"Percent": 5
}
В итоге решение выглядит весьма монструозно и меня здесь могут побить за нарушение SOLID, но я предлагаю лишее из класса вам выкинуть самостоятельно, например INotifyPropertyChanged.
Кстати, по поводу использования файла, ошибку конфликта доступа к файлу легко воспроизвести: просто закомментируйте строчку writerThread.Join();, и приложение упадет с исключением, говорящим что невозможно прочитать файл, который уже используется. А все потому что оно в этом случае не будет ждать завершения потока, записывающего в файл.
Консоль:
class Bank
{
private int money;
private string name;
private int percent;
delegate void UpRead();
event UpRead update;
public Bank(string name, int money, int percent)
{
this.money = money;
this.name = name;
this.percent = percent;
update += Threading;
}
public int Money
{
get { return money; }
set
{
money = value;
update();
}
}
public string Name
{
get { return name; }
set
{
name = value;
update();
}
}
public int Percent
{
get { return percent; }
set
{
percent = value;
update();
}
}
protected void Threading()
{
Thread thread = new Thread(new ThreadStart(Save));
thread.Start();
}
protected void Save()
{
string s = name + " " + money.ToString() + " " + Percent.ToString();
File.WriteAllText("date.txt", s);
Console.WriteLine("Saved to file date.txt");
}
}