常见运行时异常

在仓颉语言中内置了最常见的异常类,开发人员可以直接使用。

在这里插入图片描述

使用 Option

在 Option 类型中介绍了 Option 类型的定义,因为 Option 类型可以同时表示有值和无值两种状态,而无值在某些情况下也可以理解为一种错误,所以 Option 类型也可以用作错误处理。

例如,在下例中,如果函数 getOrThrow 的参数值等于 Some(v) 则将 v 的值返回,如果参数值等于 None 则抛出异常。

func getOrThrow(a: ?Int64) {
    match (a) {
        case Some(v) => v
        case None => throw NoneValueException()
    }
}

因为 Option 是一种非常常用的类型,所以仓颉为其提供了多种解构方式,以方便 Option 类型的使用,具体包括:模式匹配、getOrThrow 函数、coalescing 操作符(??),以及问号操作符(?)。下面将对这些方式逐一介绍。

模式匹配:因为 Option 类型是一种 enum 类型,所以可以使用上文提到的 enum 的模式匹配来实现对 Option 值的解构。例如,下例中函数 getString 接受一个 ?Int64 类型的参数,当参数是 Some 值时,返回其中数值的字符串表示,当参数是 None 值时,返回字符串 “none”。

func getString(p: ?Int64): String{
    match (p) {
        case Some(x) => "${x}"
        case None => "none"
    }
}
main() {
    let a = Some(1)
    let b: ?Int64 = None
    let r1 = getString(a)
    let r2 = getString(b)
    println(r1)
    println(r2)
}

上述代码的执行结果为:

1
none
coalescing 操作符(??):对于 ?T 类型的表达式 e1,如果希望 e1 的值等于 None 时同样返回一个 T 类型的值 e2,可以使用 ?? 操作符。对于表达式 e1 ?? e2,当 e1 的值等于 Some(v) 时返回 v 的值,否则返回 e2 的值。举例如下:

main() {
    let a = Some(1)
    let b: ?Int64 = None
    let r1: Int64 = a ?? 0
    let r2: Int64 = b ?? 0
    println(r1)
    println(r2)
}

上述代码的执行结果为:

1
0
问号操作符(?):? 需要和 . 或 () 或 [] 或 {}(特指尾随 lambda 调用的场景)一起使用,用以实现 Option 类型对 .,(),[] 和 {} 的支持。以 . 为例((),[] 和 {}同理),对于 ?T1 类型的表达式 e,当 e 的值等于 Some(v) 时,e?.b 的值等于 Option.Some(v.b),否则 e?.b 的值等于 Option.None,其中 T2 是 v.b 的类型。举例如下:

struct R {
    public var a: Int64
    public init(a: Int64) {
        this.a = a
    }
}

let r = R(100)
let x = Some(r)
let y = Option<R>.None
let r1 = x?.a   // r1 = Option<Int64>.Some(100)
let r2 = y?.a   // r2 = Option<Int64>.None

class C {
    var item: Int64 = 100
}
let c = C()
let c1 = Option<C>.Some(c)
let c2 = Option<C>.None
func test1() {
    c1?.item = 200             // c.item = 200
    c2?.item = 300             // no effect
}

问号操作符(?)支持多层访问,以 a?.b.c?.d 为例((),[] 和 {}同理)。表达式 a 的类型需要是某个 Option 且 T1 包含实例成员 b,b 的类型中包含实例成员变量 c 且 c 的类型是某个 Option,T2 包含实例成员 d;表达式 a?.b.c?.d 的类型为 Option,其中 T3 是 T2 的实例成员 d 的类型;当 a 的值等于 Some(va) 且 va.b.c 的值等于 Some(vc) 时,a?.b.c?.d 的值等于 Option.Some(vc.d);当 a 的值等于 Some(va) 且 va.b.c 的值等于 None 时,a?.b.c?.d 的值等于 Option.None(d 不会被求值);当 a 的值等于 None 时,a?.b.c?.d 的值等于 Option.None(b,c 和 d 都不会被求值)。

class A {
    public var b: B = B()
}

class B {
    public var c: Option<C> = C()
    public var c1: Option<C> = Option<C>.None
}

class C {
    public var d: Int64 = 100
}

main(){
    var a = Some(A())
    let a1 = a?.b.c?.d  // a1 = Option<Int64>.Some(100)
    let a2 = a?.b.c1?.d // a2 = Option<Int64>.None
    a?.b.c?.d = 200     // a.b.c.d = 200
    a?.b.c1?.d = 200    // no effect
}

getOrThrow 函数:对于 ?T 类型的表达式 e,可以通过调用 getOrThrow 函数实现解构。当 e 的值等于 Some(v) 时,getOrThrow() 返回 v 的值,否则抛出异常。举例如下:

main() {
    let a = Some(1)
    let b: ?Int64 = None
    let r1 = a.getOrThrow()
    println(r1)
    try {
        let r2 = b.getOrThrow()
    } catch (e: NoneValueException) {
        println("b is None")
    }
}

上述代码的执行结果为:

1
b is None

并发概述

并发编程是现代编程语言中不可或缺的特性,仓颉编程语言提供抢占式的线程模型作为并发编程机制。线程可以细分为两种不同概念,语言线程和 native 线程。

语言线程是编程语言中并发模型的基本执行单位。仓颉编程语言希望给开发者提供一个友好、高效、统一的并发编程界面,让开发者无需关心操作系统线程、用户态线程等差异,因此提供仓颉线程的概念。开发者在大多数情况下只需面向仓颉线程编写并发代码。
native 线程指语言实现中所使用到的线程(一般是操作系统线程),它们作为语言线程的具体实现载体。不同编程语言会以不同的方式实现语言线程。例如,一些编程语言直接通过操作系统调用来创建线程,这意味着每个语言线程对应一个 native 线程,这种实现方案一般被称之为 1:1 线程模型。此外,另有一些编程语言提供特殊的线程实现,它们允许多个语言线程在多个 native 线程上切换执行,这种也被称为 M:N 线程模型,即 M 个语言线程在 N 个 native 线程上调度执行,其中 M 和 N 不一定相等。当前,仓颉语言的实现同样采用 M:N 线程模型;因此,仓颉线程本质上是一种用户态的轻量级线程,支持抢占且相比操作系统线程更轻量化。

仓颉线程本质上是用户态的轻量级线程,每个仓颉线程都受到底层 native 线程的调度执行,并且多个仓颉线程可以由一个 native 线程执行。每个 native 线程会不断地选择一个就绪的仓颉线程完成执行,如果仓颉线程在执行过程中发生阻塞(例如等待互斥锁的释放),那么 native 线程会将当前的仓颉线程挂起,并继续选择下一个就绪的仓颉线程。发生阻塞的仓颉线程在重新就绪后会继续被 native 线程调度执行。

在大多数情况下,开发者只需要面向仓颉线程进行并发编程而不需要考虑这些细节。但在进行跨语言编程时,开发者需要谨慎调用可能发生阻塞的 foreign 函数,例如 IO 相关的操作系统调用等。例如,下列示例代码中的新线程会调用 foreign 函数 socket_read。在程序运行过程中,某一 native 线程将调度并执行该仓颉线程,在进入到 foreign 函数中后,系统调用会直接阻塞当前 native 线程直到函数执行完成。native 线程在阻塞期间将无法调度其他仓颉线程来执行,这会降低程序执行的吞吐量。

foreign socket_read(sock: Int64): CPointer<Int8>

let fut = spawn {
    let sock: Int64 = ...
    let ptr = socket_read(sock)
}

注意:

本文档在没有歧义的情况下将直接以线程简化对仓颉线程的指代。

创建线程

当开发者希望并发执行某一段代码时,只需创建一个仓颉线程即可。要创建一个新的仓颉线程,可以使用关键字 spawn 并传递一个无形参的 lambda 表达式,该 lambda 表达式即为在新线程中执行的代码。

下方示例代码中,主线程和新线程均会尝试打印一些文本:

main(): Int64 {
    spawn { =>
        println("New thread before sleeping")
        sleep(100 * Duration.millisecond) // sleep for 100ms.
        println("New thread after sleeping")
    }

    println("Main thread")

    return 0
}

在上面的例子中,新线程会在主线程结束时一起停止,无论这个新线程是否已完成运行。上方示例的输出每次可能略有不同,有可能会输出类似如下的内容:

New thread before sleeping
Main thread

sleep() 函数会让当前线程睡眠指定的时长,之后再恢复执行,其时间由指定的 Duration 类型决定,详细介绍请参见线程睡眠指定时长章节。

访问线程

使用 Future 等待线程结束并获取返回值

在上面的例子中,新创建的线程会由于主线程结束而提前结束,在缺乏顺序保证的情况下,甚至可能会出现新创建的线程还来不及得到执行就退出了。可以通过 spawn 表达式的返回值,来等待线程执行结束。

spawn 表达式的返回类型是 Future,其中 T 是类型变元,其类型与 lambda 表达式的返回类型一致。当调用 Future 的 get() 成员函数时,它将等待它的线程执行完成。

Future 的原型声明如下:

public class Future<T> {
    // Blocking the current thread, waiting for the result of the thread corresponding to the current Future object.
    // If an exception occurs in the corresponding thread, the method will throw the exception.
    public func get(): T

    // Blocking the current thread, waiting for the result of the thread corresponding to the current Future object.
    // If the corresponding thread has not completed execution within Duration, the method will throws TimeoutException.
    // If `timeout` <= Duration.Zero, its behavior is the same as `get()`.
    public func get(timeout: Duration): T

    // Non-blocking method that immediately returns Option<T>.None if thread has not finished execution.
    // Returns the computed result otherwise.
    // If an exception occurs in the corresponding thread, the method will throw the exception.
    public func tryGet(): Option<T>
}

下方示例代码演示了如何使用 Future 在 main 中等待新创建的线程执行完成:

import std.sync.*
import std.time.*

main(): Int64 {
    let fut: Future<Unit> = spawn { =>
        println("New thread before sleeping")
        sleep(100 * Duration.millisecond) // sleep for 100ms.
        println("New thread after sleeping")
    }

    println("Main thread")

    fut.get() // wait for the thread to finish.
    return 0
}

调用 Future 实例的 get() 会阻塞当前运行的线程,直到 Future 实例所代表的线程运行结束。因此,上方示例有可能会输出类似如下内容:

New thread before sleeping
Main thread
New thread after sleeping

主线程在完成打印后会因为调用 get() 而等待新创建的线程执行结束。但主线程和新线程的打印顺序具有不确定性。

如果将 fut.get() 移动到主线程的打印之前,如下所示:

import std.sync.*
import std.time.*

main(): Int64 {
    let fut: Future<Unit> = spawn { =>
        println("New thread before sleeping")
        sleep(100 * Duration.millisecond) // sleep for 100ms.
        println("New thread after sleeping")
    }

    fut.get() // wait for the thread to finish.

    println("Main thread")
    return 0
}

主线程将等待新创建的线程执行完成,然后再执行打印,因此程序的输出将变得确定,如下所示:

New thread before sleeping
New thread after sleeping
Main thread

可见,get() 的调用位置会影响线程是否能同时运行。

Future 除了可以用于阻塞等待线程执行结束以外,还可以获取线程执行的结果。如下是它提供的具体成员函数:

get(): T:阻塞等待线程执行结束,并返回执行结果,如果该线程已经结束,则直接返回执行结果。

示例代码如下:

import std.sync.*
import std.time.*

main(): Int64 {
    let fut: Future<Int64> = spawn {
        sleep(Duration.second) // sleep for 1s.
        return 1
    }

    try {
        // wait for the thread to finish, and get the result.
        let res: Int64 = fut.get()
        println("result = ${res}")
    } catch (_) {
        println("oops")
    }
    return 0
}

输出结果如下:

result = 1

get(timeout: Duration): T:阻塞等待该 Future 所代表的线程执行结束,并返回执行结果,当到达超时时间 timeout 时,如果该线程还没有执行结束,将会抛出异常 TimeoutException。如果 timeout <= Duration.Zero, 其行为与 get() 相同。

示例代码如下:

main(): Int64 {
    let fut = spawn {
        sleep(Duration.second) // sleep for 1s.
        return 1
    }

    // wait for the thread to finish, but only for 1ms.
    try {
        let res = fut.get(Duration.millisecond * 1)
        println("result: ${res}")
    } catch (_: TimeoutException) {
        println("oops")
    }
    return 0
}

输出结果如下:

oops

访问线程属性

每个 Future 对象都有一个对应的仓颉线程,以 Thread 对象为表示。Thread 类主要被用于访问线程的属性信息,例如线程标识等。需要注意的是,Thread 无法直接被实例化构造对象,仅能从 Future 的 thread 成员属性获取对应的 Thread 对象,或是通过 Thread 的静态成员属性 currentThread 得到当前正在执行线程对应的 Thread 对象。

Thread 类的部分方法定义如下(完整的方法描述可参考《仓颉编程语言库 API》)。

class Thread {
    // Get the currently running thread
    static prop currentThread: Thread

    // Get the unique identifier (represented as an integer) of the thread object
    prop id: Int64

    // Check whether the thread has any cancellation request
    prop hasPendingCancellation: Bool
}

下列示例代码在创建新线程后分别通过两种方式获取线程标识。由于主线程和新线程获取的是同一个 Thread 对象,所以他们能够打印出相同的线程标识。

main(): Unit {
    let fut = spawn {
        println("Current thread id: ${Thread.currentThread.id}")
    }
    println("New thread id: ${fut.thread.id}")
    fut.get()
}

输出结果如下(其中线程 id 会变化,也可能为其他值):

New thread id: 1
Current thread id: 1
Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐