Lesson: Exercise: Using Multiple Threads

Overview: 
Explore multi-threading by looking at an example.
Objectives: 

Understand how a multi-threaded OpMode works by looking at an example.

Content: 

If you have not read the lesson on multi-threading in the Advanced Topics unit, do so before continuing.

We are going to look at a simple example of multi-threading (also called multi-tasking). We are going to take the Tank Drive exercise and modify it to have the driving part of the code run in a new thread. The idea is that the main thread can focus on other tasks, perhaps more important or complex and the simple driving code can be in a thread, executing separately from the main thread and just taking care of driving. Create a new class called DriveTankMT and put the following code in it:


// simple teleop program that drives bot using controller joysticks in tank mode.
// this code monitors the period and stops when the period is ended.

package org.firstinspires.ftc.teamcode;

import com.qualcomm.robotcore.eventloop.opmode.LinearOpMode;
import com.qualcomm.robotcore.eventloop.opmode.TeleOp;
import com.qualcomm.robotcore.hardware.DcMotor;
import com.qualcomm.robotcore.util.Range;

@TeleOp(name="Drive Tank Multi-Thread", group="Exercises")
//@Disabled
public class DriveTankMT extends LinearOpMode
{
    DcMotor leftMotor, rightMotor;
    float   leftY, rightY;

    public DriveTankMT() throws Exception
    {
        Logging.setup();
        Logging.log("Starting DriveTankMT");
    }

    // called when init button is  pressed.
    @Override
    public void runOpMode() throws InterruptedException
    {
        leftMotor = hardwareMap.dcMotor.get("left_motor");
        rightMotor = hardwareMap.dcMotor.get("right_motor");
        rightMotor.setDirection(DcMotor.Direction.REVERSE);

        // create an instance of the DriveThread.

        Thread  driveThread = new DriveThread();

        telemetry.addData("Mode", "waiting");
        telemetry.update();

        // wait for start button.

        Logging.log("wait for start");

        waitForStart();

        Logging.log("started");

        // start the driving thread.

        driveThread.start();

        // continue with main thread.

        try
        {
            while (opModeIsActive())
            {
                telemetry.addData("Mode", "running");
                telemetry.addData("Run Time", this.getRuntime());
                telemetry.addData("Buttons", "x1=" + gamepad1.x);
                telemetry.addData("sticks", "  left=" + leftY + "  right=" + rightY);
                telemetry.update();

                idle();
            }
        }
        catch(Exception e) {Logging.log(e.getMessage());}

        Logging.log("out of while loop");

        // stop the driving thread.

        driveThread.interrupt();

        Logging.log("end");
    }

    private class DriveThread extends Thread
    {
        public DriveThread()
        {
            this.setName("DriveThread");

            Logging.log("%s", this.getName());
        }

        // called when tread.start is called. thread stays in loop to do what it does until exit is
        // signaled by main code calling thread.interrupt.
        @Override
        public void run()
        {
            Logging.log("Starting thread %s",this.getName());

            try
            {
                while (!isInterrupted())
                {
                    // we record the Y values in the main class to make showing them in telemetry
                    // easier.

                    leftY = gamepad1.left_stick_y;
                    rightY = gamepad1.right_stick_y;

                    leftMotor.setPower(Range.clip(leftY, -1.0, 1.0));
                    rightMotor.setPower(Range.clip(rightY, -1.0, 1.0));

                    idle();
                }
            }
            // interrupted means time to shutdown. note we can stop by detecting isInterrupted = true
            // or by the interrupted exception thrown from the sleep function.
            catch (InterruptedException e) {Logging.log("%s interrupted", this.getName());}
            // an error occurred in the run loop.
            catch (Exception e) {e.printStackTrace(Logging.logPrintStream);}

            Logging.log("end of thread %s", this.getName());
        }
    }
}

The first thing to notice is the use of a private inner (nested) class DriveThread, that extends java.Thread. That class will hold all of the thread specific code, which in the simple case, is just the run() method. Our code to handle driving is in the run() method. We use an inner class because it is simpler and makes accessing the members of the main class easier. Here is more on inner classes.

In the main class at start up we create an instance of the driving thread class and use that instance reference variable to control the thread. Control is pretty simple, when we want to start driving we call the start() method on the thread class and when we want driving to stop we call the interrupt() method on the thread class. While running, the DriveThread class handles the mapping of joystick input to motor power settings and you can see the main thread goes about other business.

In this example, the variables leftY and rightY are shared between the main thread and the driving thread. To reduce concurrency problems, only the driving thread sets the values of leftY and rightY. The main thread only reads from these variables. If high accuracy were needed, we would put the volatile modifier on the definition of leftY and rightY. This is adequate for simple situations. More complex data sharing between threads and a more detailed exploration of concurrency is beyond the scope of this lesson but you can read more about threads and concurrency here.

 

Lesson navigation: