Multithreading in Kotlin Multiplatform Apps

栏目: IT技术 · 发布时间: 4年前

内容简介:In the past few days, I began looking at multithreading in Kotlin Multiplatform more carefully when I started a new iOS/Android project that I wanted to share business logic for. I realized that, despite understanding the rules of multithreading in Kotlin

In the past few days, I began looking at multithreading in Kotlin Multiplatform more carefully when I started a new iOS/Android project that I wanted to share business logic for. I realized that, despite understanding the rules of multithreading in Kotlin Native, I didn’t fully grasp the implications thereof. I thought to write this blog post to share what I learned (and as a reference for my future self).

Note - the excellent Practical Kotlin Native Concurrency series from Kevin goes through a lot of these concepts in a lot more detail - I highly recommend reading the series. I hope that this post adds some value to that series by presenting some additional examples, especially viewed from the perspective of someone trying to use shared Kotlin multiplatform code in an iOS app.

The Rules and Concepts

As a review, the rules are that an object is either:

  • immutable (and therefore can be shared across multiple threads), or
  • mutable (and is confined to a single thread)

One important concept that applies heavily here:

  • freeze - freezes an object to be shared between threads - a frozen object is immutable (for the most part) - more on this later.

So let’s take some examples and see how things play out.

The Immutable Case

Singleton Objects

Let’s say that we have written the following multiplatform code:

object MathUtil {
   fun square(input: Int) = input * input
}

From Swift’s main thread, we can do:

// note - MathUtil() doesn't make a new instance, it's just
// Swift syntax for getting the instance of the object.
print(NSString(format: "number^2 is: %d", MathUtil().square(4)))

We can also do:

DispatchQueue.global(qos: .background).async {
   print(NSString(format: "number^2 from bg is: %d", MathUtil().square(4)))
}

This second example, while obvious in retrospect, is one I never realized before - you can actually call Kotlin multiplatform code from multiple threads on iOS if the code is immutable. This is a super useful building block that can be used for more complicated examples.

Note that this also works if MathUtil has some immutable val properties in there, because Kotlin Native will freeze those properties by default. This is an important point that will come into play later.

Class Instances

Let’s change things around a bit and consider a normal class (not an object /singleton).

// common/src/commonMain/kotlin/net/cafesalam/test/NumericOperations.kt

class NumericOperations(private val amount: Int) {
  fun square() = amount * amount
}

In iOS, we can use a fresh instance from this class in any thread:

let ops = NumericOperations(amount: 42)
print(NSString(format: "42^2 from main: %d", ops.square()))

DispatchQueue.global(qos: .background).async {
  let bgOps = NumericOperations(amount: 10)
  print(NSString(format: "10^2 from background: %d", bgOps.square()))
}

Now what if we replaced bgOps.square() in the body of the async lambda in the snippet above with ops.square() ? We’d get a crash:

Uncaught Kotlin exception: kotlin.native.IncorrectDereferenceException: illegal attempt to access non-shared net.cafesalam.test.common.NumericOperations@337f9a8 from other thread
        at 0   SharedCode                          0x0000000105ca0777 kfun:kotlin.Throwable.<init>(kotlin.String?)kotlin.Throwable + 87 (/Users/teamcity1/teamcity_work/4d622a065c544371/runtime/src/main/kotlin/kotlin/Throwable.kt:22:37)
        at 1   SharedCode                          0x0000000105c93545

This is saying that we cannot use the instance of NumericOperations that we made on one thread on another, because it’s a non-shared instance.

But why? Doesn’t the first rule say that immutable objects (which our class instance clearly is), can be shared across multiple threads? To quote the Stranger Threads post:

As far as the KN runtime is concerned, all non-frozen state is possibly mutable, and restricted to one thread.

So let’s suppose (for whatever reason) that we actually wanted to share the same instance across multiple threads - we can do this by freezing the instance. To do this, we can do something like this:

// common/src/iosMain/kotlin/net/cafesalam/test/Freezer.kt

object Freezer {
  fun frozenNumericOperations(amount: Int) = NumericOperations(amount).freeze()
}

Using this class, we can now share the instance across two threads:

// main thread
let frozenOps = Freezer().frozenNumericOperations(amount: 42)
print(frozenOps.square())

DispatchQueue.global(qos: .background).async {
  print(frozenOps.square())
}

The Mutable Case

So far, all the examples have only dealt with immutable data and data that could easily be frozen - let’s try adding some mutable state into the mix.

Singleton Objects

Let’s consider this example -

object Counter {
  var count: Int = 0
}

If we try to use this from Swift from the main thread:

let counter = Counter()
counter.count = 42
print(counter.count)

When we try to modify count , we get a crash -

Uncaught Kotlin exception: kotlin.native.concurrent.InvalidMutabilityException: mutation attempt of frozen net.cafesalam.test.common.SecondTestClass@a6abe8
        at 0   SharedCode                          0x000000010f42a777 kfun:kotlin.Throwable.<init>(kotlin.String?)kotlin.Throwable + 87 (/Users/teamcity1/teamcity_work/4d622a065c544371/runtime/src/main/kotlin/kotlin/Throwable.kt:22:37)

This is because the properties of an object are all frozen by default. We can override this behavior by using the @ThreadLocal annotation on the object . @ThreadLocal says that each thread gets its own copy of this object.

Changing the code to instead look like this fixes the issue:

@ThreadLocal
object Counter {
  var count: Int = 0
}

Now on iOS, we can do something like:

let counter = Counter()
counter.count = 42

DispatchQueue.global(qos: .background).async {
   let secondCounter = Counter()
   secondCounter.count = 43
   // original counter is still at 42 because of ThreadLocal
}

Note that if we were to try to share an instance between the ui thread and a background thread, we’d get an exception (due to the rules - if it’s not immutable, it must be confined to a single thread).

Class Instances

For normal classes with mutable variables, things are pretty straightforward - these instances are confined to the thread they were made on per the rules.

What if we want to have something be mutable but also be usable from multiple threads? How can we make that work?

Remember back to the point about freeze , when I mentioned that “a frozen object is immutable (for the most part).” The for the most part piece is because Kotlin Native has several interesting types - the atomic types. To quote the documentation :

Atomic values and freezing: atomics AtomicInt, AtomicLong, AtomicNativePtr and AtomicReference are unique types with regard to freezing. Namely, they provide mutating operations, while can participate in frozen subgraphs. So shared frozen objects can have fields of atomic types.

These types can exist within a frozen type (therefore being frozen) and can still be modified. So for example, we can have some shared code that looks like this:

// common/src/iosMain/kotlin/net/cafesalam/test/AtomicCounter.kt

object AtomicCounter {
  private val count = AtomicInt(0)

  fun get(): Int = count.value
  fun increase(): Int = count.addAndGet(1)
}

We can then use this on iOS from multiple threads:

// main thread
let atomic = AtomicCounter()

// first increment
atomic.increase()

DispatchQueue.global(qos: .background).async {
  // object is singleton, so this is the same instance as if we were
  // to just use atomic here directly.
  let counter = AtomicCounter()
  print(counter.get()) // prints 1
  counter.increase()
  print(counter.get()) // prints 2
}

Using these, especially AtomicReference , makes life a lot easier. Note that, like freeze , these types are only available in native code (i.e. not on jvm).

Global State

Top level values are (by default) declared on the main thread, preventing them from being used on other threads. Consider:

// common/src/commonMain/kotlin/net/cafesalam/test/Sample.kt

private val EMPTY_DATA = emptyArray<Any?>()

class TestClass() {
  private var data = EMPTY_DATA

  fun isEmpty() = data.isEmpty()
}

We can make an instance of TestClass on the main thread and use it, but we cannot make a fresh instance on a background thread. If we try to, we’d get an exception:

Uncaught Kotlin exception: kotlin.native.IncorrectDereferenceException: Trying to access top level value not marked as @ThreadLocal or @SharedImmutable from non-main thread
        at 0   SharedCode                          0x00000001045f15d7 kfun:kotlin.Throwable.<init>(kotlin.String?)kotlin.Throwable + 87 (/Users/teamcity1/teamcity_work/4d622a065c544371/runtime/src/main/kotlin/kotlin/Throwable.kt:22:37)

We can use the @ThreadLocal or @SharedImmutable annotations on the top level variable to fix this issue (depending on the behavior we want). @SharedImmutable says that this variable is immutable and therefore can be shared across threads (essentially making it frozen).

Other Points

Note that, up until now, there has been no mention of the Worker class, nor of coroutines - this is pretty neat because, even without these, the existing building blocks allow us to run code in multiple threads on the target system itself.

It would be great to write shared code that does multithreading for us as well directly in commonMain . Both the Worker class and coroutines can be used to do this.

Here’s a small example using coroutines:

object BackgroundCalculator {
  fun doSomeWork(param: Param, callback: ((Result) -> Unit)) {
     GlobalScope.launch {
       val result = withContext(Dispatchers.Default) {
          // heavy operation here that returns a Result
       }

       withContext(Dispatchers.Main) {
         lambda(result)
       }
     }
  }
}

Using the org.jetbrains.kotlinx:kotlinx-coroutines-core-common:1.3.5-native-mt artifact (or something newer), Dispatchers.Default is now backed by a single background thread, whereas Dispatchers.Main will point to the correct main thread on iOS / Android.

In this example, calling BackgroundCalculator.doSomeWork will return right away. Some time later, once the heavy calculation is done, it will call the callback that is passed in from the main thread. A sample usage could look like this:

BackgroundCalculator().doSomeWork(param: parameter) { (result: [Result]) in
  // this will be called on the main thread 
  print(result)
}

Note that objects will be frozen when they are transferred between threads using withContext . This also shows how the basic rules that we mentioned at the very start of the article apply, even with context of coroutines and workers.

Tips

Q: Why couldn’t the cows moo at the same time? A: Because they had a mootex.

isFrozen / ensureNeverFrozen

isFrozen and ensureNeverFrozen are your friends for debugging things. These are available in native code only, but using Stately ’s common artifact, you can use them in common code only. (Note - Stately exposes these in common by implementing them as expect , where the actual implementation on native calls to the actual isFrozen / ensureNeverFrozen , and otherwise just returns false or does nothing).

i expect -ed this to be fun , but I didn’t realize that it would actual ly be fun .

Unit tests

Unit tests can help find many issues - for example:

class SampleTests {
  object MutableObject { var count = 0 }

  @Test
  fun testMutableObject() {
    MutableObject.count = 42
    assertEquals(MutableObject.count, 42)
  }
}

This innocent looking test will fail with InvalidMutabilityException if you run it on iOS (i.e. ./gradlew :common:iosTest ). We fix this by adding @ThreadLocal above the object declaration as mentioned above.

I learned this (and some other tricks - like how to test and catch the issue of global state without annotations not being accessible off the ui thread) - from this post from Jake Wharton. I really recommend reading it.

Special thanks to Kevin Galligan for reviewing this post.


以上所述就是小编给大家介绍的《Multithreading in Kotlin Multiplatform Apps》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

大数据系统构建

大数据系统构建

Nathan Marz、James Warren / 马延辉、向磊、魏东琦 / 机械工业出版社 / 2017-1 / 79.00

随着社交网络、网络分析和智能型电子商务的兴起,传统的数据库系统显然已无法满足海量数据的管理需求。 作为一种新的处理模式,大数据系统应运而生,它使用多台机器并行工作,能够对海量数据进行存储、处理、分析,进而帮助用户从中提取对优化流程、实现高增长率的有用信息,做更为精准有效的决策。 但不可忽略的是,它也引入了大多数开发者并不熟悉的、困扰传统架构的复杂性问题。 本书将教你充分利用集群硬件优势的La......一起来看看 《大数据系统构建》 这本书的介绍吧!

随机密码生成器
随机密码生成器

多种字符组合密码

MD5 加密
MD5 加密

MD5 加密工具

正则表达式在线测试
正则表达式在线测试

正则表达式在线测试