关于鸿蒙arkts多线程无法共享非Sendable对象的解决方案

场景:

很多时候我们会用三方库或者系统自带的库来实现某些功能,比如websocket库,我们可能会根据自己的业务在上面增加一些润色,比如连接方法,发送方法,自动重连。再比如操作sqlite,对系统的库增加一些方法等。这些实际就是对某库对象的一个封装。比如下面的代码:

import { webSocket } from "@kit.NetworkKit";
import { hilog } from "@kit.PerformanceAnalysisKit";

enum Status{
  DISCONNECT,
  CONNECTED,
  CONNECTTING
}

export class WebsocketClient {

  url: string | undefined;
  ws: webSocket.WebSocket;
  status: Status = Status.DISCONNECT;
  DOAMIN: number = 0xFF;
  TAG = "WebsocketClient";

  constructor(url: String) {
    this.url = url as string;
    this.ws = webSocket.createWebSocket();
    this.registerStatus();
  }

  connect() {
    hilog.info(this.DOAMIN, this.TAG, `connect 当前状态:${this.status}`);
    if(this.status === Status.DISCONNECT) {
      this.status = Status.CONNECTTING;
      this.ws.connect(this.url, (err: BusinessError<void>, data: boolean) => {
        if(data) {
          hilog.info(this.DOAMIN, this.TAG, `${this.url} is connected`);
        } else {
          hilog.error(this.DOAMIN, this.TAG, `${this.url} is error: ${JSON.stringify(err)}}`)
          this.status = Status.DISCONNECT;
        }
      });
    } else {
      hilog.info(this.DOAMIN, this.TAG, `正在连接中或已连接`);
    }
  }

  registerStatus() {
    hilog.info(this.DOAMIN, this.TAG, "registerStatus");
    this.ws.on("open", (err: BusinessError<void>, value: Object) => {
      hilog.info(this.DOAMIN, this.TAG, `${this.url} onOpen`);
      this.status = Status.CONNECTED;
    });
    this.ws.on("message", (err: BusinessError<void>, value: string | ArrayBuffer) => {
      hilog.info(this.DOAMIN, this.TAG, `${this.url} onMessage: ${value}`);
    });
    this.ws.on("error", (value: BusinessError<void>) => {
      hilog.error(this.DOAMIN, this.TAG, `${this.url} onError: ${JSON.stringify(value)}`);
      this.status = Status.DISCONNECT;
    });
    this.ws.on("close", (err: BusinessError<void>, value: webSocket.CloseResult) => {
      hilog.info(this.DOAMIN, this.TAG, `${this.url} onClose: ${JSON.stringify(value)}`);
      this.status = Status.DISCONNECT;
    });
  }

  async send(msg: string) {
    hilog.info(this.DOAMIN, this.TAG, `send 当前状态:${this.status} 消息:${msg}`);
    if(this.status === Status.CONNECTED) {
      let result = await this.ws.send(msg);
      hilog.info(this.DOAMIN, this.TAG, `发送消息: ${result}`);
    } else {
      hilog.error(this.DOAMIN, this.TAG, `send 当前未处于已连接状态不能发送`);
    }
  }

  close() {
    hilog.info(this.DOAMIN, this.TAG, `close 当前状态:${this.status}}`);
    if(this.status !== Status.DISCONNECT) {
      this.ws.close();
      this.status = Status.DISCONNECT;
    } else {
      hilog.error(this.DOAMIN, this.TAG, `close 当前已经关闭`);
    }
  }

}

由于这些对象,不限于websocket,肯定不能放在UI主线程去工作的,而且还要支持在任意位置下去调用,比如在后台服务里有个什么同步调用接口的定时器,界面登录按钮等等。这对于java,kotlin,go,C++语言的开发再正常不过了,这样场景都是需要开多线程使用的。下面是潜意识里的arkts用法:

@Concurrent
function connect(client: WebsocketClient) {
  client.connect();
}

@Concurrent
function send(client: WebsocketClient) {
  client.send("发数据");
}

let client = new WebsocketClient("ws://localhost:8080/ws");

let task = new taskpool.Task(connect, client);
taskpool.execute(task);

let task2 = new taskpool.Task(send, client);
taskpool.execute(task2);

上面的调用实测是不行的,因为WebsocketClient在不同的线程中不是一个对象了,这样就乱了。那你会说,把WebsocketClient类做成单例的可以不,即static方法。不行!我已经实际测试过了。那么我能用prototype来挂载然后再调用吗?比如:

let client = new WebsocketClient("ws://localhost:8080/ws");
(client as ESObject).__proto__ = WebsocketClient.prototype;

还是不行,不能共享对象!

看到官网的教程说,想要共享对象也可以,共享的数据必须用@Sendable标注的类或者函数,而且连互斥锁都可以用。但是针对我上面说的这样场景根本不行,为什么?因为系统的webSocket.WebSocket不是Sendable的,所以不能用!那咋办?不能保证同一个对象就意味着不能在任何位置用了,那就废了。最近一直在摸索着这个问题,找到了一个差不多的解决方案,但是可能会牺牲多线程的特性,但好处就是没有数据竞争了。思路就是用进程线程间通讯的emitter。我直接上代码把:

import { JSON, taskpool } from '@kit.ArkTS';
import { WebsocketClient } from './WebsocketClient';
import { emitter } from '@kit.BasicServicesKit';


@Concurrent
function initWebSocket(url: string) {
  let client = new WebsocketClient(url);
  let connect: emitter.InnerEvent = {
    eventId: 1
  };
  let send: emitter.InnerEvent = {
    eventId: 2
  };
  let close: emitter.InnerEvent = {
    eventId: 3
  };
  emitter.on(connect, (data: emitter.EventData) => {
    client.connect();
  })
  emitter.on(send, (data: emitter.EventData) => {
    client.send(JSON.stringify(data.data));
  })
  emitter.on(close, (data: emitter.EventData) => {
    client.close();
  })
}

@Concurrent
function connectWS() {
  let connect: emitter.InnerEvent = {
    eventId: 1
  };
  emitter.emit(connect);
}

@Concurrent
function sendWS(msg: string) {
  let send: emitter.InnerEvent = {
    eventId: 2
  };
  let eventData: emitter.EventData = {
    data: {
      content: msg,
    }
  };
  emitter.emit(send, eventData);
}

@Concurrent
function closeWS() {
  let close: emitter.InnerEvent = {
    eventId: 3
  };
  emitter.emit(close);
}


@Entry
@Component
struct Index {

  @State url: string = "ws://192.168.41.201:21000/ws";
  @State message: string = "aaaaa";

  build() {
    Column({space: 15}) {
      TextInput({text: this.url}).width('30%')
      Button("初始化")
        .onClick(() => {
          let task = new taskpool.Task(initWebSocket, this.url);
          taskpool.execute(task);
        })
        .width('30%')
      Button("连接")
        .onClick(() => {
          let task = new taskpool.Task(connectWS);
          taskpool.execute(task);
        })
        .width('30%')
      Row({space: 10}) {
        TextInput({text: this.message}).width('25%')
        Button("发消息")
          .onClick(() => {
            let task = new taskpool.Task(sendWS, this.message);
            taskpool.execute(task);
          })
          .width('25%')
      }
      .justifyContent(FlexAlign.Center)
      .width('50%')
      Button("关闭")
        .onClick(() => {
          let task = new taskpool.Task(closeWS);
          taskpool.execute(task);
        })
        .width('30%')
    }
    .justifyContent(FlexAlign.Center)
    .height('100%')
    .width('100%')
  }
}

上面的代码大家应该都能看懂,但是我还是要说一些细节点。initWebSocket这个函数专门用于接收消息,其他的是发送方,这样发送方无论哪个线程都是OK的,initWebSocket函数的emitter.on始终在一个线程中接收,这样就能保证无论有多少个线程,接收方都是按照消息队列收取的,也不存在数据竞争问题了。还有一个关键点,ws的内部功能都在非UI线程中了。问题得到解决,不过我没有压力测试,不知道emitter对于背压会不会有什么问题,大家可以测下。

从log截图看,接收的线程始终在4971中。

根据华为技术给我的答复,arkts确实不支持这样的场景,要么放主线程,要么通过主线程与子线程的通讯来实现,这个emitter是我无意间想到的。。。不知道算不算旁门左道。但至少解决了我的问题。而且我也想不通微信,京东这些大厂的app都是用了什么办法,这样的使用场景我觉得是非常的常见的。

还有关注到仓颉语言,大概看了下,它支持常规的多线程,共享内存,锁 都可以用,或许以后慢慢的发展会过渡到这个语言上。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Diros2025

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值