53用d编程共享数据
消息传递是安全的并发方法.
多线程读写相同数据.共享数据不安全,就是大家(不受控的线程)都要来争.本章虽然简单,但实际中经常遇见.尽管用std.concurrency
,对core.thread
也是适用的.
在d中不是自动共享的.
默认为tls(本地线程)的.尽管所有线程都可访问模块
级变量,但每个线程只是得到一个副本.
import std.stdio;
import std.concurrency;
import core.thread;
int variable;
void printInfo(string message) {
writefln("%s: %s (@%s)", message, variable, &variable);
}
void worker() {
variable = 42;
printInfo("结束工作前");
}
void main() {
spawn(&worker);
thread_joinAll();
printInfo("结束工作后");
}
两个地址和值是不一样的.两份.
因而,spawn
不允许传递引用给线本
对象.
import std.concurrency;
void worker(bool * isDone) {
while (!(*isDone)) {
// ...
}
}
void main() {
bool isDone = false;
spawn(&worker, &isDone);// 编译错误,引用
// ...
// 希望发出信号表明终止
isDone = true;
// ...
}
线程本地数据,给不了其他线程,是引用的地址.
std.concurrency
的static assert
避免访问其他线程的可变数据.
但是可以访问全局共享(整个程序独一份)的__gshared
.在与c/c++
库(默认数据共享)交流时需要__gshared
.
线程间可变数据的共享,必须要有shared
关键字.
import std.concurrency;
void worker(shared(bool) * isDone) {
while (*isDone) {
// ...
}
}
void main() {
shared(bool) isDone = false;
spawn(&worker, &isDone);
// ...
isDone = true;// 通知可以结束了:
// ...
}
一般先考虑传递消息
,再用共享
.
immutable
是可共享的.暗含共享
.
import std.stdio;
import std.concurrency;
import core.thread;
void worker(immutable(int) * data) {
writeln("data: ", *data);
}
void main() {//不变的数据,随便用
immutable(int) i = 42;
spawn(&worker, &i); //编译
thread_joinAll();
}
无论在哪的不变
都是可以随便用的.
core.thread.thread_joinAll
等待所有子线程结束.
竞技条件示例
在线程间共享可变数据
时,要小心程序
是否正确
.
考虑多线程共享相同可变变量.
import std.stdio;
import std.concurrency;
import core.thread;
void swapper(shared(int) * first, shared(int) * second) {
foreach (i; 0 .. 10_000) {
int temp = *second;
*second = *first;*first = temp;
}
}
void main() {
shared(int) i = 1;
shared(int) j = 2;
writefln("before: %s and %s", i, j);
foreach (id; 0 .. 10) {
spawn(&swapper, &i, &j);
}
thread_joinAll();//等待所有线程来完成他们任务
writefln("after : %s and %s", i, j);
}
由于竞争
关系,都破坏了,结果是随机的,10个线程在随机改.都是瞎搞.上面原因就是多个线程,至少1个都在改.
用synchronized
(同步)来避免都来竞争.必须拿到锁才能做事情,否则就等吧.
foreach (i; 0 .. 10_000) {
synchronized {
int temp = *b;
*b = *a;
*a = temp;
}
}
改为,多个线程,要同步.
锁很贵.在有些情况下,可以用原子
来保证正确,而不用锁.
要同步多个块时,对每个块加同步
.
void incrementer(shared(int) * value) {
foreach (i; 0 .. count) {
*value = *value + 1;
}//如分别对此
}
void decrementer(shared(int) * value) {
foreach (i; 0 .. count) {//及此同步,是不正确的
*value = *value - 1;
}//因为锁不一样
}
要如下才能同步,光一个同步是不行的.
import std.stdio;
import std.concurrency;
import core.thread;
enum count = 1000;
class Lock {
}
void incrementer(shared(int) * value, shared(Lock) lock) {
foreach (i; 0 .. count) {
synchronized (lock) {//同步锁
*value = *value + 1;
}
}
}
void decrementer(shared(int) * value, shared(Lock) lock) {
foreach (i; 0 .. count) {
synchronized (lock) {//同步锁
*value = *value - 1;
}
}
}
void main() {
shared(Lock) lock = new shared(Lock)();
//任何类对象都可用来作为同步锁
shared(int) number = 0;
foreach (i; 0 .. 100) {
spawn(&incrementer, &number, lock);
//必须用同一把锁来同步,所以必须共享锁
spawn(&decrementer, &number, lock);
}
thread_joinAll();
writeln("Final value: ", number);
}
也可定义类类型
同步
synchronized class Class {
void foo() {
// ...
}
void bar() {
// ...
}
}
即在给定类对象上,同步所有(非静态)成员函数.
等价于如下类:
class Class {
void foo() {
synchronized (this) {
// ...
}
}
void bar() {
synchronized (this) {
// ...
}
}
}
在多个对象上同步时,需要同时指定所有对象.否则你等待我,我等待你,造成死锁.
例如多线程中转账.
void transferMoney(shared BankAccount from,
shared BankAccount to) {
synchronized (from) {
synchronized (to) {//分开了,是错误的
// ...
}
}
}
有可能两个线程转账,都在锁定.A锁定from(A),B锁定from(B),结果都锁定了,都在等待,死锁了.
void transferMoney(shared BankAccount from,
shared BankAccount to) {
// 注意: dmd 2.074.0不支持
synchronized (from, to) {
// ...
}
}
要这样,按顺序挨个挨个锁上,
shared static this
用于共享初化,只初化一次,static this
而则每个线程都要执行一次,以便所有线程初化模块级变量.
shared static ~this()
共享析构.
import std.stdio;
import std.concurrency;
import core.thread;
static this() {//要出错
writeln("执行static this()");
}
void worker() {
}
void main() {
spawn(&worker);
thread_joinAll();
}
要这样
int a; // 线本
immutable int b; // 所有线程,不变始终是安全的
//当然,如果多次初始化,那一定要出错
static this() {
writeln("每线程变量", &a);
a = 42;
}
shared static this() {
writeln("每程序变量", &b);
b = 43;
}
同样,shared static ~this()
,程序级别的最终处理.
原子操作
.
另一种确保只一个线程改变特定变量的方法是使用原子操作
.
由微控器,编译器,操作系统
提供.
d的原子操作在core.atomic
.
atomicOp
,应用像"+", "+="
样的模板参数到其双函数参数中.
import core.atomic;
// ...
atomicOp!"+="(*value, 1);//原子
等价于非原子行的星value += 1;
只是原子操作保证单线程执行(不受其他线程干扰).
因而,当只需要同步二元操作时,没必要使用同步
块(因为锁,所以太慢)
import core.atomic;
//...
void incrementer(shared(int) * value) {
foreach (i; 0 .. count) {
atomicOp!"+="(*value, 1);
}
}
void decrementer(shared(int) * value) {
foreach (i; 0 .. count) {
atomicOp!"-="(*value, 1);
}
}
由于是原子操作,所以不用担心出错了.没必要再用锁类了.
也可与其他二元操作一起使用atomicOp
.
cas
大名鼎鼎的比交
(比较交换).如果仍是当前值,则改变
通过同时指定当前值与期望值来用它.
如bool is_mutated = cas(address_of_variable, currentValue, newValue);
仍等于当前值,表明没其他线程改变它.则给它赋值为新值,并返回真
.相反,不同,则不变,返回假
.
void incrementer(shared(int) * value) {
foreach (i; 0 .. count) {
int currentValue;
do {
currentValue = *value;
} while (!cas(value, currentValue, currentValue + 1));
}
}
void decrementer(shared(int) * value) {
foreach (i; 0 .. count) {
int currentValue;
do {
currentValue = *value;
} while (!cas(value, currentValue, currentValue - 1));
}
}
重读旧值
,直到成功操作,调用cas(…).也叫如值为旧值,则用新值替换.其实就是个自旋锁.不断的循环.
上面的函数块无需同步
即可正常操作.
多数时候,core.atomic
模块比用同步块
快几倍.因此尽量用原子
.
原子操作允许无锁编程
数据结构.
你还可以查看core.sync
,其包含经典的并发原语.如:
core.sync.barrier core.sync.condition core.sync.config core.sync.exception core.sync.mutex core.sync.rwmutex core.sync.semaphore
不依赖时,并行(任务).依赖时,并发.
靠传递消息更好
只能共享
shared数据.不变
隐含共享
.
_ _gshared
用于同c/c++交流.
给类定义synchronized
,则只有其他线程不在其上面操作时,才能执行成员函数.特定时间只能执行一个成员函数.
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:基于图像分类模型对图像进行分类
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现