การทำงานแบบไม่พร้อมกันกับเทรด Java

แอป Android ทั้งหมดจะใช้เทรดหลักในการจัดการกับการดำเนินการของ UI การโทรเป็นเวลานาน การดำเนินการจากเทรดหลักนี้อาจทำให้เกิดการค้างและไม่ตอบสนอง สำหรับ เช่น หากแอปสร้างคำขอเครือข่ายจากเทรดหลัก UI ของแอป ถูกหยุดไว้จนกว่าจะได้รับการตอบสนองของเครือข่าย ถ้าคุณใช้ Java คุณสามารถ สร้างชุดข้อความเบื้องหลังเพิ่มเติมเพื่อจัดการการดำเนินการที่ใช้เวลานาน เทรดหลักจะยังคงจัดการการอัปเดต UI อยู่

คู่มือนี้แสดงวิธีที่นักพัฒนาซอฟต์แวร์ที่ใช้ภาษาโปรแกรม Java สามารถใช้ Thread Pool เพื่อตั้งค่าและใช้ชุดข้อความหลายรายการในแอป Android คู่มือนี้ ก็แสดงวิธีกำหนดโค้ดให้ทำงานในเทรดและวิธีสื่อสาร ระหว่างเทรดเหล่านี้ กับเทรดหลัก

ไลบรารีการเกิดขึ้นพร้อมกัน

คุณต้องเข้าใจพื้นฐานของชุดข้อความและองค์ประกอบที่เกี่ยวข้อง และกลไกต่างๆ อย่างไรก็ตาม มีห้องสมุดยอดนิยมจำนวนมากที่มีห้องสมุดระดับสูงขึ้น แอบสแตรกชันในแนวคิดเหล่านี้และยูทิลิตีที่พร้อมใช้งานสำหรับการส่งข้อมูล ระหว่างชุดข้อความได้ ไลบรารีเหล่านี้รวมถึง Guava และ RxJava สําหรับผู้ใช้ภาษาโปรแกรม Java และ Coroutines ซึ่งเราแนะนำสำหรับผู้ใช้ Kotlin

ในทางปฏิบัติ คุณควรเลือกวิธีที่เหมาะสมกับแอปและ แต่กฎการจัดชุดข้อความจะยังคงเหมือนเดิม

ภาพรวมตัวอย่าง

จากคำแนะนำเกี่ยวกับสถาปัตยกรรมแอป ตัวอย่างในหัวข้อนี้ทำให้ คำขอเครือข่ายและแสดงผลลัพธ์ไปยังเทรดหลัก จากนั้นแอป อาจแสดงผลลัพธ์นั้นบนหน้าจอ

กล่าวโดยเจาะจงคือ ViewModel จะเรียกชั้นข้อมูลในเทรดหลักไปยัง เรียกใช้คำขอเครือข่าย ชั้นข้อมูลทำหน้าที่ย้าย การดำเนินการตามคำขอเครือข่ายจากเทรดหลัก และโพสต์ผลลัพธ์กลับมา ไปยังเทรดหลักโดยใช้ Callback

หากต้องการย้ายการดำเนินการตามคำขอเครือข่ายออกจากเทรดหลัก เราจำเป็นต้องดำเนินการต่อไปนี้ สร้างชุดข้อความอื่นๆ ในแอปของเรา

สร้างชุดข้อความหลายรายการ

กลุ่มชุดข้อความ คือคอลเล็กชันที่มีการจัดการของชุดข้อความที่เรียกใช้งานใน ขนานกันจากคิว งานใหม่จะดำเนินการในชุดข้อความที่มีอยู่ เทรดจะไม่มีการใช้งาน หากต้องการส่งงานไปยัง Thread Pool ให้ใช้ ExecutorService โปรดทราบว่า ExecutorService ไม่ต้องดำเนินการใดๆ กับ Services ซึ่งเป็นคอมโพเนนต์ของแอปพลิเคชัน Android

การสร้างชุดข้อความมีราคาแพง คุณจึงควรสร้างพูลชุดข้อความเพียงครั้งเดียวในฐานะ แอปของคุณจะเริ่มต้น อย่าลืมบันทึกอินสแตนซ์ของ ExecutorService ในคลาส Application หรือในคอนเทนเนอร์การแทรกแบบ Dependency ตัวอย่างต่อไปนี้สร้าง Thread Pool ที่ประกอบด้วย 4 ชุดข้อความที่เราสามารถใช้เพื่อ เรียกใช้งานเบื้องหลัง

public class MyApplication extends Application {
    ExecutorService executorService = Executors.newFixedThreadPool(4);
}

ยังมีวิธีอื่นที่คุณจะกำหนดค่า Thread Pool ได้ตามความต้องการ ปริมาณงาน โปรดดูข้อมูลเพิ่มเติมที่การกำหนดค่า Thread Pool

ดำเนินการในชุดข้อความเบื้องหลัง

การส่งคำขอเครือข่ายในเทรดหลักจะทำให้เทรดต้องรอเทรด หรือ บล็อก จนกว่าจะได้รับการตอบกลับ เนื่องจากเทรดถูกบล็อก ระบบปฏิบัติการจึงไม่สามารถ โทรหา onDraw() แอปของคุณหยุดทำงาน ซึ่งอาจทำให้แอปพลิเคชันไม่ทำงาน กล่องโต้ตอบการตอบกลับ (ANR) ลองทำการดำเนินการนี้ในพื้นหลังแทน ชุดข้อความ

ส่งคำขอ

ก่อนอื่น เราจะมาดูชั้นเรียน LoginRepository ของเรา แล้วดูว่าผลการเรียนรู้เป็นอย่างไร คำขอเครือข่าย

// Result.java
public abstract class Result<T> {
    private Result() {}

    public static final class Success<T> extends Result<T> {
        public T data;

        public Success(T data) {
            this.data = data;
        }
    }

    public static final class Error<T> extends Result<T> {
        public Exception exception;

        public Error(Exception exception) {
            this.exception = exception;
        }
    }
}

// LoginRepository.java
public class LoginRepository {

    private final String loginUrl = "https://example.com/login";
    private final LoginResponseParser responseParser;

    public LoginRepository(LoginResponseParser responseParser) {
        this.responseParser = responseParser;
    }

    public Result<LoginResponse> makeLoginRequest(String jsonBody) {
        try {
            URL url = new URL(loginUrl);
            HttpURLConnection httpConnection = (HttpURLConnection) url.openConnection();
            httpConnection.setRequestMethod("POST");
            httpConnection.setRequestProperty("Content-Type", "application/json; charset=utf-8");
            httpConnection.setRequestProperty("Accept", "application/json");
            httpConnection.setDoOutput(true);
            httpConnection.getOutputStream().write(jsonBody.getBytes("utf-8"));

            LoginResponse loginResponse = responseParser.parse(httpConnection.getInputStream());
            return new Result.Success<LoginResponse>(loginResponse);
        } catch (Exception e) {
            return new Result.Error<LoginResponse>(e);
        }
    }
}

makeLoginRequest() เป็นแบบพร้อมกันและบล็อกชุดข้อความการโทร ในการสร้างแบบจำลอง การตอบสนองต่อคำขอเครือข่าย เรามีคลาส Result เป็นของเราเอง

เรียกใช้คำขอ

ViewModel จะเรียกใช้คำขอเครือข่ายเมื่อผู้ใช้แตะ เช่น ปุ่ม:

public class LoginViewModel {

    private final LoginRepository loginRepository;

    public LoginViewModel(LoginRepository loginRepository) {
        this.loginRepository = loginRepository;
    }

    public void makeLoginRequest(String username, String token) {
        String jsonBody = "{ username: \"" + username + "\", token: \"" + token + "\" }";
        loginRepository.makeLoginRequest(jsonBody);
    }
}

เมื่อใช้โค้ดก่อนหน้า LoginViewModel จะบล็อกเทรดหลักเมื่อสร้าง คำขอเครือข่าย เราใช้ Thread Pool ที่เราสร้างไว้ในการย้ายแล้วได้ การดำเนินการกับเทรดในเบื้องหลัง

จัดการการแทรกทรัพยากร Dependency

ขั้นแรก ตามหลักการแทรกทรัพยากร Dependency LoginRepository ใช้อินสแตนซ์ของ ผู้ดำเนินการ ซึ่งตรงข้ามกับ ExecutorService เนื่องจาก กำลังรันโค้ดและไม่ได้จัดการชุดข้อความ:

public class LoginRepository {
    ...
    private final Executor executor;

    public LoginRepository(LoginResponseParser responseParser, Executor executor) {
        this.responseParser = responseParser;
        this.executor = executor;
    }
    ...
}

เมธอด execute() ของโอเปอเรเตอร์จะใช้Runnable Runnable คือ อินเทอร์เฟซ Single Abstract Method (SAM) ที่มีเมธอด run() ที่ดำเนินการใน ชุดข้อความเมื่อเรียกใช้

ทำงานในพื้นหลัง

มาสร้างฟังก์ชันอีกรายการหนึ่งที่ชื่อว่า makeLoginRequest() ที่ย้ายฟังก์ชัน การดำเนินการกับเธรดเบื้องหลังและไม่สนใจการตอบกลับในขณะนี้:

public class LoginRepository {
    ...
    public void makeLoginRequest(final String jsonBody) {
        executor.execute(new Runnable() {
            @Override
            public void run() {
                Result<LoginResponse> ignoredResponse = makeSynchronousLoginRequest(jsonBody);
            }
        });
    }

    public Result<LoginResponse> makeSynchronousLoginRequest(String jsonBody) {
        ... // HttpURLConnection logic
    }
    ...
}

ภายในเมธอด execute() เราได้สร้าง Runnable ใหม่ด้วยบล็อกของโค้ด เราต้องการที่จะดำเนินการในเทรดเบื้องหลัง ในกรณีของเรา คือเครือข่ายซิงโครนัส เมธอด ภายในองค์กร ExecutorService จะจัดการ Runnable และ เรียกใช้ในชุดข้อความที่มีอยู่

ข้อควรพิจารณา

เทรดใดก็ตามในแอปของคุณสามารถทำงานควบคู่ไปกับเทรดอื่นๆ ได้ ซึ่งรวมถึงเทรดหลัก เทรด ดังนั้นคุณจึงควรตรวจสอบว่าโค้ดปลอดภัยของเทรด โปรดสังเกตว่าใน ตัวอย่างเช่น เราจะหลีกเลี่ยงการเขียนไปยังตัวแปรที่แชร์กันระหว่างชุดข้อความ ข้อมูลที่เปลี่ยนแปลงไม่ได้แทน ซึ่งเป็นแนวทางปฏิบัติที่ดี เนื่องจากแต่ละชุดข้อความจะทำงานร่วมกับ อินสแตนซ์ของข้อมูลเอง และช่วยให้เราหลีกเลี่ยงความซับซ้อนของการซิงค์

หากต้องการแชร์สถานะระหว่างชุดข้อความ คุณต้องจัดการการเข้าถึงอย่างระมัดระวัง จากชุดข้อความโดยใช้กลไกการซิงค์ เช่น ล็อก รายการนี้อยู่นอก ขอบเขตของคู่มือนี้ โดยทั่วไปคุณควรหลีกเลี่ยงการแชร์สถานะที่เปลี่ยนแปลงได้ ระหว่างชุดข้อความทุกครั้งที่เป็นไปได้

สื่อสารกับเทรดหลัก

ในขั้นตอนก่อนหน้า เราไม่สนใจการตอบกลับคำขอเครือข่าย หากต้องการแสดง ผลลัพธ์บนหน้าจอ LoginViewModel จำเป็นต้องทราบเกี่ยวกับผลลัพธ์นั้น สามารถทำได้โดย โดยใช้Callback

ฟังก์ชัน makeLoginRequest() ควรใช้ Callback เป็นพารามิเตอร์เพื่อให้ จึงสามารถคืนค่าแบบไม่พร้อมกัน Callback ที่มีผลลัพธ์จะเรียกว่า เมื่อใดก็ตามที่คำขอเครือข่ายดำเนินการเสร็จสมบูรณ์หรือเกิดข้อผิดพลาด ใน Kotlin เราสามารถ ใช้ฟังก์ชันลำดับที่สูงกว่า อย่างไรก็ตาม ใน Java เราต้องสร้าง Callback ใหม่ เพื่อให้มีฟังก์ชันการทำงานเหมือนกัน:

interface RepositoryCallback<T> {
    void onComplete(Result<T> result);
}

public class LoginRepository {
    ...
    public void makeLoginRequest(
        final String jsonBody,
        final RepositoryCallback<LoginResponse> callback
    ) {
        executor.execute(new Runnable() {
            @Override
            public void run() {
                try {
                    Result<LoginResponse> result = makeSynchronousLoginRequest(jsonBody);
                    callback.onComplete(result);
                } catch (Exception e) {
                    Result<LoginResponse> errorResult = new Result.Error<>(e);
                    callback.onComplete(errorResult);
                }
            }
        });
    }
  ...
}

ViewModel ต้องใช้ Callback ทันที อาจให้ผลที่แตกต่างกันไป ขึ้นอยู่กับผลลัพธ์ ดังนี้

public class LoginViewModel {
    ...
    public void makeLoginRequest(String username, String token) {
        String jsonBody = "{ username: \"" + username + "\", token: \"" + token + "\" }";
        loginRepository.makeLoginRequest(jsonBody, new RepositoryCallback<LoginResponse>() {
            @Override
            public void onComplete(Result<LoginResponse> result) {
                if (result instanceof Result.Success) {
                    // Happy path
                } else {
                    // Show error in UI
                }
            }
        });
    }
}

ในตัวอย่างนี้ การเรียกใช้ Callback จะดำเนินการในเทรดการเรียกใช้ ซึ่งเป็น เทรดพื้นหลัง ซึ่งหมายความว่าคุณไม่สามารถแก้ไขหรือสื่อสารโดยตรง กับเลเยอร์ UI จนกว่าคุณจะเปลี่ยนกลับไปใช้เทรดหลัก

ใช้ตัวจัดการ

คุณใช้เครื่องจัดการเพื่อกำหนดคิวการดำเนินการเพื่อดำเนินการในอุปกรณ์อื่นได้ ชุดข้อความ หากต้องการระบุเทรดที่จะเรียกใช้การดำเนินการ ให้สร้างพารามิเตอร์ Handler กำลังใช้ Looper สำหรับชุดข้อความ Looper คือออบเจ็กต์ที่เรียกใช้ ที่วนซ้ำข้อความสำหรับชุดข้อความที่เกี่ยวข้อง เมื่อคุณสร้าง Handler คุณ จากนั้นสามารถใช้เมธอด post(Runnable) เพื่อเรียกใช้บล็อกโค้ดใน ชุดข้อความที่เกี่ยวข้อง

Looper มีฟังก์ชันตัวช่วย getMainLooper() ซึ่งเรียกฟังก์ชัน Looper ของชุดข้อความหลัก คุณเรียกใช้โค้ดในเทรดหลักได้โดยใช้ Looper เพื่อสร้าง Handler เนื่องจากนี่เป็นกิจกรรม ที่คุณอาจทำบ่อยๆ คุณยังสามารถบันทึกอินสแตนซ์ของ Handler ในที่เดียวกับที่คุณบันทึก ExecutorService:

public class MyApplication extends Application {
    ExecutorService executorService = Executors.newFixedThreadPool(4);
    Handler mainThreadHandler = HandlerCompat.createAsync(Looper.getMainLooper());
}

คุณควรแทรกเครื่องจัดการลงในที่เก็บเนื่องจาก ให้ความยืดหยุ่นมากกว่า ตัวอย่างเช่น ในอนาคต คุณอาจต้องการส่งผ่าน Handler ที่แตกต่างกันเพื่อตั้งเวลางานในชุดข้อความแยกต่างหาก หากคุณ สื่อสารกลับไปยังชุดข้อความเดียวกัน คุณสามารถส่ง Handler ไปยัง เครื่องมือสร้างที่เก็บ ดังที่ปรากฏในตัวอย่างต่อไปนี้

public class LoginRepository {
    ...
    private final Handler resultHandler;

    public LoginRepository(LoginResponseParser responseParser, Executor executor,
            Handler resultHandler) {
        this.responseParser = responseParser;
        this.executor = executor;
        this.resultHandler = resultHandler;
    }

    public void makeLoginRequest(
        final String jsonBody,
        final RepositoryCallback<LoginResponse> callback
    ) {
        executor.execute(new Runnable() {
            @Override
            public void run() {
                try {
                    Result<LoginResponse> result = makeSynchronousLoginRequest(jsonBody);
                    notifyResult(result, callback);
                } catch (Exception e) {
                    Result<LoginResponse> errorResult = new Result.Error<>(e);
                    notifyResult(errorResult, callback);
                }
            }
        });
    }

    private void notifyResult(
        final Result<LoginResponse> result,
        final RepositoryCallback<LoginResponse> callback,
    ) {
        resultHandler.post(new Runnable() {
            @Override
            public void run() {
                callback.onComplete(result);
            }
        });
    }
    ...
}

หรือหากต้องการความยืดหยุ่นมากขึ้น คุณสามารถส่ง Handler ไปยัง ฟังก์ชัน:

public class LoginRepository {
    ...

    public void makeLoginRequest(
        final String jsonBody,
        final RepositoryCallback<LoginResponse> callback,
        final Handler resultHandler,
    ) {
        executor.execute(new Runnable() {
            @Override
            public void run() {
                try {
                    Result<LoginResponse> result = makeSynchronousLoginRequest(jsonBody);
                    notifyResult(result, callback, resultHandler);
                } catch (Exception e) {
                    Result<LoginResponse> errorResult = new Result.Error<>(e);
                    notifyResult(errorResult, callback, resultHandler);
                }
            }
        });
    }

    private void notifyResult(
        final Result<LoginResponse> result,
        final RepositoryCallback<LoginResponse> callback,
        final Handler resultHandler
    ) {
        resultHandler.post(new Runnable() {
            @Override
            public void run() {
                callback.onComplete(result);
            }
        });
    }
}

ในตัวอย่างนี้ Callback ที่ส่งผ่านไปยัง makeLoginRequest ของที่เก็บ เรียกใช้ในเทรดหลัก ซึ่งหมายความว่าคุณสามารถปรับเปลี่ยน UI ได้โดยตรง จาก Callback หรือใช้ LiveData.setValue() เพื่อสื่อสารกับ UI

กำหนดค่า Thread Pool

คุณสร้าง Thread Pool ได้โดยใช้ฟังก์ชันตัวช่วยของผู้ดำเนินการ ด้วยการตั้งค่าที่กำหนดไว้ล่วงหน้า ดังที่แสดงในโค้ดตัวอย่างก่อนหน้านี้ หรือ หากต้องการปรับแต่งรายละเอียด ของกลุ่มชุดข้อความ คุณสามารถสร้าง โดยใช้ ThreadPoolExecutor โดยตรง คุณกําหนดค่าสิ่งต่อไปนี้ได้ รายละเอียด:

  • ขนาดเริ่มต้นและขนาดพูลสูงสุด
  • แสดงเวลาและหน่วยเวลา เวลาที่ยังคงทำงานอยู่คือระยะเวลาสูงสุดที่ ชุดข้อความจะยังคงไม่มีการใช้งานก่อนที่จะปิดลง
  • คิวอินพุตที่มี Runnable งาน คิวนี้ต้องใช้เมธอด อินเทอร์เฟซ blockQueue หากต้องการปฏิบัติตามข้อกำหนดของแอป ให้ทำดังนี้ เลือกจากการใช้งานคิวที่มีอยู่ หากต้องการดูข้อมูลเพิ่มเติม โปรดดูชั้นเรียน ภาพรวมสำหรับ ThreadPoolExecutor

นี่คือตัวอย่างที่ระบุขนาด Thread Pool โดยอิงตามจำนวนรวมของ โปรเซสเซอร์ Core, เวลาการทำงาน Keep 1 วินาที และคิวอินพุต

public class MyApplication extends Application {
    /*
     * Gets the number of available cores
     * (not always the same as the maximum number of cores)
     */
    private static int NUMBER_OF_CORES = Runtime.getRuntime().availableProcessors();

    // Instantiates the queue of Runnables as a LinkedBlockingQueue
    private final BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<Runnable>();

    // Sets the amount of time an idle thread waits before terminating
    private static final int KEEP_ALIVE_TIME = 1;
    // Sets the Time Unit to seconds
    private static final TimeUnit KEEP_ALIVE_TIME_UNIT = TimeUnit.SECONDS;

    // Creates a thread pool manager
    ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
            NUMBER_OF_CORES,       // Initial pool size
            NUMBER_OF_CORES,       // Max pool size
            KEEP_ALIVE_TIME,
            KEEP_ALIVE_TIME_UNIT,
            workQueue
    );
    ...
}