@@ -78,8 +78,10 @@ This is a short guide on core features of `kotlinx.coroutines` with a series of
78
78
* [ Buffered channels] ( #buffered-channels )
79
79
* [ Shared mutable state and concurrency] ( #shared-mutable-state-and-concurrency )
80
80
* [ The problem] ( #the-problem )
81
+ * [ Volatiles are of no help] ( #volatiles-are-of-no-help )
81
82
* [ Thread-safe data structures] ( #thread-safe-data-structures )
82
- * [ Thread confinement] ( #thread-confinement )
83
+ * [ Thread confinement fine-grained] ( #thread-confinement-fine-grained )
84
+ * [ Thread confinement coarse-grained] ( #thread-confinement-coarse-grained )
83
85
* [ Mutual exclusion] ( #mutual-exclusion )
84
86
* [ Actors] ( #actors )
85
87
* [ Select expression] ( #select-expression )
@@ -1484,49 +1486,52 @@ but others are unique.
1484
1486
1485
1487
### The problem
1486
1488
1487
- Let us launch 100k coroutines all doing the same action. We'll also measure their completion time for
1488
- further comparisons:
1489
+ Let us launch a thousand coroutines all doing the same action thousand times (for a total of a million executions).
1490
+ We'll also measure their completion time for further comparisons:
1489
1491
1490
1492
<!-- - INCLUDE .*/example-sync-([0-9]+).kt
1493
+ import kotlin.coroutines.experimental.CoroutineContext
1491
1494
import kotlin.system.measureTimeMillis
1492
1495
-->
1493
1496
1494
- <!-- - INCLUDE .*/example-sync-02 .kt
1497
+ <!-- - INCLUDE .*/example-sync-03 .kt
1495
1498
import java.util.concurrent.atomic.AtomicInteger
1496
1499
-->
1497
1500
1498
- <!-- - INCLUDE .*/example-sync-04 .kt
1501
+ <!-- - INCLUDE .*/example-sync-06 .kt
1499
1502
import kotlinx.coroutines.experimental.sync.Mutex
1500
1503
-->
1501
1504
1502
- <!-- - INCLUDE .*/example-sync-05 .kt
1505
+ <!-- - INCLUDE .*/example-sync-07 .kt
1503
1506
import kotlinx.coroutines.experimental.channels.*
1504
1507
-->
1505
1508
1506
1509
``` kotlin
1507
- suspend fun massiveRun (action : suspend () -> Unit ) {
1508
- val n = 100_000
1510
+ suspend fun massiveRun (context : CoroutineContext , action : suspend () -> Unit ) {
1511
+ val n = 1000 // number of coroutines to launch
1512
+ val k = 1000 // times an action is repeated by each coroutine
1509
1513
val time = measureTimeMillis {
1510
1514
val jobs = List (n) {
1511
- launch(CommonPool ) {
1512
- action()
1515
+ launch(context ) {
1516
+ repeat(k) { action() }
1513
1517
}
1514
1518
}
1515
1519
jobs.forEach { it.join() }
1516
1520
}
1517
- println (" Completed in $time ms" )
1521
+ println (" Completed ${n * k} actions in $time ms" )
1518
1522
}
1519
1523
```
1520
1524
1521
1525
<!-- - INCLUDE .*/example-sync-([0-9]+).kt -->
1522
1526
1523
- We start with a very simple action that increments a shared mutable variable.
1527
+ We start with a very simple action that increments a shared mutable variable using
1528
+ multi-threaded [ CommonPool] context.
1524
1529
1525
1530
``` kotlin
1526
1531
var counter = 0
1527
1532
1528
1533
fun main (args : Array <String >) = runBlocking<Unit > {
1529
- massiveRun {
1534
+ massiveRun( CommonPool ) {
1530
1535
counter++
1531
1536
}
1532
1537
println (" Counter = $counter " )
@@ -1535,40 +1540,73 @@ fun main(args: Array<String>) = runBlocking<Unit> {
1535
1540
1536
1541
> You can get full code [ here] ( kotlinx-coroutines-core/src/test/kotlin/guide/example-sync-01.kt )
1537
1542
1538
- <!-- - TEST lines.size == 2 && lines[1].startsWith("Counter = ") -->
1543
+ <!-- - TEST LINES_START
1544
+ Completed 1000000 actions in
1545
+ Counter =
1546
+ -->
1547
+
1548
+ What does it print at the end? It is highly unlikely to ever print "Counter = 1000000", because a thousand coroutines
1549
+ increment the ` counter ` concurrently from multiple threads without any synchronization.
1550
+
1551
+ ### Volatiles are of no help
1539
1552
1540
- What does it print at the end? It is highly unlikely to ever print "100000", because all the
1541
- 100k coroutines increment the ` counter ` concurrently without any synchronization.
1553
+ There is common misconception that making a variable ` volatile ` solves concurrency problem. Let us try it:
1554
+
1555
+ ``` kotlin
1556
+ @Volatile // in Kotlin `volatile` is an annotation
1557
+ var counter = 0
1558
+
1559
+ fun main (args : Array <String >) = runBlocking<Unit > {
1560
+ massiveRun(CommonPool ) {
1561
+ counter++
1562
+ }
1563
+ println (" Counter = $counter " )
1564
+ }
1565
+ ```
1566
+
1567
+ > You can get full code [ here] ( kotlinx-coroutines-core/src/test/kotlin/guide/example-sync-02.kt )
1568
+
1569
+ <!-- - TEST LINES_START
1570
+ Completed 1000000 actions in
1571
+ Counter =
1572
+ -->
1573
+
1574
+ This code works slower, but we still don't get "Counter = 1000000" at the end, because volatile variables guarantee
1575
+ linearizable (this is a technical term for "atomic") reads and writes to the corresponding variable, but
1576
+ do not provide atomicity of larger actions (increment in our case).
1542
1577
1543
1578
### Thread-safe data structures
1544
1579
1545
1580
The general solution that works both for threads and for coroutines is to use a thread-safe (aka synchronized,
1546
1581
linearizable, or atomic) data structure that provides all the necessarily synchronization for the corresponding
1547
1582
operations that needs to be performed on a shared state.
1548
- In the case of a simple counter we can use ` AtomicInteger ` class:
1583
+ In the case of a simple counter we can use ` AtomicInteger ` class which has atomic ` incrementAndGet ` operations :
1549
1584
1550
1585
``` kotlin
1551
1586
var counter = AtomicInteger ()
1552
1587
1553
1588
fun main (args : Array <String >) = runBlocking<Unit > {
1554
- massiveRun {
1589
+ massiveRun( CommonPool ) {
1555
1590
counter.incrementAndGet()
1556
1591
}
1557
1592
println (" Counter = ${counter.get()} " )
1558
1593
}
1559
1594
```
1560
1595
1561
- > You can get full code [ here] ( kotlinx-coroutines-core/src/test/kotlin/guide/example-sync-02 .kt )
1596
+ > You can get full code [ here] ( kotlinx-coroutines-core/src/test/kotlin/guide/example-sync-03 .kt )
1562
1597
1563
- <!-- - TEST lines.size == 2 && lines[1] == "Counter = 100000" -->
1598
+ <!-- - TEST ARBITRARY_TIME
1599
+ Completed 1000000 actions in xxx ms
1600
+ Counter = 1000000
1601
+ -->
1564
1602
1565
1603
This is the fastest solution for this particular problem. It works for plain counters, collections, queues and other
1566
1604
standard data structures and basic operations on them. However, it does not easily scale to complex
1567
1605
state or to complex operations that do not have ready-to-use thread-safe implementations.
1568
1606
1569
- ### Thread confinement
1607
+ ### Thread confinement fine-grained
1570
1608
1571
- Thread confinement is an approach to the problem of shared mutable state where all access to the particular shared
1609
+ _ Thread confinement _ is an approach to the problem of shared mutable state where all access to the particular shared
1572
1610
state is confined to a single thread. It is typically used in UI applications, where all UI state is confined to
1573
1611
the single event-dispatch/application thread. It is easy to apply with coroutines by using a
1574
1612
single-threaded context:
@@ -1578,18 +1616,51 @@ val counterContext = newSingleThreadContext("CounterContext")
1578
1616
var counter = 0
1579
1617
1580
1618
fun main (args : Array <String >) = runBlocking<Unit > {
1581
- massiveRun {
1582
- run (counterContext) {
1619
+ massiveRun( CommonPool ) { // run each coroutine in CommonPool
1620
+ run (counterContext) { // but confine each increment to the single-threaded context
1583
1621
counter++
1584
1622
}
1585
1623
}
1586
1624
println (" Counter = $counter " )
1587
1625
}
1588
1626
```
1589
1627
1590
- > You can get full code [ here] ( kotlinx-coroutines-core/src/test/kotlin/guide/example-sync-03.kt )
1628
+ > You can get full code [ here] ( kotlinx-coroutines-core/src/test/kotlin/guide/example-sync-04.kt )
1629
+
1630
+ <!-- - TEST ARBITRARY_TIME
1631
+ Completed 1000000 actions in xxx ms
1632
+ Counter = 1000000
1633
+ -->
1634
+
1635
+ This code works very slowly, because it does _ fine-grained_ thread-confinement. Each individual increment switches
1636
+ from multi-threaded ` CommonPool ` context to the single-threaded context using [ run] block.
1637
+
1638
+ ### Thread confinement coarse-grained
1639
+
1640
+ In practice, thread confinement is performed in large chunks, e.g. big pieces of state-updating business logic
1641
+ are confined to the single thread. The following example does it like that, running each coroutine in
1642
+ the single-threaded context to start with.
1591
1643
1592
- <!-- - TEST lines.size == 2 && lines[1] == "Counter = 100000" -->
1644
+ ``` kotlin
1645
+ val counterContext = newSingleThreadContext(" CounterContext" )
1646
+ var counter = 0
1647
+
1648
+ fun main (args : Array <String >) = runBlocking<Unit > {
1649
+ massiveRun(counterContext) { // run each coroutine in the single-threaded context
1650
+ counter++
1651
+ }
1652
+ println (" Counter = $counter " )
1653
+ }
1654
+ ```
1655
+
1656
+ > You can get full code [ here] ( kotlinx-coroutines-core/src/test/kotlin/guide/example-sync-05.kt )
1657
+
1658
+ <!-- - TEST ARBITRARY_TIME
1659
+ Completed 1000000 actions in xxx ms
1660
+ Counter = 1000000
1661
+ -->
1662
+
1663
+ This now works much faster and produces correct result.
1593
1664
1594
1665
### Mutual exclusion
1595
1666
@@ -1603,7 +1674,7 @@ val mutex = Mutex()
1603
1674
var counter = 0
1604
1675
1605
1676
fun main (args : Array <String >) = runBlocking<Unit > {
1606
- massiveRun {
1677
+ massiveRun( CommonPool ) {
1607
1678
mutex.lock()
1608
1679
try { counter++ }
1609
1680
finally { mutex.unlock() }
@@ -1612,9 +1683,16 @@ fun main(args: Array<String>) = runBlocking<Unit> {
1612
1683
}
1613
1684
```
1614
1685
1615
- > You can get full code [ here] ( kotlinx-coroutines-core/src/test/kotlin/guide/example-sync-04.kt )
1686
+ > You can get full code [ here] ( kotlinx-coroutines-core/src/test/kotlin/guide/example-sync-06.kt )
1687
+
1688
+ <!-- - TEST ARBITRARY_TIME
1689
+ Completed 1000000 actions in xxx ms
1690
+ Counter = 1000000
1691
+ -->
1616
1692
1617
- <!-- - TEST lines.size == 2 && lines[1] == "Counter = 100000" -->
1693
+ The locking in this example is fine-grained, so it pays the price. However, it is a good choice for some situations
1694
+ where you absolutely must modify some shared state periodically, but there is no natural thread that this state
1695
+ is confined to.
1618
1696
1619
1697
### Actors
1620
1698
@@ -1643,7 +1721,7 @@ fun counterActor(request: ReceiveChannel<CounterMsg>) = launch(CommonPool) {
1643
1721
fun main (args : Array <String >) = runBlocking<Unit > {
1644
1722
val request = Channel <CounterMsg >()
1645
1723
counterActor(request)
1646
- massiveRun {
1724
+ massiveRun( CommonPool ) {
1647
1725
request.send(IncCounter )
1648
1726
}
1649
1727
val response = Channel <Int >()
@@ -1652,14 +1730,20 @@ fun main(args: Array<String>) = runBlocking<Unit> {
1652
1730
}
1653
1731
```
1654
1732
1655
- > You can get full code [ here] ( kotlinx-coroutines-core/src/test/kotlin/guide/example-sync-05 .kt )
1733
+ > You can get full code [ here] ( kotlinx-coroutines-core/src/test/kotlin/guide/example-sync-07 .kt )
1656
1734
1657
- <!-- - TEST lines.size == 2 && lines[1] == "Counter = 100000" -->
1735
+ <!-- - TEST ARBITRARY_TIME
1736
+ Completed 1000000 actions in xxx ms
1737
+ Counter = 1000000
1738
+ -->
1658
1739
1659
1740
Notice, that it does not matter (for correctness) what context the actor itself is executed in. An actor is
1660
1741
a coroutine and a coroutine is executed sequentially, so confinement of the state to the specific coroutine
1661
1742
works as a solution to the problem of shared mutable state.
1662
1743
1744
+ Actor is more efficient than locking under load, because in this case it always has work to do and does not
1745
+ have to switch at all.
1746
+
1663
1747
## Select expression
1664
1748
1665
1749
Select expression makes it possible to await multiple suspending functions simultaneously and _ select_
0 commit comments