Передача исходных общедоступных переменных в качестве параметров метода, которые необходимо постоянно проверять

Я почти уверен, что этот вопрос уже задавался раньше, но, вероятно, не так, как я собираюсь его задать.

Я пытаюсь создать простой метод под названием 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, что означает, что он никогда не перестанет ожидать.

  1. Есть ли способ заставить эту переменную передаваться так, чтобы она постоянно проверяла, есть ли Homed == true в операторе if в DAwaiter ?

  2. Можно ли это расширить до нескольких переменных, например, условия, когда я жду Axis1 && Axis2 == true?

Любой совет очень ценится.

object waiter упакует переменную bool, поэтому она теперь не связана с исходным полем bool, поскольку является копией...
Ivan Petrov 29.08.2024 01:41

Огромное вам всем спасибо за оперативную помощь, вы замечательные люди. Если возникнут еще проблемы, я попрошу еще помощи. В противном случае я закрываю вопрос.

Cameron Hadland 29.08.2024 20:33
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
2
50
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Анализ

Ваша проблема, как упоминал «Иван Петров», заключается в том, что вы передаете значение 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; }
}

ArduinoComms Sim с семафорами

{
    #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; }
}

Вот макет кода, с помощью которого я тестировал этот ответ: Клон

IV. 29.08.2024 04:18

Большое спасибо. Я уже несколько месяцев спотыкаюсь об этой проблеме «как дождаться ввода Arduino», и мне наконец объяснили, как использовать семафор. Вы не только ответили на этот вопрос, но и ответили на мои предыдущие 4. Глядя на ваше решение, я понимаю, что мне еще многому предстоит научиться. ТИСМ.

Cameron Hadland 29.08.2024 20:37

Я только что запустил ваш код. Всем, кто планирует опробовать этот пример, необходимо преобразовать любые новые операторы целевого типа в обычные операторы для C# 9.0+. Т.е. public SemaphoreSlim SlimShady = new(1,1); -> public SemaphoreSlim SlimShadier = new SemaphoreSlim(1,1);.

Cameron Hadland 29.08.2024 21:16

Ой черт. Это доставило вам неприятности? Легко исправить! Я изменил публикацию и репозиторий, чтобы вы могли получить новый коммит, если хотите.

IV. 29.08.2024 21:28

Ненавижу просить большего, но у меня есть еще один вопрос. Я заметил, что во время вашей имитации возврата в исходное положение вы перемещаете одну ось за раз. Я не прошу вас писать какой-то новый код, но можно ли с помощью этой структуры перемещать несколько осей одновременно? (Т.е. иметь «ожидание XDone» и «ожидание YDone» одновременно?) Я предполагаю, что это будет сделано путем разделения движения каждой оси на отдельную задачу, которая будет запускаться в конце домашней задачи. . Спасибо.

Cameron Hadland 29.08.2024 21:41

Ничего страшного, я тоже об этом думал! Итак, дело в том, что он опирается на ваш встроенный код в Arduino. Вы создаете потоки в коде C таким образом, что сначала подталкиваете X, а затем подталкиваете Y, пока не найдете цель? Для этого потребуется один подход (по сути, установка всех семафоров и выполнение асинхронного WaitAll())). Но я бы сделал это с помощью одного Seek(int X, int Y, bool homeFirst) метода в коде Arduino C, который делает это без многопоточности и записывает это ack в последовательный порт, когда он заканчивается.

IV. 29.08.2024 21:49

Давайте продолжим обсуждение в чате.

Cameron Hadland 29.08.2024 21:55

Другие вопросы по теме