【问题标题】:What is the proper way to cancel coroutines with common mutex用普通互斥体取消协程的正确方法是什么
【发布时间】:2021-11-21 06:13:35
【问题描述】:

我遇到了这个问题。

我有(至少)6 个协程,它们在通过互斥锁管理的地图上工作。

有时我需要在不同的场景中取消一个、多个或所有协程。

取消协程时处理互斥锁的最佳方法是什么? (事实是我真的不知道取消协程是否是锁定互斥锁的协程)。互斥体“系统”有什么巧妙的技巧来解决这个问题吗?


2021.09.30 11:28 GMT+2 (DST)

我的编码相当复杂,所以我将其简化并在这里展示主要问题

... 
class HomeFragment:Fragment(){
...
private lateinit var googleMap:GoogleMap

val mapMutex = Mutex()
...

override fun onViewCreated(view:View, savedInstanceState: Bundle?) {
...
binding.fragmentHomeMapView?.geMapAsync { _googleMap ->

  _googleMap?.let{ safeGoogleMap ->
    googleMap = safeGoogleMap
  }?:let{
    Message.error("Error creating map (null)") 
  }

  ...
   
  homeViewModel.apply {
    ...
    //observer & coroutine 1 
    liveDataMapFlagged?.observe(
      viewLifeCycleOwner
    ){flaggedMapDetailResult->

      //Here I want to stop the lifecycleScope job below if it is already 
      //running and do some cleanup before entering (do I need to access the
      //mutex if cleanup influence the google map ?)
      //If I cancel the job, will the mutex then unlock gracefully ?

      flaggedMapDetailResult?.apply {
        ...
        lifecycleScope.launchWhenStarted { //Here I want to catch the job with i.e 'flagJob = lifeCycleScope.launchWhe...'  
          ...
          withContext(Dispatchers.Default){
            ...
            mapMutex.withLock {   //suspends if locked
              withContext(Dispatchers.Main){
                selectedSiteMarker?.remove()
                selectedCircle?.remove() 
                ... // Doing some cleanup... removing markers
              }
              ... // Creating new markers
              var flaggedSiteMarkerLatLng = coordinateSiteLatitude?.let safeLatitude@{safeLatitude->
                 return@safeLatitude coordinateSiteLongitude?.let safeLongitude@{safeLongitude->
                 return@safeLongitude LatLng(safeLatitude,safeLongitude)
                 }
              }
              ...
              flaggedSiteMarkerLatLng?.let { safeFlaggedSiteMarkerLatLng ->
                val selectedSiteOptions =     
                  MarkerOptions()
                    .position(safeFlaggedSiteMarkerLatLng)
                    .anchor(0.5f,0.5f)
                    .visible(flaggedMarkerState)
                    .flat(true)
                    .zIndex(10f)
                    .title(setTicketNumber(ticketNumber))
                    .snippet(appointmentName?:"Name is missing")
                    .icon(vSelectedSiteIcon)

              selectedSiteMarker = withContext(Dispatchers.Main){
                googleMap.addMarker(selectedSiteOptions)?.also{
                  it.tag = siteId
                }
              }
              ... //Do some more adding

            } //End mutex
            ...
          }//End dispatchers default
          ...
        }//End lifecycleScope.launchWhenStarted
        ...
      }?:let{//End apply
        ...//Cleanup if no data present
        lifeCycleScope.launchWhenStarted{ //Shoud harvest Job and stop above
                                          //if it is called before ending...
                                          //if necessary
          mapMutex.withLock{
            //Cleanup markers         
          }
        } 
      }
      ...
    }//End observer 1


    //observer 2
    liveDataMapListFromFiltered2?.observer(
      viewLifeCycleOwner
    ){mapDetailList ->

      //Should check if job below is running and cancel gracefully and
      //clean up data 

      ...//Do some work on mapDetailList and create new datasets
      lifecycleScope.launchWhenStarted{ //Scope start (should harvest job)
        ...
        withContext(Dispatchers.Default) //Default context
        {
           ...//Do some heavy work on list (no need for mutex)
           
        }

        mapMutex.withLock {
          withContext(Dispatchers.Main)
          {
            //Do work on googlemap. Move camera etc.
          } 

        }

        ...//Do other not map related work
        
        mapMutex.withLock {
          withContext(Dispatchers.Main)
          {
            //Do work on googlemap. Move camera etc.
          } 

        }

        ...//Do other not map related work

        mapMutex.withLock {
          withContext(Dispatchers.Main)
          {
            //Do work on googlemap. Move camera etc.
          } 
        }//end mutex
      }//end scope 
    }//end observer 2 
  }//end viewmode
}//end gogleMap
     
 

【问题讨论】:

  • 您对取消和互斥有什么顾虑?您是否担心取消当前持有锁的协程后,它不会被正确释放?或者取消会发生在修改一些数据的过程中,所以可能会留下不一致的状态?
  • 我管理的数据的一致性,但是互斥锁是否被我正在取消的协程锁定的事实更糟。
  • 我刚刚添加了代码场景,其中问题显示了 3 个主要的协程场景。

标签: kotlin mutex coroutine


【解决方案1】:

一般来说,cancel 是一个正常的异常,您可以捕获它以便运行清理操作,您可以查看 closing resources 上的示例。

此外,由于您仍然可以在清理期间取消,因此对于关键操作您可以prevent further cancellation。把你的工作放在一起可以有这样的东西:

my_mutex.lock()
try {
    // locked stuff
} finally {
    withContext(NonCancellable) {
        // clean up
        my_mutex.unlock()
    }
}

我认为NonCancellable 在仅解锁的情况下做得过火,因为它应该是原子的,但我不确定。如果是这种情况,我只是查找了这个模式,显然这很常见,他们有一些东西more nifty

mutex.withLock {
    // locked stuff
}

正如链接中所说的

还有 withLock 扩展函数,方便表示mutex.lock(); try { ... } finally { mutex.unlock() } 模式。

【讨论】:

  • 啊哈..我正在使用`mutex.withLock { ... },我将在上面的帖子中解释更多。
  • @RoarGrønmo 然后看起来你很好。这是处理您的问题的“最佳”或惯用方式。
  • @RoarGrønmo 就像我写的那样,答案是肯定的。
  • @RoarGrønmo 呵呵,我给茶勺加糖。是的,即使取消。联系withLock 的全部意义在于执行我在答案开头所写的操作 - 捕获取消异常(当您取消它时会在作业中引发异常),解锁互斥锁,然后作业才结束。这是它的主要优点之一。
  • 就像@kabanus 所说,这正是协程取消和线程中断通过抛出异常而不是仅仅“杀死”后台任务来工作的原因。这是为了能够进行适当的清理。这也是为什么像use() (try-with-resources) 或withLock() 这样的资源处理机制在finally 块中进行清理的原因。 Java/Kotlin 始终关注代码的可靠性。
【解决方案2】:

我已接受 @kabanus 对我的问题的回答,因为它会导致我的代码发生工作变化。

这个概念是(我不明白),如果互斥锁的格式为mutex.withLock{ ... },那么当拥抱协程作业被取消时,互斥锁会自动解锁。

从概念上看是这样的:

class HomeFragment : Fragment(){
  //...
  val commonMutex = Mutex()
  //...
  override fun onViewCreated(view:View, savedInstanceState:Bundle?){
    super.onViewCreated(view, savedInstanceState)
    //...
    val job1:Job?=null
    val job2:Job?=null
    val job3:Job?=null
    //...
    binding.fragmentHomeMyView?.getMyAsyncView{ 
      //could be any view with async work like i.e async GoogleMaps 
      binding.apply{ //I like to let bindings embrace if they exists.
        //...
        homeViewModel.apply{ //like to let homeViewModel embrace if they exists
          //...
          liveDataSet1?.observe( //LiveData set 1 observer
            viewLifecycleOwner
          ){dataSetResult1->
            //Will check if my lengthy coroutine job 1 is still running
            //If it is -> cancel it, since the observer provides new dataset
            //Note ! If your dataset is meant to be mutable, you should do a 
            //dataset copy after the cancellation so it doesn't overrun it on next
            //observer update
            //...
            if(job1?.isActive == true){ 
              job1?.cancel()
            } 
            //...
            dataSetResult1?.apply{ //like to let the dataSet embrace if there are 
                                   //many members
              job1 = lifecycleScope.launchWhenStarted{
                //I used lifecycleScope here, you can use other coroutine "bases"
                withContext(Dispatchers.Default){
                  //Doing heavy work which doesn't imply a mutex situation
                  
                  commonMutex.withLock{ //Locking mutex section 
                    //Work on data which should be shared between two or more 
                    //coroutines 
                    withContext(Dispatchers.Main){
                      //Do screenupdates if necessary
                    } //end context main
                
                  } //end commonMutex.withLock

                }//end context default
                        
              }//end coroutine Job (lifecycleScope)
            } //end dataSetResult1 apply
          } //end dataSetResult1 observer 

          liveDataSet2?.observe( //LiveData set 2 observer
            viewLifecycleOwner
          ){dataSetResult2->
            //Will check if my lengthy coroutine job 2 is still running
            //If it is -> cancel it, since the observer provides new dataset
            //Note ! If your dataset is meant to be mutable, you should do a 
            //dataset copy after the cancellation so it doesn't overrun it on next
            //observer update
            //...
            if(job2?.isActive == true){ 
              job2?.cancel()
            } 
            //...
            dataSetResult2?.apply{ //like to let the dataSet embrace if there are 
                                   //many members
              job1 = lifecycleScope.launchWhenStarted{
                //I used lifecycleScope here, you can use other coroutine "bases"
                withContext(Dispatchers.Default){
                  //Doing heavy work which doesn't imply a mutex situation
                  
                  commonMutex.withLock{ //Locking mutex section 
                    //Work on data which should be shared between two or more 
                    //coroutines 
                    withContext(Dispatchers.Main){
                      //Do screenupdates if necessary
                    } //end context main
                
                  } //end commonMutex.withLock

                }//end context default
                        
              }//end coroutine Job (lifecycleScope)
            } //end dataSetResult2 apply
          } //end dataSetResult2 observer 

          liveDataSet3?.observe( //LiveData set 3 observer
            viewLifecycleOwner
          ){dataSetResult3->
            //Will check if my lengthy coroutine job 3 is still running
            //If it is -> cancel it, since the observer provides new dataset
            //Note ! If your dataset is meant to be mutable, you should do a 
            //dataset copy after the cancellation so it doesn't overrun it on next
            //observer update
            //...
            if(job3?.isActive == true){ 
              job3?.cancel()
            } 
            //...
            dataSetResult3?.apply{ //like to let the dataSet embrace if there are 
                                   //many members
              job3 = lifecycleScope.launchWhenStarted{
                //I used lifecycleScope here, you can use other coroutine "bases"
                withContext(Dispatchers.Default){
                  //Doing heavy work which doesn't imply a mutex situation
                  
                  commonMutex.withLock{ //Locking mutex section 
                    //Work on data which should be shared between two or more 
                    //coroutines 
                    withContext(Dispatchers.Main){
                      //Do screenupdates if necessary
                    } //end context main
                
                  } //end commonMutex.withLock

                }//end context default
                        
              }//end coroutine Job (lifecycleScope)
            } //end dataSetResult3 apply
          } //end dataSetResult3 observer 
        } //end homeViewModel.apply
      } //end binding.apply
    } //end asyncView
  } //end onViewCreated
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-10-03
    • 2012-07-06
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多