├── .gitignore ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── src ├── verifier │ ├── Original.cfsm │ ├── FixedSelect.cfsm │ ├── WithPriority.cfsm │ ├── FixedPriority.cfsm │ ├── WithBuffer.cfsm │ └── CFSMVerifier.kt ├── Data.kt ├── DownloaderWithMailbox.kt ├── DownloaderOriginal.kt ├── DownloaderWithPriority.kt ├── DownloaderFixedPriority.kt ├── DownloaderWithBuffer.kt ├── DownloaderWithUnlimitedBuffer.kt ├── Downloader.go └── DownloaderFixedSelect.kt ├── gradlew.bat ├── README.md └── gradlew /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | .idea 3 | out 4 | 5 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elizarov/DeadlocksInCSP/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | name = DeadlocksInCSP 2 | version = 1.0-SNAPSHOT 3 | group = org.jetbrains.kotlinx 4 | 5 | kotlin_version = 1.3.11 6 | coroutines_version = 1.1.0 -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Wed May 30 12:27:47 MSK 2018 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip 7 | -------------------------------------------------------------------------------- /src/verifier/Original.cfsm: -------------------------------------------------------------------------------- 1 | # State machines of original code (with deadlock) 2 | 3 | E # Environment constantly sending references 4 | 0 r! 0 5 | . 6 | 7 | D # Downloader coroutine 8 | 0 r? 1 # reference.receive 9 | 1 l! 0 # locations.send 10 | 0 c? 0 # contents.receive 11 | . 12 | 13 | W 4 # Worker coroutines pool 14 | 0 l? 1 # locations.receive 15 | 1 c! 0 # contents.send 16 | . 17 | -------------------------------------------------------------------------------- /src/verifier/FixedSelect.cfsm: -------------------------------------------------------------------------------- 1 | # State machines of fixed code with select on locations.send 2 | 3 | E # Environment constantly sending references 4 | 0 r! 0 5 | . 6 | 7 | D # Downloader coroutine 8 | 0 r? 1 # reference.receive 9 | 1 l! 0 # locations.send 10 | 0 c? 0 # contents.receive 11 | 1 c? 1 # contents.receive (too!) 12 | . 13 | 14 | W 4 # Worker coroutines pool 15 | 0 l? 1 # locations.receive 16 | 1 c! 0 # contents.send 17 | . 18 | -------------------------------------------------------------------------------- /src/verifier/WithPriority.cfsm: -------------------------------------------------------------------------------- 1 | # State machines of original code (with deadlock) 2 | 3 | E # Environment constantly sending references 4 | 0 r! 0 5 | . 6 | 7 | D # Downloader coroutine 8 | 0 c?@0 0 # contents.receive (highest priority) 9 | 0 r?@1 1 # reference.receive (after than) 10 | 1 l! 0 # locations.send 11 | . 12 | 13 | W 4 # Worker coroutines pool 14 | 0 l? 1 # locations.receive 15 | 1 c! 0 # contents.send 16 | . 17 | -------------------------------------------------------------------------------- /src/verifier/FixedPriority.cfsm: -------------------------------------------------------------------------------- 1 | # State machines of original code (with deadlock) 2 | 3 | E # Environment constantly sending references 4 | 0 r! 0 5 | . 6 | 7 | c %1 # buffer for channel c 8 | 9 | D # Downloader coroutine 10 | 0 c?@0 0 # contents.receive (highest priority) 11 | 0 r?@1 1 # reference.receive (after than) 12 | 1 l! 0 # locations.send 13 | . 14 | 15 | W 1 # Worker coroutines pool 16 | 0 l? 1 # locations.receive 17 | 1 c! 0 # contents.send 18 | . 19 | -------------------------------------------------------------------------------- /src/verifier/WithBuffer.cfsm: -------------------------------------------------------------------------------- 1 | # State machines of original code with buffer on contents channel (still deadlocks) 2 | 3 | E # Environment constantly sending references 4 | 0 r! 0 5 | . 6 | 7 | c %4 # buffer for contents channel 8 | l %4 # buffer for locations channel 9 | 10 | D # Downloader coroutine 11 | 0 r? 1 # reference.receive 12 | 1 l! 0 # locations.send 13 | 0 c? 0 # contents.receive 14 | . 15 | 16 | W 4 # Worker coroutines pool 17 | 0 l? 1 # locations.receive 18 | 1 c! 0 # contents.send 19 | . 20 | -------------------------------------------------------------------------------- /src/Data.kt: -------------------------------------------------------------------------------- 1 | import kotlinx.coroutines.* 2 | import java.text.* 3 | import java.util.* 4 | 5 | data class Reference(val index: Int) 6 | data class Location(val index: Int) 7 | data class Content(val index: Int) 8 | data class LocContent(val loc: Location, val content: Content) 9 | 10 | fun Reference.resolveLocation(): Location { 11 | log("Resolving location for $this") 12 | return Location(index) 13 | } 14 | 15 | suspend fun downloadContent(loc: Location): Content { 16 | log("Downloading $loc") 17 | delay(10) 18 | return Content(loc.index) 19 | } 20 | 21 | fun processContent(ref: Reference, content: Content) { 22 | log("Processing $ref $content") 23 | } 24 | 25 | private fun log(msg: String) { 26 | val time = SimpleDateFormat("HH:mm:ss.sss").format(Date()) 27 | println("$time [${Thread.currentThread().name}] $msg") 28 | } 29 | 30 | -------------------------------------------------------------------------------- /src/DownloaderWithMailbox.kt: -------------------------------------------------------------------------------- 1 | import kotlinx.coroutines.* 2 | import kotlinx.coroutines.channels.* 3 | 4 | private const val N_WORKERS = 4 5 | 6 | private fun CoroutineScope.downloader( 7 | mailbox: ReceiveChannel, 8 | locations: SendChannel 9 | ) = launch { 10 | val requested = mutableMapOf>() 11 | for (msg in mailbox) { 12 | when (msg) { 13 | is Reference -> { 14 | val ref = msg 15 | val loc = ref.resolveLocation() 16 | val refs = requested[loc] 17 | if (refs == null) { 18 | requested[loc] = mutableListOf(ref) 19 | locations.send(loc) 20 | } else { 21 | refs.add(ref) 22 | } 23 | } 24 | is LocContent -> { 25 | val (loc, content) = msg 26 | val refs = requested.remove(loc)!! 27 | for (ref in refs) { 28 | processContent(ref, content) 29 | } 30 | } 31 | } 32 | } 33 | } 34 | 35 | private fun CoroutineScope.worker( 36 | locations: ReceiveChannel, 37 | contents: SendChannel 38 | ) = launch { 39 | for (loc in locations) { 40 | val content = downloadContent(loc) 41 | contents.send(LocContent(loc, content)) 42 | } 43 | } 44 | 45 | private fun CoroutineScope.processReferences(): SendChannel { 46 | val locations = Channel() 47 | val mailbox = Channel() 48 | repeat(N_WORKERS) { worker(locations, mailbox) } 49 | downloader(mailbox, locations) 50 | return mailbox 51 | } 52 | 53 | fun main() = runBlocking { 54 | withTimeout(3000) { 55 | val references = processReferences() 56 | var index = 1 57 | while (true) { 58 | references.send(Reference(index++)) 59 | } 60 | } 61 | } -------------------------------------------------------------------------------- /src/DownloaderOriginal.kt: -------------------------------------------------------------------------------- 1 | import kotlinx.coroutines.* 2 | import kotlinx.coroutines.channels.* 3 | import kotlinx.coroutines.selects.* 4 | 5 | private const val N_WORKERS = 4 6 | 7 | private fun CoroutineScope.downloader( 8 | references: ReceiveChannel, 9 | locations: SendChannel, 10 | contents: ReceiveChannel 11 | ) = launch { 12 | val requested = mutableMapOf>() 13 | while (true) { 14 | select { 15 | references.onReceive { ref -> 16 | val loc = ref.resolveLocation() 17 | val refs = requested[loc] 18 | if (refs == null) { 19 | requested[loc] = mutableListOf(ref) 20 | locations.send(loc) 21 | } else { 22 | refs.add(ref) 23 | } 24 | } 25 | contents.onReceive { (loc, content) -> 26 | val refs = requested.remove(loc)!! 27 | for (ref in refs) { 28 | processContent(ref, content) 29 | } 30 | } 31 | } 32 | } 33 | } 34 | 35 | private fun CoroutineScope.worker( 36 | locations: ReceiveChannel, 37 | contents: SendChannel 38 | ) = launch { 39 | for (loc in locations) { 40 | val content = downloadContent(loc) 41 | contents.send(LocContent(loc, content)) 42 | } 43 | } 44 | 45 | private fun CoroutineScope.processReferences( 46 | references: ReceiveChannel 47 | ) { 48 | val locations = Channel() 49 | val contents = Channel() 50 | repeat(N_WORKERS) { worker(locations, contents) } 51 | downloader(references, locations, contents) 52 | } 53 | 54 | fun main() = runBlocking { 55 | withTimeout(3000) { 56 | val references = Channel() 57 | processReferences(references) 58 | var index = 1 59 | while (true) { 60 | references.send(Reference(index++)) 61 | } 62 | } 63 | } -------------------------------------------------------------------------------- /src/DownloaderWithPriority.kt: -------------------------------------------------------------------------------- 1 | import kotlinx.coroutines.* 2 | import kotlinx.coroutines.channels.* 3 | import kotlinx.coroutines.selects.* 4 | 5 | private const val N_WORKERS = 4 6 | 7 | private fun CoroutineScope.downloader( 8 | references: ReceiveChannel, 9 | locations: SendChannel, 10 | contents: ReceiveChannel 11 | ) = launch { 12 | val requested = mutableMapOf>() 13 | while (true) { 14 | select { 15 | contents.onReceive { (loc, content) -> 16 | val refs = requested.remove(loc)!! 17 | for (ref in refs) { 18 | processContent(ref, content) 19 | } 20 | } 21 | references.onReceive { ref -> 22 | val loc = ref.resolveLocation() 23 | val refs = requested[loc] 24 | if (refs == null) { 25 | requested[loc] = mutableListOf(ref) 26 | locations.send(loc) 27 | } else { 28 | refs.add(ref) 29 | } 30 | } 31 | } 32 | } 33 | } 34 | 35 | private fun CoroutineScope.worker( 36 | locations: ReceiveChannel, 37 | contents: SendChannel 38 | ) = launch { 39 | for (loc in locations) { 40 | val content = downloadContent(loc) 41 | contents.send(LocContent(loc, content)) 42 | } 43 | } 44 | 45 | private fun CoroutineScope.processReferences( 46 | references: ReceiveChannel 47 | ) { 48 | val locations = Channel() 49 | val contents = Channel() 50 | repeat(N_WORKERS) { worker(locations, contents) } 51 | downloader(references, locations, contents) 52 | } 53 | 54 | fun main() = runBlocking { 55 | withTimeout(3000) { 56 | val references = Channel() 57 | processReferences(references) 58 | var index = 1 59 | while (true) { 60 | references.send(Reference(index++)) 61 | } 62 | } 63 | } -------------------------------------------------------------------------------- /src/DownloaderFixedPriority.kt: -------------------------------------------------------------------------------- 1 | import kotlinx.coroutines.* 2 | import kotlinx.coroutines.channels.* 3 | import kotlinx.coroutines.selects.* 4 | 5 | private const val N_WORKERS = 4 6 | 7 | private fun CoroutineScope.downloader( 8 | references: ReceiveChannel, 9 | locations: SendChannel, 10 | contents: ReceiveChannel 11 | ) = launch { 12 | val requested = mutableMapOf>() 13 | while (true) { 14 | select { 15 | contents.onReceive { (loc, content) -> 16 | val refs = requested.remove(loc)!! 17 | for (ref in refs) { 18 | processContent(ref, content) 19 | } 20 | } 21 | references.onReceive { ref -> 22 | val loc = ref.resolveLocation() 23 | val refs = requested[loc] 24 | if (refs == null) { 25 | requested[loc] = mutableListOf(ref) 26 | locations.send(loc) 27 | } else { 28 | refs.add(ref) 29 | } 30 | } 31 | } 32 | } 33 | } 34 | 35 | private fun CoroutineScope.worker( 36 | locations: ReceiveChannel, 37 | contents: SendChannel 38 | ) = launch { 39 | for (loc in locations) { 40 | val content = downloadContent(loc) 41 | contents.send(LocContent(loc, content)) 42 | } 43 | } 44 | 45 | private fun CoroutineScope.processReferences( 46 | references: ReceiveChannel 47 | ) { 48 | val locations = Channel() 49 | val contents = Channel(1) 50 | repeat(N_WORKERS) { worker(locations, contents) } 51 | downloader(references, locations, contents) 52 | } 53 | 54 | fun main() = runBlocking { 55 | withTimeout(3000) { 56 | val references = Channel() 57 | processReferences(references) 58 | var index = 1 59 | while (true) { 60 | references.send(Reference(index++)) 61 | } 62 | } 63 | } -------------------------------------------------------------------------------- /src/DownloaderWithBuffer.kt: -------------------------------------------------------------------------------- 1 | import kotlinx.coroutines.* 2 | import kotlinx.coroutines.channels.* 3 | import kotlinx.coroutines.selects.* 4 | 5 | private const val N_WORKERS = 4 6 | 7 | private fun CoroutineScope.downloader( 8 | references: ReceiveChannel, 9 | locations: SendChannel, 10 | contents: ReceiveChannel 11 | ) = launch { 12 | val requested = mutableMapOf>() 13 | while (true) { 14 | select { 15 | references.onReceive { ref -> 16 | val loc = ref.resolveLocation() 17 | val refs = requested[loc] 18 | if (refs == null) { 19 | requested[loc] = mutableListOf(ref) 20 | locations.send(loc) 21 | } else { 22 | refs.add(ref) 23 | } 24 | } 25 | contents.onReceive { (loc, content) -> 26 | val refs = requested.remove(loc)!! 27 | for (ref in refs) { 28 | processContent(ref, content) 29 | } 30 | } 31 | } 32 | } 33 | } 34 | 35 | private fun CoroutineScope.worker( 36 | locations: ReceiveChannel, 37 | contents: SendChannel 38 | ) = launch { 39 | for (loc in locations) { 40 | val content = downloadContent(loc) 41 | contents.send(LocContent(loc, content)) 42 | } 43 | } 44 | 45 | private fun CoroutineScope.processReferences( 46 | references: ReceiveChannel 47 | ) { 48 | val locations = Channel(N_WORKERS) 49 | val contents = Channel(N_WORKERS) 50 | repeat(N_WORKERS) { worker(locations, contents) } 51 | downloader(references, locations, contents) 52 | } 53 | 54 | fun main() = runBlocking { 55 | withTimeout(3000) { 56 | val references = Channel() 57 | processReferences(references) 58 | var index = 1 59 | while (true) { 60 | references.send(Reference(index++)) 61 | } 62 | } 63 | } -------------------------------------------------------------------------------- /src/DownloaderWithUnlimitedBuffer.kt: -------------------------------------------------------------------------------- 1 | import kotlinx.coroutines.* 2 | import kotlinx.coroutines.channels.* 3 | import kotlinx.coroutines.selects.* 4 | 5 | private const val N_WORKERS = 4 6 | 7 | private fun CoroutineScope.downloader( 8 | references: ReceiveChannel, 9 | locations: SendChannel, 10 | contents: ReceiveChannel 11 | ) = launch { 12 | val requested = mutableMapOf>() 13 | while (true) { 14 | select { 15 | references.onReceive { ref -> 16 | val loc = ref.resolveLocation() 17 | val refs = requested[loc] 18 | if (refs == null) { 19 | requested[loc] = mutableListOf(ref) 20 | locations.send(loc) 21 | } else { 22 | refs.add(ref) 23 | } 24 | } 25 | contents.onReceive { (loc, content) -> 26 | val refs = requested.remove(loc)!! 27 | for (ref in refs) { 28 | processContent(ref, content) 29 | } 30 | } 31 | } 32 | } 33 | } 34 | 35 | private fun CoroutineScope.worker( 36 | locations: ReceiveChannel, 37 | contents: SendChannel 38 | ) = launch { 39 | for (loc in locations) { 40 | val content = downloadContent(loc) 41 | contents.send(LocContent(loc, content)) 42 | } 43 | } 44 | 45 | private fun CoroutineScope.processReferences( 46 | references: ReceiveChannel 47 | ) { 48 | val locations = Channel() 49 | val contents = Channel(Channel.UNLIMITED) 50 | repeat(N_WORKERS) { worker(locations, contents) } 51 | downloader(references, locations, contents) 52 | } 53 | 54 | fun main() = runBlocking { 55 | withTimeout(3000) { 56 | val references = Channel() 57 | processReferences(references) 58 | var index = 1 59 | while (true) { 60 | references.send(Reference(index++)) 61 | } 62 | } 63 | } -------------------------------------------------------------------------------- /src/Downloader.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | ) 7 | 8 | const nWorkers int = 4 9 | 10 | type Reference struct { index int } 11 | type Location struct { index int } 12 | type Content struct { index int } 13 | type LocContent struct { loc Location; content Content } 14 | 15 | func (ref Reference) resolveLocation() Location { 16 | log("Resolving location for %#v", ref) 17 | return Location{ref.index} 18 | } 19 | 20 | func downloadContent(loc Location) Content { 21 | log("Downloading %#v", loc) 22 | time.Sleep(10 * time.Millisecond) 23 | return Content{loc.index} 24 | } 25 | 26 | func processContent(ref Reference, content Content) { 27 | log("Processing %#v %#v", ref, content) 28 | } 29 | 30 | func log(format string, a... interface{}) { 31 | b := []interface{} { time.Now().Format(time.RFC3339) } 32 | fmt.Printf("%s " + format + "\n", append(b, a...)...) 33 | } 34 | 35 | func downloader( 36 | references <-chan Reference, 37 | locations chan<- Location, 38 | contents <-chan LocContent, 39 | ) { 40 | requested := make(map[Location][]Reference) 41 | for { 42 | select { 43 | case ref := <-references: 44 | loc := ref.resolveLocation() 45 | refs, present := requested[loc] 46 | if !present { 47 | requested[loc] = []Reference { ref } 48 | locations <- loc 49 | } else { 50 | requested[loc] = append(refs, ref) 51 | } 52 | case lc := <-contents: 53 | refs := requested[lc.loc] 54 | delete(requested, lc.loc) 55 | for _, ref := range refs { 56 | processContent(ref, lc.content) 57 | } 58 | } 59 | } 60 | } 61 | 62 | func worker( 63 | locations <-chan Location, 64 | contents chan<- LocContent, 65 | ) { 66 | for loc := range locations { 67 | content := downloadContent(loc) 68 | contents <- LocContent{ loc, content} 69 | } 70 | } 71 | 72 | func processReferences(references <-chan Reference) { 73 | locations := make(chan Location) 74 | contents := make(chan LocContent) 75 | for i := 0; i < nWorkers; i++ { go worker(locations, contents) } 76 | go downloader(references, locations, contents) 77 | } 78 | 79 | func main() { 80 | references := make(chan Reference) 81 | processReferences(references) 82 | for index := 1 ;; index++ { 83 | references <- Reference{index} 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/DownloaderFixedSelect.kt: -------------------------------------------------------------------------------- 1 | import kotlinx.coroutines.* 2 | import kotlinx.coroutines.channels.* 3 | import kotlinx.coroutines.selects.* 4 | 5 | private const val N_WORKERS = 4 6 | 7 | private fun CoroutineScope.downloader( 8 | references: ReceiveChannel, 9 | locations: SendChannel, 10 | contents: ReceiveChannel 11 | ) = launch { 12 | val requested = mutableMapOf>() 13 | var locationToSend: Location? = null 14 | while (true) { 15 | select { 16 | // When locationToSend is set then try sending it instead of receiving new reference 17 | locationToSend?.let { 18 | locations.onSend(it) { 19 | locationToSend = null // clear when sent 20 | } 21 | } ?: references.onReceive { ref -> 22 | val loc = ref.resolveLocation() 23 | val refs = requested[loc] 24 | if (refs == null) { 25 | requested[loc] = mutableListOf(ref) 26 | locationToSend = loc 27 | } else { 28 | refs.add(ref) 29 | } 30 | } 31 | contents.onReceive { (loc, content) -> 32 | val refs = requested.remove(loc)!! 33 | for (ref in refs) { 34 | processContent(ref, content) 35 | } 36 | } 37 | } 38 | } 39 | } 40 | 41 | private fun CoroutineScope.worker( 42 | locations: ReceiveChannel, 43 | contents: SendChannel 44 | ) = launch { 45 | for (loc in locations) { 46 | val content = downloadContent(loc) 47 | contents.send(LocContent(loc, content)) 48 | } 49 | } 50 | 51 | private fun CoroutineScope.processReferences( 52 | references: ReceiveChannel 53 | ) { 54 | val locations = Channel() 55 | val contents = Channel() 56 | repeat(N_WORKERS) { worker(locations, contents) } 57 | downloader(references, locations, contents) 58 | } 59 | 60 | fun main() = runBlocking { 61 | withTimeout(3000) { 62 | val references = Channel() 63 | processReferences(references) 64 | var index = 1 65 | while (true) { 66 | references.send(Reference(index++)) 67 | } 68 | } 69 | } -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Deadlocks in non-hierarchical CSP 2 | 3 | This is a companion project with code for 4 | [the corresponding blog post](https://medium.com/@elizarov/deadlocks-in-non-hierarchical-csp-e5910d137cc). 5 | 6 | The original code based on Kotlin Conf 2018 talk "Kotlin Coroutines in Practice" 7 | ([video](https://www.youtube.com/watch?v=a3agLJQ6vt8), [slides](https://speakerdeck.com/elizarov/kotlin-coroutines-in-practice-at-kotlinconf-2018)). 8 | 9 | ## Code 10 | 11 | See the following sources: 12 | 13 | | Source | Description | Playground | 14 | | ------ | ----------- | ---------- | 15 | | [DownloaderOriginal.kt](src/DownloaderOriginal.kt) | The original version from presentation that suffers from deadlock. | [playground](https://tinyurl.com/yasm6els) | 16 | | [Downloader.go](src/Downloader.go) | Go version of the same code. | [playground](https://play.golang.org/p/uaaMhdmsVmS) | 17 | | [DownloaderWithMailbox.kt](src/DownloaderWithMailbox.kt) | Does not use `select`, but actor patter instead. Still deadlocks. | [playground](https://tinyurl.com/y9ru24yo) | 18 | | [DownloaderWithBuffer.kt](src/DownloaderWithBuffer.kt) | Same as original but with buffers for both `locations` and `contents` channels. Still deadlocks. | [playground](https://tinyurl.com/yd8g6gsa) | 19 | | [DownloaderWithUnlimitedBuffer.kt](src/DownloaderWithUnlimitedBuffer.kt)| Uses unlimited buffer for `contents` channel and seems to work. | [playground](https://tinyurl.com/ycsrdr6g) | 20 | | [DownloaderWithPriority.kt](src/DownloaderWithPriority.kt) | Gives `contents` channel priority over `references` in downloader coroutine. Still deadlocks. | [playground](https://tinyurl.com/y8kk4gk9) | 21 | | [DownloaderFixedPriority.kt](src/DownloaderFixedPriority.kt) | Fixes the problem by adjusting priority of `select` in downloader (`contents` first) and using a buffer for `contents` channels. | [playground](https://tinyurl.com/y8bbo5v7) | 22 | | [DownloaderFixedSelect.kt](src/DownloaderFixedSelect.kt) | Fixes the problem by performing `locations.send` inside `select` in downloader. | [playground](https://tinyurl.com/ycagcomy) | 23 | 24 | ## Verifier 25 | 26 | There is a formal model-checker that takes a simple description of the corresponding 27 | communicating final-state machines and exhaustively checks all system states for the presence of deadlocks. 28 | It's code is in [CFSMVerifier.kt](src/verifier/CFSMVerifier.kt) file and there are configuration files for 29 | all examples in its [directory](src/verifier/). The verifier supports plain CFSM using rendezvous channels, 30 | has built in capability to represent buffered channels with an addition process, and supports priorities 31 | for Kotlin-style `select` expression using addition "transitional" states. 32 | 33 | To use it, run `CFSMVerifier` with path(s) to the corresponding `.cfsm` configuration files. 34 | 35 | 36 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /src/verifier/CFSMVerifier.kt: -------------------------------------------------------------------------------- 1 | package verifier 2 | 3 | import java.io.* 4 | import java.util.* 5 | import java.util.regex.* 6 | import kotlin.math.* 7 | import kotlin.system.* 8 | 9 | fun main(args: Array) { 10 | if (args.isEmpty()) { 11 | println("Usage: CSPVerifier ..") 12 | return 13 | } 14 | for (arg in args) { 15 | val file = File(arg) 16 | if (file.name.contains('*')) { 17 | val regex = Regex(file.name.split("*").map { Pattern.quote(it) }.joinToString(".*")) 18 | file.parentFile 19 | .listFiles { _, name -> regex.matches(name) } 20 | .sortedBy { it.name } 21 | .forEach { analyzeFile(it) } 22 | } else { 23 | analyzeFile(file) 24 | } 25 | } 26 | } 27 | 28 | fun analyzeFile(file: File) { 29 | println("====== Reading $file") 30 | val cfsm = LineNumberReader(file.reader()).use { 31 | try { 32 | it.readCFSM() 33 | } catch (e: Exception) { 34 | System.err.println("ERROR: ${file.absolutePath}: ${it.lineNumber}: ${e.message}") 35 | throw e 36 | } 37 | } 38 | println("-- Processes: $cfsm") 39 | println("-- Channels: ${cfsm.channels.joinToString(" ")}") 40 | val time = measureTimeMillis { cfsm.analyze() } 41 | println("Done in $time ms") 42 | } 43 | 44 | private fun CFSM.analyze() { 45 | val v = mutableMapOf() 46 | val initialState = GlobalState(procs.map { it.t.keys.first() }.toTypedArray()) 47 | val queue = ArrayDeque() 48 | v[initialState] = null 49 | queue.addLast(initialState) 50 | 51 | fun enqueue(next: GlobalState, transition: Transition) { 52 | if (next !in v) { 53 | v[next] = transition 54 | queue.addLast(next) 55 | } 56 | } 57 | 58 | val csMap = mutableMapOf() 59 | val pris = IntArray(procs.size) 60 | val hasPriorities = hasPriorities 61 | while (true) { 62 | // Get position to analyze from the queue 63 | val cur = queue.pollFirst() ?: break 64 | // Clear temp data 65 | for ((_, cs) in csMap) cs.clear() 66 | if (hasPriorities) pris.fill(Int.MAX_VALUE) 67 | // Analyze all desired operations on channels 68 | for (p in procs.withIndex()) { 69 | val fromState = cur.s[p.index] 70 | val actMap = procs[p.index].t[fromState] ?: continue 71 | for ((act, toState) in actMap) { 72 | val cs = csMap.getOrPut(act.chan) { ChanState() } 73 | when (act.op) { 74 | Operation.SND -> cs.snd += ProcState(p.index, fromState, toState, act.pri) 75 | Operation.RCV -> cs.rcv += ProcState(p.index, fromState, toState, act.pri) 76 | } 77 | } 78 | } 79 | // Find best priorities for all possible communications in each state 80 | if (hasPriorities) { 81 | for (p in procs.withIndex()) { 82 | val fromState = cur.s[p.index] 83 | val actMap = procs[p.index].t[fromState] ?: continue 84 | for ((act, _) in actMap) { 85 | val cs = csMap.getOrPut(act.chan) { ChanState() } 86 | val possible = when (act.op) { 87 | Operation.SND -> cs.rcv.any { it.index != p.index } 88 | Operation.RCV -> cs.snd.any { it.index != p.index } 89 | } 90 | if (possible) pris[p.index] = min(pris[p.index], act.pri) 91 | } 92 | } 93 | } 94 | // Find possible communications state transitions 95 | var active = false 96 | for ((chan, cs) in csMap) { 97 | for (snd in cs.snd) for (rcv in cs.rcv) { 98 | if (snd.index != rcv.index && snd.pri == pris[snd.index] && rcv.pri == pris[rcv.index]) { 99 | // Note: Buffer processes perform truly atomic transitions 100 | val sndToState = snd.toState.beginMoveIf(hasPriorities && !procs[snd.index].bufferProc) 101 | val rcvToState = rcv.toState.beginMoveIf(hasPriorities && !procs[rcv.index].bufferProc) 102 | val sndMove = Move(snd.index, snd.fromState, sndToState) 103 | val rcvMove = Move(rcv.index, rcv.fromState, rcvToState) 104 | enqueue(cur.next(sndMove, rcvMove), Comm(chan, sndMove, rcvMove)) 105 | active = true // active state, because it can move 106 | } 107 | } 108 | } 109 | // Find possible finishing state transition (if working with priorities) 110 | if (hasPriorities) { 111 | for (p in procs.withIndex()) { 112 | val fromState = cur.s[p.index] 113 | val toState = fromState.finishMove() ?: continue 114 | val move = Move(p.index, fromState, toState) 115 | enqueue(cur.next(move), move) 116 | active = true 117 | } 118 | } 119 | if (!active) { 120 | dumpDeadlock(cur, v) // found deadlock 121 | break 122 | } 123 | } 124 | println("Analyzed ${v.size} states") 125 | } 126 | 127 | private const val MOVING = '\'' 128 | private fun String.beginMoveIf(hasPriorities: Boolean) = if (hasPriorities) "$this$MOVING" else this 129 | private fun String.finishMove() = if (endsWith(MOVING)) dropLast(1) else null 130 | 131 | private fun CFSM.dumpDeadlock( 132 | cur: GlobalState, 133 | v: MutableMap 134 | ) { 135 | println("Found deadlock at ${cur.string()}") 136 | val ts = mutableListOf() 137 | val prev = cur.next() 138 | while (true) { 139 | val t = v[prev] ?: break 140 | ts += t 141 | when (t) { 142 | is Comm -> { 143 | prev.s[t.snd.index] = t.snd.fromState 144 | prev.s[t.rcv.index] = t.rcv.fromState 145 | } 146 | is Move -> { 147 | prev.s[t.index] = t.fromState 148 | } 149 | } 150 | } 151 | ts.reverse() 152 | with(Layout()) { 153 | for (t in ts) println(t.string()) 154 | } 155 | } 156 | 157 | class GlobalState(val s: Array) { 158 | override fun equals(other: Any?): Boolean = (other as? GlobalState)?.s?.contentEquals(s) ?: false 159 | override fun hashCode(): Int = s.contentHashCode() 160 | fun next(vararg moves: Move) = GlobalState(s.clone()).apply { 161 | moves.forEach { s[it.index] = it.toState } 162 | } 163 | } 164 | 165 | sealed class Transition 166 | data class Comm(val chan: String, val snd: Move, val rcv: Move) : Transition() 167 | data class Move(val index: Int, val fromState: String, val toState: String) : Transition() 168 | 169 | class ChanState { 170 | val snd = mutableListOf() 171 | val rcv = mutableListOf() 172 | 173 | fun clear() { 174 | snd.clear() 175 | rcv.clear() 176 | } 177 | } 178 | 179 | data class ProcState(val index: Int, val fromState: String, val toState: String, val pri: Int) 180 | 181 | fun LineNumberReader.readCFSM() = CFSM().apply { 182 | val bufChans = mutableSetOf() // buffered channels 183 | val seenChans = mutableSetOf() // already seen channels 184 | while (true) { 185 | val procLine = nextLine() ?: break 186 | val procName = procLine.substringBefore(" ") 187 | val procCountStr = procLine.substringAfter(" ", "1") 188 | if (procCountStr.startsWith("%")) { 189 | // create channel buffer coroutine 190 | val n = procCountStr.drop(1).toInt() 191 | check(procName !in seenChans) { "Channel buffer specification must precede channel usage '$procName' "} 192 | seenChans += procName 193 | bufChans += procName 194 | addProc(procName, createBufferProc(procName, n)) 195 | } else { 196 | val procCount = procCountStr.toInt() 197 | check(procCount >= 1) { "Invalid process count $procCount" } 198 | val proc = readProc(bufChans) 199 | if (procCount == 1) { 200 | addProc(procName, proc) 201 | } else { 202 | repeat(procCount) { addProc(procName + it, proc) } 203 | } 204 | seenChans += proc.t.values.flatMap { it.keys.map { it.chan } } 205 | } 206 | } 207 | } 208 | 209 | private fun createBufferProc(name: String, n: Int) = 210 | Proc(bufferProc = true).apply { 211 | for (i in 0..n) { 212 | if (i > 0) addAction(i.toString(), Action("$name-", Operation.SND), (i - 1).toString()) 213 | if (i < n) addAction(i.toString(), Action("$name+", Operation.RCV), (i + 1).toString()) 214 | } 215 | } 216 | 217 | 218 | private fun CFSM.addProc(procName: String, proc: Proc) { 219 | check(procName !in names) { "Duplicate process name '$procName'" } 220 | names += procName 221 | procs += proc 222 | } 223 | 224 | val SPACES = Regex("\\s+") 225 | 226 | fun LineNumberReader.readProc(bufChans: Set): Proc = Proc().apply { 227 | while (true) { 228 | val line = nextLine().takeIf { it != "." } ?: break 229 | val s = line.split(SPACES, limit = 3) 230 | check(s.size == 3) { "Transition line shall have format: " } 231 | val (fromState, actStr, toState) = s 232 | val act = actStr.toAction(bufChans) 233 | addAction(fromState, act, toState) 234 | } 235 | } 236 | 237 | private fun Proc.addAction(fromState: String, act: Action, toState: String) { 238 | val actMap = t.getOrPut(fromState) { mutableMapOf() } 239 | check(act !in actMap) { "Duplicate action '$act' at state '$fromState'" } 240 | actMap[act] = toState 241 | } 242 | 243 | fun LineNumberReader.nextLine(): String? { 244 | while (true) { 245 | val line = readLine() ?: return null 246 | return line.substringBefore("#").trim().takeIf { it.isNotEmpty() } ?: continue 247 | } 248 | } 249 | 250 | class CFSM { 251 | val names = mutableListOf() 252 | val procs = mutableListOf() 253 | 254 | val hasPriorities get() = procs.any { it.t.values.any { it.keys.any { it.pri != 0 } } } 255 | val channels: Set get() = procs.flatMap { it.t.values.flatMap { it.keys.map { it.chan } } }.toSet() 256 | 257 | override fun toString(): String = names.withIndex().joinToString(" ") { 258 | "${it.value}[${procs[it.index].t.size}]" 259 | } 260 | 261 | fun GlobalState.string() = names.withIndex().joinToString(" ") { 262 | "${it.value}.${s[it.index]}" 263 | } 264 | 265 | inner class Layout { 266 | private val nl = names.maxBy { it.length }!!.length 267 | private val sl = procs.flatMap { it.t.keys }.maxBy { it.length }!!.length + if (hasPriorities) 1 else 0 268 | private val cl = channels.maxBy { it.length }!!.length 269 | 270 | val String.p get() = padStart(nl) 271 | val String.s get() = padEnd(sl) 272 | val String.c get() = padStart(cl) 273 | 274 | fun Transition.string(): String { 275 | return when (this) { 276 | is Comm -> "${names[snd.index].p}.(${snd.fromState.s} ${chan.c}! ${snd.toState.s})" + 277 | " ${names[rcv.index].p}.(${rcv.fromState.s} ${chan.c}? ${rcv.toState.s})" 278 | is Move -> "${names[index].p}.(${fromState.s} ${"-".c}> ${toState.s})" 279 | } 280 | } 281 | } 282 | } 283 | 284 | class Proc(val bufferProc: Boolean = false) { 285 | val t = mutableMapOf>() 286 | } 287 | 288 | data class Action(val chan: String, val op: Operation, val pri: Int = 0) { 289 | override fun toString(): String = chan + op.ch 290 | } 291 | 292 | enum class Operation(val ch: Char) { 293 | SND('!'), 294 | RCV('?') 295 | } 296 | 297 | fun String.toAction(bufChans: Set): Action { 298 | require(length > 1) { "Invalid action '$this'" } 299 | val chanOp = substringBefore('@') 300 | val pri = substringAfter('@', "0").toInt() 301 | val chan = chanOp.dropLast(1) 302 | val op = chanOp.last().toOperation() 303 | return when { 304 | chan !in bufChans -> Action(chan, op, pri) 305 | op == Operation.SND -> Action("$chan+", op, pri) 306 | else -> Action("$chan-", op, pri) 307 | } 308 | } 309 | 310 | fun Char.toOperation() = when (this) { 311 | '!' -> Operation.SND 312 | '?' -> Operation.RCV 313 | else -> error("Unrecognized operation char '$this'") 314 | } 315 | 316 | --------------------------------------------------------------------------------