| ... | ... | @@ -301,81 +301,86 @@ test "concurrent access" { |
| 301 | 301 | |
| 302 | 302 | const num_writers: usize = 2; |
| 303 | 303 | const num_readers: usize = 4; |
| 304 | | const num_writes: usize = 4096; |
| 305 | | const num_reads: usize = 8192; |
| 304 | const num_writes: usize = 1000; |
| 305 | const num_reads: usize = 2000; |
| 306 | 306 | |
| 307 | 307 | const Runner = struct { |
| 308 | | const Self = @This(); |
| 308 | const Runner = @This(); |
| 309 | 309 | |
| 310 | | rwl: RwLock = .{}, |
| 311 | | writes: usize = 0, |
| 312 | | reads: std.atomic.Value(usize) = std.atomic.Value(usize).init(0), |
| 310 | rwl: RwLock, |
| 311 | writes: usize, |
| 312 | reads: std.atomic.Value(usize), |
| 313 | 313 | |
| 314 | | term1: usize = 0, |
| 315 | | term2: usize = 0, |
| 316 | | term_sum: usize = 0, |
| 314 | val_a: usize, |
| 315 | val_b: usize, |
| 317 | 316 | |
| 318 | | fn reader(self: *Self) !void { |
| 317 | fn reader(run: *Runner, thread_idx: usize) !void { |
| 318 | var prng = std.Random.DefaultPrng.init(thread_idx); |
| 319 | const rnd = prng.random(); |
| 319 | 320 | while (true) { |
| 320 | | self.rwl.lockShared(); |
| 321 | | defer self.rwl.unlockShared(); |
| 321 | run.rwl.lockShared(); |
| 322 | defer run.rwl.unlockShared(); |
| 322 | 323 | |
| 323 | | if (self.writes >= num_writes or self.reads.load(.unordered) >= num_reads) |
| 324 | | break; |
| 324 | try testing.expect(run.writes <= num_writes); |
| 325 | if (run.reads.fetchAdd(1, .monotonic) >= num_reads) break; |
| 325 | 326 | |
| 326 | | try self.check(); |
| 327 | // We use `volatile` accesses so that we can make sure the memory is accessed either |
| 328 | // side of a yield, maximising chances of a race. |
| 329 | const a_ptr: *const volatile usize = &run.val_a; |
| 330 | const b_ptr: *const volatile usize = &run.val_b; |
| 327 | 331 | |
| 328 | | _ = self.reads.fetchAdd(1, .monotonic); |
| 332 | const old_a = a_ptr.*; |
| 333 | if (rnd.boolean()) try std.Thread.yield(); |
| 334 | const old_b = b_ptr.*; |
| 335 | try testing.expect(old_a == old_b); |
| 329 | 336 | } |
| 330 | 337 | } |
| 331 | 338 | |
| 332 | | fn writer(self: *Self, thread_idx: usize) !void { |
| 339 | fn writer(run: *Runner, thread_idx: usize) !void { |
| 333 | 340 | var prng = std.Random.DefaultPrng.init(thread_idx); |
| 334 | | var rnd = prng.random(); |
| 335 | | |
| 341 | const rnd = prng.random(); |
| 336 | 342 | while (true) { |
| 337 | | self.rwl.lock(); |
| 338 | | defer self.rwl.unlock(); |
| 343 | run.rwl.lock(); |
| 344 | defer run.rwl.unlock(); |
| 339 | 345 | |
| 340 | | if (self.writes >= num_writes) |
| 341 | | break; |
| 346 | try testing.expect(run.writes <= num_writes); |
| 347 | if (run.writes == num_writes) break; |
| 342 | 348 | |
| 343 | | try self.check(); |
| 349 | // We use `volatile` accesses so that we can make sure the memory is accessed either |
| 350 | // side of a yield, maximising chances of a race. |
| 351 | const a_ptr: *volatile usize = &run.val_a; |
| 352 | const b_ptr: *volatile usize = &run.val_b; |
| 344 | 353 | |
| 345 | | const term1 = rnd.int(usize); |
| 346 | | self.term1 = term1; |
| 347 | | try std.Thread.yield(); |
| 354 | const new_val = rnd.int(usize); |
| 348 | 355 | |
| 349 | | const term2 = rnd.int(usize); |
| 350 | | self.term2 = term2; |
| 351 | | try std.Thread.yield(); |
| 356 | const old_a = a_ptr.*; |
| 357 | a_ptr.* = new_val; |
| 358 | if (rnd.boolean()) try std.Thread.yield(); |
| 359 | const old_b = b_ptr.*; |
| 360 | b_ptr.* = new_val; |
| 361 | try testing.expect(old_a == old_b); |
| 352 | 362 | |
| 353 | | self.term_sum = term1 +% term2; |
| 354 | | self.writes += 1; |
| 363 | run.writes += 1; |
| 355 | 364 | } |
| 356 | 365 | } |
| 357 | | |
| 358 | | fn check(self: *const Self) !void { |
| 359 | | const term_sum = self.term_sum; |
| 360 | | try std.Thread.yield(); |
| 361 | | |
| 362 | | const term2 = self.term2; |
| 363 | | try std.Thread.yield(); |
| 364 | | |
| 365 | | const term1 = self.term1; |
| 366 | | try testing.expectEqual(term_sum, term1 +% term2); |
| 367 | | } |
| 368 | 366 | }; |
| 369 | 367 | |
| 370 | | var runner = Runner{}; |
| 371 | | var threads: [num_writers + num_readers]std.Thread = undefined; |
| 372 | | |
| 373 | | for (threads[0..num_writers], 0..) |*t, i| t.* = try std.Thread.spawn(.{}, Runner.writer, .{ &runner, i }); |
| 374 | | for (threads[num_writers..]) |*t| t.* = try std.Thread.spawn(.{}, Runner.reader, .{&runner}); |
| 368 | var run: Runner = .{ |
| 369 | .rwl = .{}, |
| 370 | .writes = 0, |
| 371 | .reads = .init(0), |
| 372 | .val_a = 0, |
| 373 | .val_b = 0, |
| 374 | }; |
| 375 | var write_threads: [num_writers]std.Thread = undefined; |
| 376 | var read_threads: [num_readers]std.Thread = undefined; |
| 375 | 377 | |
| 376 | | for (threads) |t| t.join(); |
| 378 | for (&write_threads, 0..) |*t, i| t.* = try .spawn(.{}, Runner.writer, .{ &run, i }); |
| 379 | for (&read_threads, num_writers..) |*t, i| t.* = try .spawn(.{}, Runner.reader, .{ &run, i }); |
| 377 | 380 | |
| 378 | | try testing.expectEqual(num_writes, runner.writes); |
| 381 | for (write_threads) |t| t.join(); |
| 382 | for (read_threads) |t| t.join(); |
| 379 | 383 | |
| 380 | | //std.debug.print("reads={}\n", .{ runner.reads.load(.unordered)}); |
| 384 | try testing.expect(run.writes == num_writes); |
| 385 | try testing.expect(run.reads.raw >= num_reads); |
| 381 | 386 | } |