1# Reasonably Using Multithreaded Shared Memory
2## Introduction
3During application development, some time-consuming operations are executed in subthreads to prevent the main thread from being blocked. In this case, the subthreads need to access data in the main thread. ArkTS adopts the actor model that is based on message communication and therefore features memory isolation, which requires data serialization during cross-thread transmission. However, ArkTS supports shared memory through SharedArrayBuffer.
4
5If the data volume is large and multiple threads are required for simultaneous operations, you are advised to use SharedArrayBuffer to reduce the extra overhead caused by data replication and serialization during cross-thread transmission. Example scenarios are audio/video decoding and playback, and simultaneous file read and write by multiple threads. In addition, you can use locks to prevent data disorder when multiple threads simultaneously operate the same memory. This topic describes SharedArrayBuffer and locks in details. For details about the usage and principles of multiple threads, see [Multithread Capability Scenarios](./multi_thread_capability.md).
6## Working Principles
7A **SharedArrayBuffer** object is a raw binary data buffer with a fixed length. It can store any type of data including numbers and strings. It can be transferred between multiple threads. The objects before and after the transfer point to the same memory block, achieving memory sharing. If multiple operations are simultaneously performed by subthreads to modify data stored in a **SharedArrayBuffer** object, you must use atomic operations to ensure data synchronization. Atomic operations ensure that the current operation is complete before the next operation starts. The following uses an example to describe the necessity of atomic operations for purposes of synchronization. The full code can be obtained from [AtomicsUsage.ets](https://gitee.com/openharmony/applications_app_samples/blob/master/code/Performance/PerformanceLibrary/feature/memoryShared/src/main/ets/pages/AtomicsUsage.ets).
8### Non-atomic Operation
9
10```javascript
11......
12// Non-atomic operation, which performs auto-increment operations for 10,000 times.
13@Concurrent
14function normalProcess(int32Array: Int32Array) {
15  for (let i = 0; i < 10000; i++) {
16    int32Array[0]++;
17  }
18}
19// Atomic operation, which performs auto-increment operations for 10,000 times.
20@Concurrent
21function atomicsProcess(int32Array: Int32Array) {
22  for (let i = 0; i < 10000; i++) {
23    Atomics.add(int32Array, 0, 1);
24  }
25}
26......
27@State result: string = "Calculation result:";
28private taskNum: number = 2;
29private scroller: Scroller = new Scroller();
30......
31Button ("Non-atomic Operation")
32  .width("80%")
33  .fontSize(30)
34  .fontWeight(FontWeight.Bold)
35  .margin({ top: 30 })
36  .onClick(async () => {
37     this.sharedArrayBufferUsage(false);
38  })
39Scroll(this.scroller) {
40  Column() {
41    Text(this.result)
42      .width("80%")
43      .fontSize(30)
44      .fontWeight(FontWeight.Bold)
45      .fontColor(Color.Blue)
46  }
47}.height("60%")
48.margin({ top: 30 })
49......
50// Determine whether to use atomic operations based on the passed-in value of isAtomics.
51sharedArrayBufferUsage(isAtomics: boolean) {
52  // Create a 4-byte SharedArrayBuffer object.
53  let sab: SharedArrayBuffer = new SharedArrayBuffer(4);
54  // SharedArrayBuffer is a raw binary data buffer and cannot be directly used. Therefore, SharedArrayBuffer is converted to the Int32Array type for subsequent operations.
55  let int32Array: Int32Array = new Int32Array(sab);
56  int32Array[0] = 0;
57  let taskGroup: taskpool.TaskGroup = new taskpool.TaskGroup();
58  // Create a Task object and put it in the task group for execution.
59  for (let i = 0; i < this.taskNum; i++) {
60    if (isAtomics) {
61      taskGroup.addTask(new taskpool.Task(atomicsProcess, int32Array));
62    } else {
63      taskGroup.addTask(new taskpool.Task(normalProcess, int32Array));
64    }
65  }
66  taskpool.execute(taskGroup).then(() => {
67    // Print the result on the <Text> component.
68    this.result = this.result + "\n" + int32Array;
69    // If the mouse pointer is not at the bottom of the screen, scroll to the bottom.
70    if (!this.scroller.isAtEnd()) {
71      this.scroller.scrollEdge(Edge.Bottom);
72    }
73  }).catch((e: BusinessError) => {
74    logger.error(e.message);
75  })
76}
77
78```
79In this code, two tasks are created, each of which is used to perform 10,000 auto-increment operations on the **SharedArrayBuffer** object. The expected result is 20000. The calculation result may not be 20000, and the result may be different each time you touch the **Non-atomic Operation** button. This is because SharedArrayBuffer is shared memory. When multiple threads perform auto-increment at the same time, the same memory block is operated. The auto-increment operation, however, is not an atomic operation and can be divided into the following three steps:
80- Step 1: Obtains the value from the memory.
81- Step 2: Increases the obtained value by 1.
82- Step 3: Writes the result to the memory.
83
84When multiple threads operate the memory at the same time, the following situation occurs: Thread A obtains the value 1000 from the memory and increases the value by 1 (the result is now 1001). Before thread A writes the result to the memory, thread B obtains the value, which is still 1000. Thread A writes 1001 to the memory. Thread B increases the value by 1 (the result is now 1001) and writes the result 1001 to the memory. As a result, the operation of increasing the value by 1 is performed twice, but the result is 1001 instead of 1002. The result does not meet the expectation.
85### Atomic Operations
86The code below uses the atomic operation **Atomics.add()** to perform the auto-increment operation.
87
88```javascript
89......
90Button("Atomic Operation")
91  .width("80%")
92  .fontSize(30)
93  .fontWeight(FontWeight.Bold)
94  .margin({ top: 30 })
95  .onClick(async () => {
96    this.sharedArrayBufferUsage(true);
97  })
98......
99
100```
101No matter how many times you touch the **Atomic Operation** button, the result is always 20,000. This is because the atomic operation is one or a series of operations that cannot be interrupted. It ensures the operation of thread A is not interrupted by thread B, and thread B always obtains the new value written by thread A to the memory. Therefore, when using SharedArrayBuffer, use atomic operations to ensure data synchronization and avoid data disorder.
102## Example
103In the case of complex logic, it cannot be ensured the entire thread carries out an atomic operation. Locks are introduced to address this scenario.
104### Implementation of Locks
105Concurrent programming focuses on solving the problems of task division, synchronization, and mutual exclusion between threads, and locks are the important way to implement mutual exclusion. The code snippet below implements the lock **NonReentrantLock** by using Atomics and SharedArrayBuffer.
106
107The **constructor()** method initializes the lock by passing in a **SharedArrayBuffer** object. Multiple threads operate the same shared memory and use a flag bit to control the lock status.
108
109```javascript
110const UNLOCKED = 0;
111const LOCKED_SINGLE = 1;
112const LOCKED_MULTI = 2;
113export class NonReentrantLock {
114  flag: Int32Array;
115  constructor(sab: SharedArrayBuffer) { // Pass in a 4-byte SharedArrayBuffer.
116    this.flag= new Int32Array(sab); // The view is a one-bit int array (1 = 4 bytes * 8 / 32 bits).
117  }
118
119  lock(): void {...}
120  tryLock(): boolean {...}
121  unlock(): void {...}
122}
123
124```
125The **lock()** method is used to obtain a lock. A thread that fails to obtain the lock is blocked.
126
127```javascript
128lock(): void {
129  const flag= this.flag;
130  let c = UNLOCKED;
131  // If the current value is UNLOCKED at the index 0 of the flag array, the value is changed to LOCKED_SINGLE. Otherwise, the code enters the do-while loop and the thread is blocked.
132  if ((c = Atomics.compareExchange(flag, 0, UNLOCKED, LOCKED_SINGLE)) !== UNLOCKED) {
133    do {
134      // A thread fails to obtain the lock. Its flag bit is changed to LOCKED_MULTI, and the thread is blocked.
135      if (c === LOCKED_MULTI || Atomics.compareExchange(flag, 0, LOCKED_SINGLE, LOCKED_MULTI) !== UNLOCKED) {
136        Atomics.wait(flag, 0, LOCKED_MULTI);
137      }
138    // If the thread is woken up and fails to obtain the lock again, the thread returns to the loop and is blocked again.
139    } while ((c = Atomics.compareExchange(flag, 0, UNLOCKED, LOCKED_MULTI)) !== UNLOCKED);
140  }
141}
142
143```
144The **tryLock()** method is used to try to obtain the lock. If the lock is obtained, **true** is returned. If the lock fails to be obtained, **false** is returned, but the thread is not blocked.
145
146```javascript
147tryLock(): boolean {
148  const flag= this.flag;
149  return Atomics.compareExchange(flag, 0, UNLOCKED, LOCKED_SINGLE) === UNLOCKED;
150}
151
152```
153The **unlock()** method is used to release the lock.
154
155```javascript
156unlock(): void {
157  // Local flag to ensure that only the thread that has the lock can release the lock.
158  const flag= this.flag;
159  let v0 = Atomics.sub(flag, 0, 1);
160  if (v0 !== LOCKED_SINGLE) {
161    Atomics.store(flag, 0, UNLOCKED);
162    // Wake up only one thread waiting at the index 0 of the array, check the while condition in the lock() method, and try to obtain the lock.
163    Atomics.notify(flag, 0, 1);
164  }
165}
166
167```
168### Usage of Locks
169In the example scenario below where multiple threads write data to a file, improper manipulation on the shared memory makes the thread insecure. Consequently, garbled characters are displayed in the output file. The lock **NonReentrantLock** implemented above is used to solve the problem.
170
171The main thread uses the **startWrite(useLock: boolean)** method to enable multiple threads to write data to the file and uses the **useLock** parameter to determine whether to use the lock.
172
173```javascript
174@Component
175export struct LockUsage {
176  taskNum: number = 10; // Number of tasks. The actual number of concurrent threads depends on the device.
177  baseDir: string = getContext().filesDir + '/TextDir'; // Application sandbox path of the file.
178  sabInLock: SharedArrayBuffer = new SharedArrayBuffer(4); // Shared memory used by the main thread to initialize the lock flag bit of the subthread.
179  sabForLine: SharedArrayBuffer = new SharedArrayBuffer(4); // Shared memory used by the main thread to initialize the offset bit of the subthread.
180  @State result: string = "";
181  build() {
182    Row() {
183      Column() {
184        // The button indicates that the lock is not used.
185        Button($r('app.string.not_use_lock'))
186          .width("80%").fontSize(30)
187          .fontWeight(FontWeight.Bold)
188          .margin({ top: 30 })
189          .onClick(async () => {
190            this.startWrite(false);
191          })
192        // The button indicates that the lock is used.
193        Button($r('app.string.use_lock'))
194          .width("80%")
195          .fontSize(30)
196          .fontWeight(FontWeight.Bold)
197          .margin({ top: 30 })
198          .onClick(async () => {
199            this.startWrite(true);
200          })
201        // Running status description
202        Text(this.result)
203          .width("80%")
204          .fontSize(30)
205          .fontWeight(FontWeight.Bold)
206          .fontColor(Color.Blue)
207          .margin({ top: 30 })
208      }
209      .width('100%')
210    }
211    .height('100%')
212  }
213  startWrite(useLock: boolean): void {
214    // Specify the running status as "Starting writing data to the file".
215    this.result = getContext().resourceManager.getStringSync($r('app.string.write_file_start'));
216    // Initialize the offset for writing.
217    let whichLineToWrite: Int32Array = new Int32Array(this.sabForLine);
218    Atomics.store(whichLineToWrite, 0, 0);
219    // Enable multiple threads to write the file based on the offset.
220    // Initialize the lock through sabInLock:SharedArrayBuffer of the main thread to ensure that multiple threads operate the same lock flag.
221    // Initialize the offset bit through sabForLine:SharedArrayBuffer of the main thread to ensure that multiple threads operate the same offset.
222    let taskPoolGroup: taskpool.TaskGroup = new taskpool.TaskGroup();
223    for (let i: number = 0; i < this.taskNum; i++) {
224      taskPoolGroup.addTask(new taskpool.Task(createWriteTask, this.baseDir, i, this.sabInLock, this.sabForLine, useLock));
225    }
226    taskpool.execute(taskPoolGroup).then(() => {
227      // Specify the running status as "Writing the file succeeded."
228      this.result = this.result = getContext().resourceManager.getStringSync($r('app.string.write_file_success'));
229    }).catch(() => {
230      // Specify the running status as "Failed to write the file."
231      this.result = getContext().resourceManager.getStringSync($r('app.string.write_file_failed'));
232    })
233  }
234}
235
236```
237The subthread writes data to the file at the specified position based on the offset and specifies the position to write next time by automatically incrementing the offset.
238
239```javascript
240@Concurrent
241async function createWriteTask(baseDir: string, writeText: number, sabInLock: SharedArrayBuffer, sabForLine: SharedArrayBuffer, useLock: boolean): Promise<void> {
242  class Option {// Interface method parameter class for writing data to a file.
243    offset: number = 0;
244    length: number = 0;
245    encoding: string = 'utf-8';
246
247    constructor(offset: number, length: number) {
248      this.offset = offset;
249      this.length = length;
250    }
251  }
252  // Initialize the output file directory.
253  let filePath: string | undefined = undefined;
254  filePath = baseDir + useLock ? "/useLock.txt" : "/unusedLock.txt";
255  if (!fs.accessSync(baseDir)) {
256    fs.mkdirSync(baseDir);
257  }
258  // Use SharedArrayBuffer passed in by the main thread to initialize the lock.
259  let nrl: NonReentrantLock | undefined = undefined;
260  if (useLock) {
261    nrl = new NonReentrantLock(sabInLock);
262  }
263  // Use SharedArrayBuffer passed in by the main thread to initialize the offset for file writing.
264  let whichLineToWrite: Int32Array = new Int32Array(sabForLine);
265  let str: string = writeText + '\n';
266  for (let i: number = 0; i < 100; i++) {
267    // Obtain the lock.
268    if (useLock && nrl !== undefined) {
269      nrl.lock();
270    }
271    // Write data to the file.
272    let file: fs.File = fs.openSync(filePath, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);
273    try {
274      fs.writeSync(file.fd, str, new Option(whichLineToWrite[0], str.length));
275    } catch (err) {
276      logger.error(`errorCode : ${err.code},errMessage : ${err.message}`);
277    }
278    fs.closeSync(file);
279    // Modify the offset to specify the position for the next write.
280    whichLineToWrite[0] += str.length;
281    // Release the lock.
282    if (useLock && nrl !== undefined) {
283      nrl.unlock();
284    }
285  }
286}
287
288```
289View the written file in the application sandbox directory. The **unusedLock.txt** file contains less than 1000 lines with garbled characters, as shown in Figure 1.
290
291**Figure 1** File written without using the lock
292
293![unusedLock.txt](./figures/not_use_lock.png)
294
295The **usedLock.txt** file contains 1000 lines without garbled characters, as shown in Figure 2.
296
297**Figure 2** File written using the lock
298
299![usedLock.txt](./figures/use_lock.png)
300
301## Summary
302ArkTS supports inter-thread communication through shared memory, although it uses the actor model based on message communication. When SharedArrayBuffer is used to implement shared memory, atomic operations and locks can be used to solve the problem of synchronization and mutual exclusion between threads. Proper use of multithreaded shared memory can improve application performance while ensuring thread security.
303
304 <!--no_check-->