loltrol
loltrol

Reputation: 103

Synchronized procedures execution order

For the last few days I have been unsuccessfully trying to understand the behavior of Delphi and its component for working with BLE. I'm not sure the problem is related to bluetooth or winrt, maybe i'm just missing something fundamental about delphi and multithreading.

In my application i create an instance of TBluetoothLE, assign events for device discovery (OnDiscoverLEDevice) and discovery end (OnEndDiscoverDevices). Then i start discovery with BluetoothLE.DiscoverDevices(10000). As i understand, i can rely on that events are executed in main thread (and they really are, as i can see in System.Bluetooth.Components.pas):

procedure TBluetoothLE.DoOnDiscoverLEDevice(const Sender: TObject; const ADevice: TBluetoothLEDevice; Rssi: Integer; const ScanResponse: TScanResponse);
begin
  if FEnabled and Assigned(FOnDiscoverLEDevice) then
    TThread.Synchronize(nil, procedure begin
      if FEnabled and Assigned(FOnDiscoverLEDevice) then
          FOnDiscoverLEDevice(Sender, ADevice, Rssi, ScanResponse);
    end);
end;

procedure TBluetoothLE.DoOnEndDiscoverDevices(const Sender: TObject; const ADeviceList: TBluetoothLEDeviceList);
begin
  if FEnabled and Assigned(FOnEndDiscoverDevices) then
    TThread.Synchronize(nil, procedure begin
      if FEnabled and Assigned(FOnEndDiscoverDevices) then
          FOnEndDiscoverDevices(Sender, ADeviceList);
    end);
end;

So, when i write code of handlers for discovery and its end, i can be sure that they will be executed only when application calls CheckSynchronize somewhere in its main loop (or immediately, if TThread.Synchronize is called in main thread). And handler execution can never be interrupted: when the programm entered my handler, it will safely (for the sake of visual components and application integrity) execute every line of the handler code.

But i stumbled across very strange behaviour, as if handlers execution is mixed up.

minimum, reproducible example

object MainForm: TMainForm
  Left = 0
  Top = 0
  Caption = 'BLE Thread Issue'
  ClientHeight = 316
  ClientWidth = 444
  Position = ScreenCenter
  FormFactor.Width = 320
  FormFactor.Height = 480
  FormFactor.Devices = [Desktop]
  OnCreate = FormCreate
  DesignerMasterStyle = 0
  object StartButton: TButton
    Align = Top
    Size.Width = 444.000000000000000000
    Size.Height = 65.000000000000000000
    Size.PlatformDefault = False
    TabOrder = 1
    Text = 'StartButton'
    OnClick = StartButtonClick
  end
  object Memo: TMemo
    Touch.InteractiveGestures = [Pan, LongTap, DoubleTap]
    DataDetectorTypes = []
    Align = Client
    Size.Width = 444.000000000000000000
    Size.Height = 251.000000000000000000
    Size.PlatformDefault = False
    TabOrder = 2
    Viewport.Width = 440.000000000000000000
    Viewport.Height = 247.000000000000000000
  end
  object BluetoothLE: TBluetoothLE
    Enabled = True
    OnDiscoverLEDevice = DeviceDiscovered
    OnEndDiscoverDevices = DiscoveryFinished
    Left = 24
    Top = 8
  end
end
unit MainUnit;

interface

uses
  System.SysUtils, System.Types, System.UITypes, System.Classes, System.Variants,
  System.Generics.Collections, System.RegularExpressions,
  FMX.Types, FMX.Controls, FMX.Forms, FMX.Graphics, FMX.Dialogs,
  System.Bluetooth, System.Bluetooth.Components, FMX.Controls.Presentation,
  FMX.StdCtrls, FMX.Memo.Types, FMX.ScrollBox, FMX.Memo,
  WinAPI.Winrt, WinAPI.WinRT.Utils;

type
  TMainForm = class(TForm)
    BluetoothLE: TBluetoothLE;
    StartButton: TButton;
    Memo: TMemo;
    procedure DiscoveryFinished(const Sender: TObject; const ADeviceList: TBluetoothLEDeviceList);
    procedure StartButtonClick(Sender: TObject);
    procedure DeviceDiscovered(const Sender: TObject; const ADevice: TBluetoothLEDevice; Rssi: Integer; const ScanResponse: TScanResponse);
    procedure FormCreate(Sender: TObject);
  private
    { Private declarations }
    FDiscoveredBluetoothLEDevices: TList<TBluetoothLEDevice>;
    FStatusLabel: TLabel;
  public
    { Public declarations }
  end;

var
  MainForm: TMainForm;

implementation

{$R *.fmx}

procedure TMainForm.DeviceDiscovered(const Sender: TObject; const ADevice: TBluetoothLEDevice; Rssi: Integer; const ScanResponse: TScanResponse);
var
  LMark: Integer;
begin
  LMark:=TThread.GetTickCount;

  Memo.Lines.Add(Format('[%u] DeviceDiscovered_%u', [TThread.GetTickCount, LMark]));
  if FStatusLabel=nil then Exit;

  if not FDiscoveredBluetoothLEDevices.Contains(ADevice) then
    if Pos(ADevice.DeviceName, '<my device significator>')<>0 then
      FDiscoveredBluetoothLEDevices.Add(ADevice);

  Memo.Lines.Add(Format('[%u] FStatusLabel_%u', [TThread.GetTickCount, LMark]));
  FStatusLabel.Text:=FDiscoveredBluetoothLEDevices.Count.ToString;
end;

procedure TMainForm.DiscoveryFinished(const Sender: TObject; const ADeviceList: TBluetoothLEDeviceList);
begin
  FreeAndNil(FStatusLabel);

  Memo.Lines.Add(Format('[%u] DiscoveryFinished', [TThread.GetTickCount]));
end;

procedure TMainForm.FormCreate(Sender: TObject);
begin
  TThread.NameThreadForDebugging('MainThread');
  FDiscoveredBluetoothLEDevices:=TList<TBluetoothLEDevice>.Create;
end;

procedure TMainForm.StartButtonClick(Sender: TObject);
begin
  FStatusLabel:=TLabel.Create(Self);
  FStatusLabel.Align:=TAlignLayout.Bottom;
  FStatusLabel.Parent:=Self;
  Memo.Lines.Clear;
  FDiscoveredBluetoothLEDevices.Clear;

  BluetoothLE.DiscoverDevices(10000);
end;

end.

Sometimes everything works fine, but sometimes i got access violation in DeviceDiscovered on FStatusLabel.Text:=FDiscoveredBluetoothLEDevices.Count.ToString; because FStatusLabel is nil! I have no idea why is this possible, because FStatusLabel is checked against nil at the begining of the handler. The memo content is such:

MRE in process

Looks like as if the execution of DeviceDiscovered was interrupted by DiscoveryFinished, where TLabel was freed, and by the end of DeviceDiscovered access violation happened. But isn't this insane? How could DiscoveryFinished be fired without the programm finished processing current syncronized method (DeviceDiscovered)?

UPDATE

The problem does not reproduce on VCL. Only on FMX.

UPDATE 2

Ok, it seems the mystery is unveiled. I have managed to get call stack of my application main thread:

enter image description here

Interestingly, that DiscoveryFinished is executing while DeviceDiscovered is upper in the stack! It seems that the reason for this is in CheckInitialized call in blutooth device name getter (in the handler for DeviceDiscovered i access this field). The getter is not simply returns stored string, but tries to perform some async operations:

procedure TWinRTBluetoothLEDevice.CheckInitialized;
var
  LBLEDeviceAsyncOp: IAsyncOperation_1__IBluetoothLEDevice;
begin
  if not Assigned(FBluetoothLEDevice) or FClosed then
  begin
    if FId = 0 then
    begin
      if TAsyncOperation<IAsyncOperation_1__IBluetoothLEDevice>.Wait(
        TBluetoothLEDevice.Statics.FromBluetoothAddressAsync(FAddress), LBLEDeviceAsyncOp) = AsyncStatus.Completed then
      begin
        FBluetoothLEDevice := LBLEDeviceAsyncOp.GetResults;
        FClosed := False;
        if Assigned(FBluetoothLEDevice) then
        begin
          if DeviceName.IsEmpty then
            FDeviceName := FBluetoothLEDevice.Name.ToString;
          FConnectionStatusChangeDelegate := TConnectionStatusChangeEventHandler.Create(Self);
          FBluetoothLEDevice.add_ConnectionStatusChanged(FConnectionStatusChangeDelegate);
        end;
      end;
    end
    else
      if TAsyncOperation<IAsyncOperation_1__IBluetoothLEDevice>.Wait(
          TBluetoothLEDevice.Statics.FromIdAsync(FId), LBLEDeviceAsyncOp) = AsyncStatus.Completed then
      begin
        FBluetoothLEDevice := LBLEDeviceAsyncOp.GetResults;
        FClosed := False;
        if DeviceName.IsEmpty then
          FDeviceName := FBluetoothLEDevice.Name.ToString;
        FConnectionStatusChangeDelegate := TConnectionStatusChangeEventHandler.Create(Self);
        FBluetoothLEDevice.add_ConnectionStatusChanged(FConnectionStatusChangeDelegate);
      end;
  end;
end;

I dont quite understand these WinRT envelopes, but looks like they spawn threads, which at the some time (on destroing) call CheckSyncronize. Since WinRT operation is async and original thread is running, DiscoveryFinished gets into syncronization queue before that.

So, i should avoid accessing TBluetoothLEDevice.DeviceName in event handlers. It reminds me traps of Application.ProcessMessages...

Upvotes: 0

Views: 63

Answers (0)

Related Questions