| ... | @@ -301,81 +301,86 @@ test "concurrent access" { | ... | @@ -301,81 +301,86 @@ test "concurrent access" { |
| 301 | | 301 | |
| 302 | const num_writers: usize = 2; | 302 | const num_writers: usize = 2; |
| 303 | const num_readers: usize = 4; | 303 | const num_readers: usize = 4; |
| 304 | const num_writes: usize = 4096; | 304 | const num_writes: usize = 1000; |
| 305 | const num_reads: usize = 8192; | 305 | const num_reads: usize = 2000; |
| 306 | | 306 | |
| 307 | const Runner = struct { | 307 | const Runner = struct { |
| 308 | const Self = @This(); | 308 | const Runner = @This(); |
| 309 | | 309 | |
| 310 | rwl: RwLock = .{}, | 310 | rwl: RwLock, |
| 311 | writes: usize = 0, | 311 | writes: usize, |
| 312 | reads: std.atomic.Value(usize) = std.atomic.Value(usize).init(0), | 312 | reads: std.atomic.Value(usize), |
| 313 | | 313 | |
| 314 | term1: usize = 0, | 314 | val_a: usize, |
| 315 | term2: usize = 0, | 315 | val_b: usize, |
| 316 | term_sum: usize = 0, | | |
| 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 | while (true) { | 320 | while (true) { |
| 320 | self.rwl.lockShared(); | 321 | run.rwl.lockShared(); |
| 321 | defer self.rwl.unlockShared(); | 322 | defer run.rwl.unlockShared(); |
| 322 | | 323 | |
| 323 | if (self.writes >= num_writes or self.reads.load(.unordered) >= num_reads) | 324 | try testing.expect(run.writes <= num_writes); |
| 324 | break; | 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 | var prng = std.Random.DefaultPrng.init(thread_idx); | 340 | var prng = std.Random.DefaultPrng.init(thread_idx); |
| 334 | var rnd = prng.random(); | 341 | const rnd = prng.random(); |
| 335 | | | |
| 336 | while (true) { | 342 | while (true) { |
| 337 | self.rwl.lock(); | 343 | run.rwl.lock(); |
| 338 | defer self.rwl.unlock(); | 344 | defer run.rwl.unlock(); |
| 339 | | 345 | |
| 340 | if (self.writes >= num_writes) | 346 | try testing.expect(run.writes <= num_writes); |
| 341 | break; | 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); | 354 | const new_val = rnd.int(usize); |
| 346 | self.term1 = term1; | | |
| 347 | try std.Thread.yield(); | | |
| 348 | | 355 | |
| 349 | const term2 = rnd.int(usize); | 356 | const old_a = a_ptr.*; |
| 350 | self.term2 = term2; | 357 | a_ptr.* = new_val; |
| 351 | try std.Thread.yield(); | 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; | 363 | run.writes += 1; |
| 354 | self.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{}; | 368 | var run: Runner = .{ |
| 371 | var threads: [num_writers + num_readers]std.Thread = undefined; | 369 | .rwl = .{}, |
| 372 | | 370 | .writes = 0, |
| 373 | for (threads[0..num_writers], 0..) |*t, i| t.* = try std.Thread.spawn(.{}, Runner.writer, .{ &runner, i }); | 371 | .reads = .init(0), |
| 374 | for (threads[num_writers..]) |*t| t.* = try std.Thread.spawn(.{}, Runner.reader, .{&runner}); | 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 | } |