Я почти уверен, что этот вопрос уже задавался раньше, но, вероятно, не так, как я собираюсь его задать.
Я пытаюсь создать простой метод под названием DAwaiter
, который ожидает, пока определенная общедоступная переменная bool не станет истинной другим потоком или событием. Я не пытаюсь передать новую копию переменной в метод, я пытаюсь постоянно проверять значение конкретной, уже созданной переменной.
Я мог бы решить эту проблему, создав для каждой задачи, например домашней задачи, отдельную копию DAwaiter
, но я ленив и не хочу обновлять каждую команду отдельно, если я могу это сделать.
Я пробовал использовать ref bool waiter
, но не мог понять, как его эффективно использовать. Я тоже пробовал использовать object waiter
и не знаю, почему это не работает.
Вот мой пример:
public class MyForm : Form
{
ArduinoComms AConnection = new ArduinoComms();
private void homeButton_Click(object sender, EventArgs e)
{
Task.Run(AConnection.Home);
}
}
public class ArduinoComms
{
public SerialPort Port = new SerialPort(/*parameters here*/); //creates and instances an internal serial port.
Port.DataReceived = new SerialDataReceivedEventHandler(Port_ReceivedData);
public bool XDone, YDone, Homed, Ready, Stopped, Locked = false; //initializes a lot of bools
string NewDataContent = "Default newDataContent - should be inaccessible. If you see this, an error has occurred.";
public void Home()
{
Homed = false;
Ready = false;
XDone = false;
YDone = false;
Logger("Beginning home");
//WriteMyCommand(2); sends command to arduino.
if (!DAwaiter(Homed, true, 250)) { return; }
Logger("finished home, beginning backoff");
XDone = false;
YDone = false;
//WriteMyCommand(0, backoff);
//WriteMyCommand(1, backoff);
if (!DAwaiter(XDone && YDone, true, 250)) { return; }
Logger("Finished home");
Ready = true;
}
internal bool DAwaiter(object waiter, bool expectedValue, int delay)
{
bool waiterVal = (bool)waiter;
CancellationToken ct = cts.Token;
Logger($"{Homed}");
Logger($"Beginning DAwaiter");
while (waiterVal != expectedValue)
{
Logger($"awaiting... {waiterVal}");
Thread.Sleep(delay);
if (ct.IsCancellationRequested)
{
Logger($"Cancelled DAwaiter.");
return false;
}
waiterVal = waiter;
}
Logger($"DAwaiter finished true.");
return true;
}
private void port_DataReceived(object sender, SerialDataReceivedEventArgs e)
{
SerialPort spL = (SerialPort)sender; //instances an internal object w/ same type as the event's sender.
byte[] buf = new byte[spL.BytesToRead]; //instantiates a buffer of appropriate length.
spL.Read(buf, 0, buf.Length); //reads from the sender, which inherits the data from the sender, which *is* our serial port.
NewDataContent = $"{System.Text.Encoding.ASCII.GetString(buf)}"; //assembles the byte array into a string.
Logger($"Received: {NewDataContent}"); //prints the result for debug.
string[] thingsToParse = NewDataContent.Split('\n'); //splits the string into an array along the newline in case multiple responses are sent in the same message.
foreach (string thing in thingsToParse) //checks each newline instance individually.
{
switch (thing)
{
case string c when c.Contains("Home done"): //checks incoming data for the arduino's report phrase "Home done" when it is homed.
Homed = true;
Logger($"Homed {Homed}");
break;
default: break; //do nothing
}
}
}
public void Logger(string message)
{
//log the message.
}
}
В этом примере я использую DAwaiter
, чтобы дождаться, пока Arduino на другом конце последовательного порта ответит домашним сигналом. Он регистрирует этот «домашний» сигнал, поворачивая общедоступную переменную Homed = true
. Однако метод DAwaiter передает только начальное значение Homed
, что означает, что он никогда не перестанет ожидать.
Есть ли способ заставить эту переменную передаваться так, чтобы она постоянно проверяла, есть ли Homed == true
в операторе if в DAwaiter
?
Можно ли это расширить до нескольких переменных, например, условия, когда я жду Axis1 && Axis2 == true
?
Любой совет очень ценится.
Огромное вам всем спасибо за оперативную помощь, вы замечательные люди. Если возникнут еще проблемы, я попрошу еще помощи. В противном случае я закрываю вопрос.
Ваша проблема, как упоминал «Иван Петров», заключается в том, что вы передаете значение bool как объект, а затем приводите его к значению bool. Когда вы выполняете эту операцию упаковки, передаваемый вами объект преобразуется из ссылочного типа в тип значения. Другая проблема в вашей реализации заключается в том, что даже если вам удастся передать значение как ссылочный тип, используя только объект, используемый механизм совершенно небезопасен. Вышеупомянутый факт вызван тем, что несколько потоков читают и записывают данные из и в один и тот же объект, что может вызвать состояние гонки. Состояние гонки — это исключение уровня ОС, при котором к адресу памяти в оперативной памяти одновременно обращаются два или более потоков, выполняющих операцию записи. Это может привести к повреждению данных в оперативной памяти.
Решение состоит в том, чтобы передать значение bool в качестве ссылочного значения с помощью ключевого слова ref. Ключевое слово ref не изменит тип объекта bool, а вместо этого передаст указатель на значение в стеке. Это потокобезопасно, поскольку адрес переменной в памяти передается между потоками. Если вы пытаетесь передать объекты, выделенные в куче, между потоками, рекомендуется использовать оператор lock
, чтобы зафиксировать адрес, выделенный в куче, в стеке до тех пор, пока поток не завершит свою процедуру с объектом.
public static void Main(){
string thread_name = String.Empty;
bool value = false;
// Create and start 10 threads
for(int i = 0; i < 10; i++)
new Thread(()=>{ThreadTask(ref thread_name, ref value);}).Start();
// On the original thread 'lock' the string value and read both the bool and string value
while(true){
lock(thread_name){
Console.WriteLine(thread_name);
Console.WriteLine($"{value}\n\n");
Thread.Sleep(500);
}
}
}
public static void ThreadTask(ref string thread_name, ref bool value){
while(true){
lock(thread_name){
thread_name = $"Thread Id: {Thread.CurrentThread.ManagedThreadId}";
value = !value;
}
}
}
Глядя на цель вашего кода, вы, похоже, используете DAwaiter
как своего рода прокрутку собственного объекта синхронизации. Возможно, проще использовать один из готовых объектов синхронизации, например SemaphoreSlim.
Я создал симуляцию/макет того, как можно преобразовать логические значения в семафоры и ожидать их асинхронно. (Поскольку у вас есть MyForm : Form
, как можно было бы предположить, я сделал пример в Winforms, но фактический код семафора переносим).
public partial class MyForm : Form
{
public MyForm()
{
InitializeComponent();
ArduinoComms = new ArduinoComms();
ArduinoComms.Log += (sender, e) =>
{
richTextBox.AppendText($@"{DateTime.Now:hh\:mm\:ss\.ffff}: {e.Message}{Environment.NewLine}");
richTextBox.SelectionStart = richTextBox.Text.Length;
richTextBox.ScrollToCaret();
};
buttonHome.Click += async (sender, e) =>
{
UseWaitCursor = true;
buttonHome.Enabled = false;
await ArduinoComms.Home();
buttonHome.Enabled = true;
UseWaitCursor = false;
// Cursor may need to be "nudged" to redraw
Cursor.Position = new Point(Cursor.Position.X + 1, 0);
};
}
ArduinoComms ArduinoComms { get; }
}
{
#region S I M
class MockSerialPort
{
public MockSerialPort(byte[] simBuffer) => SimBuffer = simBuffer;
public byte[] SimBuffer { get; }
};
#endregion S I M
public ArduinoComms()
{
Port.DataReceived += Port_DataReceived;
}
public SerialPort Port = new SerialPort(/*parameters here*/); //creates and instances an internal serial port.
public SemaphoreSlim XDone = new SemaphoreSlim(1, 1);
public SemaphoreSlim YDone = new SemaphoreSlim(1, 1);
public SemaphoreSlim Homed = new SemaphoreSlim(1, 1);
public SemaphoreSlim Ready = new SemaphoreSlim(1, 1);
public SemaphoreSlim Stopped = new SemaphoreSlim(1, 1);
public SemaphoreSlim Locked = new SemaphoreSlim(1, 1);
string NewDataContent = "Default newDataContent - should be inaccessible. If you see this, an error has occurred.";
public async Task Home()
{
Logger($"Beginning home");
try
{
// Await for any previous calls to clear
await Homed.WaitAsync(timeout: TimeSpan.FromSeconds(10));
// Send command to arduino as Fire and Forget.
_ = MockWriteMyCommand(2);
await Homed.WaitAsync();
}
finally
{
Homed.Release();
}
Logger("finished home, beginning backoff");
try
{
await XDone.WaitAsync(timeout: TimeSpan.FromSeconds(10));
_ = MockWriteMyCommand(0, backoff: true);
await XDone.WaitAsync();
}
finally
{
XDone.Release();
}
try
{
await YDone.WaitAsync(timeout: TimeSpan.FromSeconds(10));
_ = MockWriteMyCommand(1, backoff: true);
await YDone.WaitAsync();
}
finally
{
YDone.Release();
}
Logger($"Finished home{Environment.NewLine}");
}
Random _rando = new Random(Seed: 1); // Seed is for repeatability during testing
private async Task MockWriteMyCommand(int cmd, bool? backoff = null)
{
switch (cmd)
{
case 0:
await Task.Delay(TimeSpan.FromSeconds(_rando.Next(1, 4)));
Port_DataReceived(new MockSerialPort(System.Text.Encoding.ASCII.GetBytes($"XDone backoff = {backoff}")), default);
break;
case 1:
await Task.Delay(TimeSpan.FromSeconds(_rando.Next(1, 4)));
Port_DataReceived(new MockSerialPort(System.Text.Encoding.ASCII.GetBytes($"YDone backoff = {backoff}")), default);
break;
case 2:
await Task.Delay(TimeSpan.FromSeconds(_rando.Next(1, 4)));
Port_DataReceived(new MockSerialPort(System.Text.Encoding.ASCII.GetBytes("Home done")), default);
break;
default:
Debug.Fail("Unrecognized command");
break;
}
}
private void Port_DataReceived(object sender, SerialDataReceivedEventArgs e)
{
byte[] buf;
switch (sender?.GetType().Name)
{
case nameof(SerialPort):
var spL = (SerialPort)sender;
buf = new byte[spL.BytesToRead]; //instantiates a buffer of appropriate length.
spL.Read(buf, 0, buf.Length); //reads from the sender, which inherits the data from the sender, which *is* our serial port.
break;
case nameof(MockSerialPort):
var mspL = (MockSerialPort)sender;
buf = mspL.SimBuffer;
break;
default: throw new NotImplementedException();
}
NewDataContent = $"{System.Text.Encoding.ASCII.GetString(buf)}"; //assembles the byte array into a string.
Logger($"Received: {NewDataContent}"); //prints the result for debug.
string[] thingsToParse = NewDataContent.Split('\n'); //splits the string into an array along the newline in case multiple responses are sent in the same message.
foreach (string thing in thingsToParse) //checks each newline instance individually.
{
switch (thing)
{
case string c when c.Contains("Home done"): //checks incoming data for the arduino's report phrase "Home done" when it is homed.
Homed.Release();
Logger($"Homed");
break;
case string c when c.Contains("XDone"):
XDone.Release();
Logger($"XDone");
break;
case string c when c.Contains("YDone"):
YDone.Release();
Logger($"YDone");
break;
default: break; //do nothing
}
}
}
public event EventHandler<LoggerMessageArgs> Log;
public void Logger(string message) => Log?.Invoke(this, new LoggerMessageArgs(message));
}
public class LoggerMessageArgs
{
public LoggerMessageArgs(string message) => Message = message;
public string Message { get; }
}
Вот макет кода, с помощью которого я тестировал этот ответ: Клон
Большое спасибо. Я уже несколько месяцев спотыкаюсь об этой проблеме «как дождаться ввода Arduino», и мне наконец объяснили, как использовать семафор. Вы не только ответили на этот вопрос, но и ответили на мои предыдущие 4. Глядя на ваше решение, я понимаю, что мне еще многому предстоит научиться. ТИСМ.
Я только что запустил ваш код. Всем, кто планирует опробовать этот пример, необходимо преобразовать любые новые операторы целевого типа в обычные операторы для C# 9.0+. Т.е. public SemaphoreSlim SlimShady = new(1,1);
-> public SemaphoreSlim SlimShadier = new SemaphoreSlim(1,1);
.
Ой черт. Это доставило вам неприятности? Легко исправить! Я изменил публикацию и репозиторий, чтобы вы могли получить новый коммит, если хотите.
Ненавижу просить большего, но у меня есть еще один вопрос. Я заметил, что во время вашей имитации возврата в исходное положение вы перемещаете одну ось за раз. Я не прошу вас писать какой-то новый код, но можно ли с помощью этой структуры перемещать несколько осей одновременно? (Т.е. иметь «ожидание XDone» и «ожидание YDone» одновременно?) Я предполагаю, что это будет сделано путем разделения движения каждой оси на отдельную задачу, которая будет запускаться в конце домашней задачи. . Спасибо.
Ничего страшного, я тоже об этом думал! Итак, дело в том, что он опирается на ваш встроенный код в Arduino. Вы создаете потоки в коде C таким образом, что сначала подталкиваете X, а затем подталкиваете Y, пока не найдете цель? Для этого потребуется один подход (по сути, установка всех семафоров и выполнение асинхронного WaitAll())
). Но я бы сделал это с помощью одного Seek(int X, int Y, bool homeFirst)
метода в коде Arduino C, который делает это без многопоточности и записывает это ack
в последовательный порт, когда он заканчивается.
Давайте продолжим обсуждение в чате.
object waiter
упакует переменную bool, поэтому она теперь не связана с исходным полем bool, поскольку является копией...