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》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

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

重来2

重来2

[美] 贾森·弗里德、[美] 戴维·海涅迈尔·汉森 / 苏西 / 中信出版社 / 2014-4-8 / 39.00元

“不再需要办公室”,这不仅仅是未来才有的事——它已经发生了。现在,轮到你迈开脚步,跟上时代的步伐了。 上百万的员工和成千上万的企业已经发现了远程工作的乐趣和好处。然而,远程工作方式还没有成为常见的选择。事实上,远程工作的技术手段都已齐备。还没有升级换代的,是人们的思想。 这本书的目的就是帮你把想法升级换代。作者会向你展示远程工作的诸多好处:可以找到最优秀的人才,从摧残灵魂的通勤路上解脱......一起来看看 《重来2》 这本书的介绍吧!

图片转BASE64编码
图片转BASE64编码

在线图片转Base64编码工具

HTML 编码/解码
HTML 编码/解码

HTML 编码/解码

XML 在线格式化
XML 在线格式化

在线 XML 格式化压缩工具