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 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 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-->