What is the proper way to cancel coroutines with common mutex

Issue

I have run into this problem.

I have (at least) 6 coroutines which works on a map which is managed through a mutex.

Sometimes I need to cancel one, more or all the coroutines in different scenarios.

What is the best way to cope with the mutex when cancelling the coroutine(s) ? (The fact is that I really don’t know if the cancelling coroutine was the one which locked the mutex). Do the mutex “system” has any neat trick to cope with this ?


ADDITION 2021.09.30 11:28 GMT+2 (DST)

My coding is fairly complex, so I simplify it and show the main problem here

... 
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

Solution

Generally, a cancel is a normal exception, which you can just catch in order to run clean-up operations, you can see the example on closing resources.

In addition, since you can still by cancelled during clean-up, for critical operations you can prevent further cancellation. Put together your job can have something like:

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

I think NonCancellable is overdoing it in the case of only an unlock since it is supposed to be atomic, but I am not sure. If this is the case, I just looked up this pattern and apparently this is so common they have something more nifty:

mutex.withLock {
    // locked stuff
}

As it says in the link

There is also withLock extension function that conveniently represents mutex.lock(); try { ... } finally { mutex.unlock() } pattern.

Answered By – kabanus

Leave a Comment