농농씨 2023. 10. 31. 00:15

스레드의 context switching이 어떻게 이루어지는지, 멀티스레드프로그래밍을 하면서 알아볼 예정.

 

지난 주의 뷰페이저 실습 프로젝트에서 이어서 할 것이다.

 

1. 첫번째 스레드 실습

1-1. onCreate()에 testThread1()라는 함수 호출하고 onCreate밖에 함수 선언하기

1-2. private fun testThread1()

// testThread1() 함수
private fun testThread1() {
        Thread(){
            for (i in 1 until 1001){
                print(i)
            }
        }.start()

        Thread(){
            for (i in 1000 downTo 0){
                print(i)
            }
        }.start()
    }

여기서 실행해서 logcat 보면 스레드가 990쯤부터 끊기고 다시 1000부터 아래로 내려가고 내려가다가 숫자 또 끊김

알수있는것?

두 개의 thread가 여러 cpu의 context switch 에 의해서 어느 스레드가 먼저/나중에 실행되는지 우리가 알 수 없다.

만약 메인 스레드와 워커 스레드가 동시에 ui의 텍스트뷰를 수정하려 할 때 뭐가 먼저 실행될지 알수 없다!

 

2. 두번째 스레드 실습(숫자 카운트)

2-1. onCreate에 testThread2() 함수 호출

2-2. onCreate 위에 var count = 0 변수 선언

2-3. private fun testThread2()

// testThread2() 함수
private fun testThread2() {
        Thread(){
            while (true) {
                count++
            }
        }.start()

        Thread(){
            while (true) {
                count--
                if (count < 0) {
                    println("zero")
                }
            }
        }.start()
    }

무한루프 이용해서 변수의 숫자를 증가시키는 스레드와 감소시키는 스레드를 동시에 진행시킴

실행시켜보면

zero 계속 나오고 같은 줄 계속 출력됨(엎치락 뒤치락)

역시나 뭐가 먼저 실행되는지 우리는 알 수 없다.

 

세번째 실습: 뷰페이저 자동으로 넘어가게 하기!

스레드를 사용할 때는 Handler와 Looper가 필요하므로 차근차근 구현해보자.

1. MainActvity에서 imageSwiper라는 객체 먼저 선언해주고,

2. inner class ImageSwiper()

2-1. ImageSwiper()라는 이름의 inner 클래스도 MainActivity 안에 만들기.

그리고 Thread() 클래스를 상속받게하기.

Thread()라는 클래스로부터 run() 이라는 함수 override 하기.

run() 안에 코드블럭(코드)을 작성하면, 스레드의 역할로 코드블럭을 실행한다.

🤔

private val imageSwiper: ImageSwiper()

override fun onCreate() { ... }

inner class ImageSwiper : Thread() {
        override fun run() {
            super.run()
        }
    }

(아까 예제 코드(testThread1(), testThread2())에서 Thread() {} 안에 있는 코드블럭의 내용을 run() 함수에 넣어서

원하는 때에 실행할 수 있도록 thread를 제어한다고 생각하면 됨.)

 

2-2. try/catch 구문으로 코드블럭을 감싸보자

스레드가 중단되는 것을 Interrupt라고 함. 

catch (e: InterruptedException)

Interrupt 이벤트가 발생하면 스레드는 종료되고 catch 구문이 InterruptedException 을 반환하게 된다.

 

try {
while (true) {
sleep(1000)
}

try 부분에서는 무한루프를 돌게 하고

sleep() 함수를 선언하면 miliSecond 단위로 숫자를 입력했을 때 1000mili second면 1초 쉬고 그 다음이 실행된다.

1초 쉰 다음에 여기서는 무한루프이기때문에  해당 작업이 반복된다.

 

원래 여기서 핸들러를 더 추가하지 않고 실행하면 화면이 넘어가는 게 제어가 되지 않을 것이다.

2-3. 핸들러 추가

onCreate 밖에 변수 추가

private val pagerHandler = Handler(Looper.getMainLooper())

 

❗️이론복습

스레드 하나에는 핸들러와 메시지 큐가 각각 하나씩 있다. 

루퍼도 있는데, 루퍼가 메시지 큐를 감시하면서 이벤트가 발생했을 때 핸들러로 전달해준다.

 

2-4. 핸들러에 이벤트 대처 등록

inner class ImageSlider의 run() 함수 안에서

try/catch 구문 중 try 안에

pagerHandler에 post를 사용해서 이벤트에 어떻게 대처할 것인지 등록을 해줄 것이다.

pagerHandler.post {
    var position = binding.imageAreaVp.currentItem
    if (position == 2) {
        position = 0
    } else {
        position++
    }
    
    binding.imageAreaVp.currentItem = position
}

position은 내가 만들었던 뷰페이저(binding)의 현재 아이템(currentItem)의 번호를 지정하는 변수로 설정.

position 값이 2가 되면 position을 0으로 만들어주고

아니면 하나씩 증가시킨다.

 

그리고 position값에 따라 binding한 뷰페이저의 현재 화면을 업데이트한다.(여기까지 핸들러 안의 구문)

 

3. Interrupt 발생시

catch 구문 안에 Interrupt 발생시 로그찍기

Log.d("INTERRUPT", "쓰레드 종료")

interrupt()

interrupt() 함수 실행시켜서 스레드 종료시킴

(종료안시키면?)

 

4. 생명주기 함수들(onCreate/onDestroy)

안드로이드의 액티비티의 생명주기 중 하나인 onDestroy()

 

액티비티가 종료되었을 때의 행동으로

imageSwiper.interrupt()

imageSwiper라는 ImageSwiper 클래스의 객체의 스레드를 종료시키도록 함.

 

onCreate 함수에서는

imageSwiper.start()

이미지 스와이프하도록 함

 

// MainActivity 전체
package com.iyr.viewpager2practice

import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.viewpager2.widget.ViewPager2
import com.iyr.viewpager2practice.databinding.ActivityMainBinding

class MainActivity : ComponentActivity() {
    private lateinit var binding: ActivityMainBinding
    private val imgList = mutableListOf<String>()
    private val textList = mutableListOf<String>()
    var count = 0
    private val pagetHandler = Handler(Looper.getMainLooper())

    private val imageSwiper: ImageSwiper = ImageSwiper()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        initDummyData()
        initViewPager()

        imageSwiper.start()

//        testThread1()
//        testThread2()

    }

    inner class ImageSwiper : Thread() {
        override fun run() {
            try {
                while (true) {
                    sleep(1000)

                    pagetHandler.post {
                        var position = binding.imageAreaVp.currentItem
                        if (position == 2) {
                            position = 0
                        } else {
                            position++
                        }

                        binding.imageAreaVp.currentItem = position
                    }

                }
            } catch (e: InterruptedException) {
                Log.d("INTERRUPT", "쓰레드 종료")
            }

        }
    }

    override fun onDestroy() {
        super.onDestroy()
        imageSwiper.interrupt()
    }

    private fun testThread2() {
        Thread() {
            while (true) {
                count++
            }
        }.start()

        Thread() {
            while (true) {
                count--
                if (count < 0) {
                    println("zero")
                }
            }
        }.start()
    }

    private fun testThread1() {
        Thread() {
            for (i in 1 until 1001) {
                print(i)
            }
        }.start()

        Thread() {
            for (i in 1000 downTo 0) {
                print(i)
            }
        }.start()
    }

    private fun initViewPager() {
        binding.imageAreaVp.adapter = ImageSliderVPAdapter(applicationContext, imgList)
        binding.imageAreaVp.orientation = ViewPager2.ORIENTATION_HORIZONTAL

        setTextViewChangeEvent()
    }

    private fun setTextViewChangeEvent() {
        binding.imageAreaVp.registerOnPageChangeCallback(object :
            ViewPager2.OnPageChangeCallback() {
            override fun onPageSelected(position: Int) {
                binding.textTv.text = textList[position]
            }
        })
    }

    private fun initDummyData() {
        imgList.add("https://cdn.pixabay.com/photo/2014/11/30/14/11/cat-551554_1280.jpg")
        imgList.add("https://cdn.pixabay.com/photo/2014/04/13/20/49/cat-323262_1280.jpg")
        imgList.add("https://cdn.pixabay.com/photo/2017/07/25/01/22/cat-2536662_1280.jpg")

        textList.add("냥")
        textList.add("냥냥")
        textList.add("냥냥냥")
    }
}

 

이로써 앱을 실행시켰을 때 onCreate 함수의 imageSwiper.start() 함수가 실행되면서

스레드가 실행되고 뷰페이저 화면이 (1초마다) 자동으로 넘어가는 걸 기대할 수 있고,

액티비티가 종료되었을 때는 onDestroy 함수가 호출이 돼서 interrupt가 실행돼서 스레드가 종료되도록 했다.

 

kuit6주차실습.mov
4.85MB

 

 

 

 

 

질문 1. 탭레이아웃 뷰페이저 어댑터는 뷰페이저의 어댑터란 걸까?흠... 탭레이아웃의 어댑터란걸까?

chat GPT 답변:

탭 레이아웃과 뷰페이저 어댑터는 다른 개념이며 서로 다른 역할을 합니다. 

1. **뷰페이저 어댑터 (ViewPager Adapter):** 뷰페이저 어댑터는 뷰페이저(ViewPager)와 연결된 데이터를 관리하고 화면에 표시할 페이지들을 제공하는 역할을 합니다. 뷰페이저는 여러 페이지를 스와이프하며 표시하는 위젯이며, 뷰페이저 어댑터는 이러한 페이지들의 데이터를 관리하고 각 페이지를 생성합니다. 뷰페이저 어댑터는 `FragmentPagerAdapter` 또는 `FragmentStatePagerAdapter`를 상속받아 구현되며, 페이지의 내용을 프래그먼트로 표시할 때 사용됩니다.

2. **탭 레이아웃 어댑터 (TabLayout Adapter):** 탭 레이아웃은 주로 뷰페이저와 함께 사용되며, 탭 레이아웃 어댑터는 탭 레이아웃 내의 탭들을 관리하고 각 탭을 뷰페이저의 페이지와 연결하는 역할을 합니다. 탭 레이아웃 어댑터는 주로 `FragmentPagerAdapter` 또는 `FragmentStatePagerAdapter`와 함께 사용되어 뷰페이저의 각 페이지와 탭을 매칭시킵니다. 이것은 사용자가 탭을 선택하면 해당 페이지가 뷰페이저에 표시되는 방식을 구현할 때 유용합니다.

둘을 연결하는 어댑터가 있는 것이 아니라, 각각의 역할을 수행하는 어댑터가 별도로 있습니다. 탭 레이아웃과 뷰페이저를 함께 사용할 때, 뷰페이저 어댑터는 페이지 데이터를 관리하고, 탭 레이아웃 어댑터는 탭과 뷰페이저의 페이지를 관리하며 두 개의 구성 요소를 연결하여 사용자가 탭을 선택하면 뷰페이저의 페이지가 변경되도록 조정합니다.

 

 

질문2. TabLayoutMediator(binding.mainTb, binding.mainVp)와 같은 함수에서 왜 세번째 인자로 람다함수를 넣어줄 때 소괄호 안에 들어가지 않고 옆에 중괄호로 써야 하는지 궁금해

 

 

chat GPT 답변:

소괄호(())를 사용하여 람다 함수를 감싸지 않는 이유는 Kotlin의 규칙에 따라, 만약 함수의 마지막 인자가 람다 함수이고 함수 호출이 괄호 안에 포함된 경우, 괄호를 밖으로 빼는 것이 허용됩니다. 이것은 코드를 더 간결하게 만들기 위한 Kotlin의 문법적인 특징 중 하나입니다.

따라서 TabLayoutMediator 함수의 마지막 인자가 람다 함수이며 괄호 안에 넣지 않아도 됩니다. Kotlin에서는 이러한 구문을 지원하여 코드를 보다 가독성 있게 만들어줍니다.

 

 

질문3. 실습 영상에서는 Private val imageSwiper = ImageSwiper() 로 해도 오류가 안 뜨는데 나는 Private val imageSwiper : ImageSwiper = ImageSwiper() 로 수정해야 했다. 왜지?

 

chat GPT 답변:`Private val imageSwiper : ImageSwiper = ImageSwiper()`와 `Private val imageSwiper = ImageSwiper()`는 Kotlin에서 변수를 초기화하는 두 가지 다른 방법 중 하나를 나타냅니다.

1. `Private val imageSwiper = ImageSwiper()`: 이 방식은 Kotlin에서 타입 추론을 통해 변수의 타입을 추측하도록 하는 방법입니다. Kotlin은 변수의 오른쪽에 할당된 값의 타입을 보고 변수의 타입을 추론합니다. 따라서 `ImageSwiper`의 생성자를 호출하여 해당 클래스의 인스턴스를 변수에 할당하면 변수의 타입은 `ImageSwiper`로 추론됩니다.

2. `Private val imageSwiper : ImageSwiper = ImageSwiper()`: 이 방식은 변수의 타입을 명시적으로 선언하는 방법입니다. 여기서 변수 `imageSwiper`의 타입을 명시적으로 `ImageSwiper`로 지정하고, 이 변수를 `ImageSwiper` 클래스의 인스턴스로 초기화합니다.

둘 중 어느 것을 사용해도 프로그램이 동작할 것이며, 선택은 개발자의 스타일과 선호에 따라 다를 수 있습니다. Kotlin의 타입 추론 기능을 활용하면 코드를 간결하게 작성할 수 있고, 타입을 명시적으로 선언하는 것은 가독성과 명확성을 높일 수 있습니다. 따라서 두 방법 중 하나를 선택하여 사용하면 됩니다.

 

 

 

 

 

 

 

 

 

 

 

앞으로는 1., 2., 3., 등의 번호에는 함수나 프래그먼트 이름 쓰고

그 아래에 1-1, 1-2 등으로 세부 구현할 주제 느낌으로 쓰기