Master of threads under DigitalMicrograph

2019-07-19 14:14发布

问题:

I have created two threads in DigitalMicrograph and they are executed as soon as the script is executed.
I want something different.

Let's imagine two buttons for the threads (start and stop thread).
How can I add code to activate the threads only when I push buttons ?

It would be very helpful if you had a code exemple for me.

回答1:

There are a couple of things to consider:

  • You can not allocate new objects from within the UIframe object. (To be more precise: From a method invoked by a UI action. You can allocate f.e. in the constructor or with an Init() method at start.) Therefore you allocate them beforehand and then let the UIframe object know about them.

  • You often want the UIframe object to be aware of the thread object, but also the thread-object to be aware of the UIframe object. (So that the UI can change if something in the thread object wants it to.)

  • Having objects as member variables of objects is a bit dangerous, because those objects can only be released once the 'keeping' object gets released to. If two objects hold each-other as member variables, you're in a dead-locked situation! For this reason, it is save to use weak referencing: Keep only the objectID numbers as member variables and look objects up on need.

The following code-example should give you a starting point. It consists of 2 classes and a main call. The code is split in this answer, just copy & paste it into a single script file for testing.

First the thread object:

class myThread:Thread
{
  number linkedDLG_ID
  number externalBreak

  myThread( object self )  
  {
    result( self.ScriptObjectGetID() + " created.\n" )
  }

  ~myThread( object self )
  {
    result( self.ScriptObjectGetID() + " destroyed.\n" )
  }

  void SetLinkedDialogID( object self, number ID ) { linkedDLG_ID = ID; }
  void InterruptAtNextChance( object self ) { externalBreak = 1; }

  void RunThread( object self )
  {
    number maxLoop = 30

    object callDLG = GetScriptObjectFromID( linkedDLG_ID )
    externalBreak = 0
    for( number i=0; i<maxLoop; i++ )
    {
      sleep( 0.1 )
      Result( i + "\n" )
      if ( callDLG.ScriptObjectIsValid() )
      {
        callDLG.DLGSetProgress( "progress", (i+1)/maxLoop )
        callDLG.ValidateView()
      }

      if ( externalBreak )
        break;
    }

    // Cleanup at end of thread
    if ( callDLG.ScriptObjectIsValid() )
    {
      callDLG.DLGSetProgress( "progress", 0 )
      callDLG.LookUpElement( "DBevel" ).DLGValue( 0 )
      callDLG.ValidateView( )
    }
  }
}
  • Any threading class is derived from the class Thread.

  • The class has two member variables. One will hold the ID of the UI-object, the other is a simple Boolean to allow 'outside' calls to stop a running thread.

  • The first two methods are the constructor and the destructor. They are not really needed in this example, but it is good practice to put them in during script development, because they will indicate in the results-window when an object of that class gets created and when it gets destroyed. That helps tracking memory leaks and dead-lock situations.

  • The next two methods allow 'outside' calls to set the two member variables.

  • The RunThread method is the heart of any Thread class. It has to be of exactly this signature because it overrides the according method of the parent class Thread from which we derive our class MyThread. The RunThread method gets launched into a separate background thread, when the method StartThread() is called. ( StartThread() is a method of the class Thread. )

  • The actual code in RunThread is in two parts:

    1. An 'action-loop' doing anything you want but allowing a quick-exit if the Boolean variable changes value. This is how external calls can interrupt. This is discussed a bit further down.

    2. A 'clean-up' part where the object can influence the UI object, discussed below as well.

Next is the UI class:

class myDialog:UIframe
{
  object callThread

  myDialog( object self )
  {
    result( self.ScriptObjectGetID() + " created.\n" )
  }
  ~myDialog( object self )
  {
    result( self.ScriptObjectGetID() + " destroyed.\n")
  }


  TagGroup CreateDLG( object self )
  {
    image i := IntegerImage( "", 1, 0, 25, 25)
    i = 0; i[ 2 , 2 , 23 , 23 ] = 1;
    image onImage, offImage
    onImage   = RGB( 0*i , 200*i , 0*i )
    offImage  = RGB( 200*i , 0*i , 0*i )

    TagGroup tgItems, tg, button, label, progress
    tg = DLGCreateDialog("Dialog",tgItems)
    button = DLGCreateDualStateBevelButton( "DBevel", onImage, offImage, "StartPressed" )
    progress = DLGCreateProgressBar( "progress" ).DLGfill( "X" )
    label = DLGCreateLabel( "start/stop" )
    tgItems.DLGAddElement( DLGGroupItems( button , label ).DLGTableLayout( 2 , 1 , 0 ) )
    tgItems.DLGAddElement( progress )
    return tg
  }

  object Init(object self, number callThreadID )
  {
    // Assign thread-object via weak-reference
    callThread = GetScriptObjectFromID( callThreadID )      
    if ( !callThread.ScriptObjectIsvalid() )
      Throw( "Invalid thread object passed in! Object of given ID not found." )

    // Pass weak-reference to thread object
    callThread.SetLinkedDialogID( self.ScriptObjectGetID() )  
    return self.super.init( self.CreateDLG() )
  }

  void StartPressed( object self )
  {
    number active = self.LookupElement( "DBevel" ).DLGGetValue()
    if ( active )
      callThread.StartThread()
    else
      callThread.InterruptAtNextChance()
  } 
}
  • Any dialog (UI) class is derived from the class UIframe.

  • This class has only one member variable: An object, which will be the thread-object.

  • Again there are a constructor/destructor method for easier debugging.

  • The CreateDLG method builds the tagGroup describing the dialog. I will not go into details here, but essentially it creates the following dialog when displayed:

  • The Init() method initializes the object. The Init() method of the base class UIframe requires a descriptive TagGroup and returns the UI object itself. We call on this in the last line of our extended Init() method, and use our class-method to create the tagGroup:

    return self.super.init( self.CreateDLG() )

    The code before is what links our thread-object to the UI-object. We pass in a number, which is the object-ID of our thread-object. We now get the according object from memory and assign it to our local member variable. (NB: The variable now holds the object itself, not a copy or clone of it! )

    callThread = GetScriptObjectFromID( callThreadID )

    Right away, we check if the lookup was successful and actually returned a valid object. If not, we stop our script with a thrown exception. From now on, the UI-object 'contains' the thread-object and can use it.

  • Now comes the back-link. Now that the UI object has been allocated, it also has an object-ID. We feed this number into our thread-object.

    callThread.SetLinkedDialogID( self.ScriptObjectGetID() )

    From now on, the thread object is nicely linked to the UI-object. Looking back to the myThread class, you will notice that we use the same trick of looking up and locally storing the object in the RunThread() method.

  • StartPressed() is the method linked to our dialog button. This button is a bevel button, so we query its state, which is the state after the bevel-button changed, and act accordingly. We either launch the RunThread() method of our thread object as a background-thread, or invoke the according 'interrupt' method, which simply sets the Boolean variable

Finally the main script:

void StartScript()
 {
   object threadObject = alloc( myThread )
   object dlgObject = alloc( myDialog ).Init( threadObject.ScriptObjectGetID() )
   dlgObject.display( "Dialog" )
 }
StartScript()
  • Not a lot going on here. We first create the threadObject of the myThread class, and then the dialog UI object.

  • We initialize the dialog object with the ID of the existing threadObject, and then display it as a modeless dialog.

Some points to notice:

  • Whenever you use object variables in DigitalMicrograph scripting, you should put them into a structure block. This ensures that the objects get out-of-scope and deleted, when the structure block is left. Object variables defined and allocated in the main script are not destructed at the end of the script. For this reason, we have encapsulated the main script in a method itself.

  • We have used two different methods of linking in this example:

    • Direct: The myDialog class really keeps the thread-object itself as a member variable. Although we initialized it with the ID only, we immediately linked the object to a member variable.

    • Weak reference: The myThread class only holds the object-ID of the dialog-object as a member variable.

    Why have we done this? If the myThread class would keep the dialog-object as a member, then the two objects would hold each-other in a dead-lock situation. Neither can be destructed because of the other. But why have we not used the same for the myDialog class? Because we want to display the dialog as a modeless dialog in a background thread itself!

    Think of the main-script:

    1. We create the thread-object
    2. We create the dialog-object
    3. We display the dialog-object (But we don't stop script execution here!)
    4. The script ends

    But when the script ends, the object variables threadObject and dlgObject go out of scope! They will be immediately destructed, unless something keeps them in memory. The dlgObject stays in memory, because we displayed it as modeless dialog. It will be released, when the according dialog window is closed. But what keeps the threadObject? Nothing! Once the RunThread() method has finished, it would be released and subsequently destructed. However, because it is a member of the dlgObject it will not be destructed.