Dlang 并行化

好难受,dlang 生态太差,没办法,学了半天才明白。

我尽量以精炼的语言解释。

采用 定义,例子(代码),解释 的步骤讲解。

所以你可能看到很多代码,一点解释……

我会省略一些 import,让代码短一些

parallelism 并行

感觉好废物,这一小部分了解即可。

这部分只需要会 parallelmap & amap 其实就差不多了。

介绍比较实用的几种方法。

parallel 迭代

foreach (i; parallel(range, work_uint_size = 100)) {
// do something here
}

其中 work_unit_size 表示最多同时运行的数量。

例子

import std.stdio, std.parallelism;
import core.thread; struct Producer {
void produce() {
Thread.sleep(1.seconds); writeln("Process +1");
}
}; void main() {
auto prods = new Producer[](10); foreach (prod; parallel(prods)) {
prod.produce();
}
}

Task

创建任务:

auto theTask = task!anOperation(arguments);
// or
auto theTask = task(&someFunction, parameters...)

运行任务:theTask.executeInNewThread()

查看是否完成:if (theTask.done) { ... }

获取结果:auto result = theTask.yeildForce()


asyncBuf

感觉没啥用。

并行保存多个需要长时间制作的元素。还需要保证使用的长时间的……

例子:

struct Producer {
int i, total; bool empty() const {
return total <= i;
} int front() const {
return i;
} void popFront() {
writefln("Producing product ID: %d", i);
Thread.sleep(1.seconds / 2);
++i;
}
}; void main() {
auto prods = Producer(0, 10);
foreach (prod; taskPool.asyncBuf(prods, 3)) {
writef("Got product id: %d\n", prod);
Thread.sleep(1.seconds);
writeln("Used product...");
}
}

map & amap

先看例子:

int increase(int x) {
Thread.sleep(500.msecs);
return x + 3;
} void main() {
int[] nums;
foreach (i; 0 .. 10) {
nums ~= i;
} // auto results = taskPool.map!increase(nums);
auto results = taskPool.amap!increase(nums);
foreach (result; results) {
writeln(result);
}
}

可以类比 python 中的 map

两者的区别:

  • map 可以指定同时运行的数量,而 amap 是有多少运行多少。

  • map 会一定程度上按顺序执行,而 amap 并不是顺序执行,它依靠 RandomAccessRange,也就是随机顺序执行。


消息并发

我不知道怎么翻译,反正就是 Message Passing Concurrency

核心方法: spawn (唤起)

我们可以形象的认为,spawn 方法可以唤起一个新的工人(线程)来为我们工作。

并且这个工人与主线程是分开的(先看代码后面解释):

import std.stdio;
import std.concurrency;
import core.thread;
void worker() {
foreach (i; 0 .. 5) {
Thread.sleep(500.msecs);
writeln(i, " (worker) in ", thisTid); } }
void main() {
Tid myWorkerTid = spawn(&worker);
foreach (i; 0 .. 5) {
Thread.sleep(300.msecs);
writeln(i, " (main) in ", thisTid); } writeln("main is done!");
}

最终输出:

0 (main) in Tid(7f0eb19bc0b0)
0 (worker) in Tid(7f0eb19bc000)
1 (main) in Tid(7f0eb19bc0b0)
2 (main) in Tid(7f0eb19bc0b0)
1 (worker) in Tid(7f0eb19bc000)
3 (main) in Tid(7f0eb19bc0b0)
2 (worker) in Tid(7f0eb19bc000)
4 (main) in Tid(7f0eb19bc0b0)
main is done!
3 (worker) in Tid(7f0eb19bc000)
4 (worker) in Tid(7f0eb19bc000)

实际输出可能略有差异。

解释

  • spawn(&worker) 唤起了一个新的线程运行 worker 函数,并返回了新的线程的 id 是一个结构体 Tid

  • thisTid 类似于一个宏,用于获取当前所在线程的 id


发送消息

先看代码后解释:

void worker() {
int value = 0;
while (value >= 0) {
value = receiveOnly!int();
double result = cast(double)value / 7;
ownerTid.send(result);
}
} void main() {
Tid myWorker = spawn(&worker); foreach (val; 0 .. 10) {
myWorker.send(val);
double result = receiveOnly!double();
writefln("Send %s got %s", val, result);
} myWorker.send(-1); // terminate worker process
}

最终输出:

Send 0 got 0
Send 1 got 0.142857
Send 2 got 0.285714
Send 3 got 0.428571
Send 4 got 0.571429
Send 5 got 0.714286
Send 6 got 0.857143
Send 7 got 1
Send 8 got 1.14286
Send 9 got 1.28571

解释

  • ownerTid 类似于一个宏,用于取得唤醒自己的线程的 Tid,从而发送消息。

  • Tid.send(...) 可以向 Tid 代表的那个线程发送一条消息。

    • 如果同时要发送多个东西,在发送的地方是 Tid.send(a, b, c, ...)

    • 在接受的地方要变化为 receiveOnly!(typeof(a), typeof(b), typeof(c), ...),最终得到的是一个 tuple,可以通过下标访问。

  • receiveOnly!type() 表示只接受类型为 type 的消息。

  • 最后 myWorker.send(-1) 是根据代码逻辑结束的,并不属于通法。

如果我们需要更灵活的接受方法怎么办?

void workerFunc() {
bool isDone = false;
while (!isDone) {
void intHandler(int message) {
writeln("handling int message: ", message); if (message == -1) {
writeln("exiting");
isDone = true;
}
} void stringHandler(string message) {
writeln("handling string message: ", message);
}
    
        receive(&intHandler, &stringHandler);
    }    
}

我们可以指定多种 Handler 以处理不同的数据类型。利用 receive 注册 到处理类型消息的函数中。


更优雅的方式

处理更多的类型:

struct Exit {}

void worker() {
bool done = false; while (!done) {
receive(
(int message) {
writeln("int message ", message);
}, (string message) {
writeln("string message", message);
}, (Exit message) {
writeln("Exit message");
done = true;
}, (Variant message) {
writeln("Unexpected message: ", message);
}
);
}
} void main() {
Tid myWorker = spawn(&worker); myWorker.send(10);
myWorker.send("hello");
myWorker.send(10.1);
myWorker.send(Exit());
}

主要是使用了匿名函数……

解释

  • 利用 std.variant.Variant 以接收任何类型的数据。但是需要保证,处理所有类型数据的方法应该放在最后面,不然会导致全部被判断成 Variant

超时接受

我们可以定一个超时时间,超过这个时间就直接返回。

先看代码:

struct Exit {}

void worker() {
bool done = false; while (!done) {
bool received = receiveTimeout(600.msecs,
(Exit message) {
writeln("Exit message");
done = true;
}, (Variant message) {
writeln("Some message: ", message);
}
);
if (!received) {
writeln("no message yet...");
}
}
} void main() {
Tid myWorker = spawn(&worker); myWorker.send(10);
myWorker.send("hello");
Thread.sleep(1.seconds);
myWorker.send(10.1);
myWorker.send(Exit());
}

最终输出

Some message: 10
Some message: hello
no message yet...
Some message: 10.1
Exit message

解释

  • receiveTimeout 只比 recieve 多了一个参数,用于指定超时时间。

  • 返回一个 bool 变量,如果为 false 则没有接收到任何消息。


等待所有线程结束thread_joinAll()

一般来说放在需要放的地方……即可。


数据共享

终于讲到这里了。

我们先考虑一个程序:

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("Before the worker is terminated");
} void main() {
spawn(&worker);
thread_joinAll();
printInfo("After the worker is terminated");
}

其输出是这样的:

Before the worker is terminated: 42 (@7F308C88C530)
After the worker is terminated: 0 (@7F308C98D730)

可以发现,同样的变量在不同的线程里面地址是不一样的,也就是说数据是独立的,所以要有共享。

此时我们只需要修改:

shared int variable;

即可。

实际上写为 shared(int) variable; 会更标准,但是好麻烦……

当然,不得不说,有了消息传递,那么数据共享就是备用的方案了。


Data Race

数据竞争是一个很常见的问题。

例子

void worker(shared int* i) {
foreach (t; 0 .. 200000) {
*i = *i + 1;
}
} void main() {
shared int i = 0; foreach (id; 0 .. 10) {
spawn(&worker, &i);
} thread_joinAll();
writeln("after i to ", i);
}

期望输出 2000000,但是实际输出可能远小于此。

所以我们要考虑同步:

void worker(shared int* i) {
foreach (t; 0 .. 200000) {
synchronized {
*i = *i + 1;
}
}
}

解释

  • synchronized 会隐式地创建一个锁,保证只有一个线程会持有这个锁,并且执行这些操作。

  • 有些时候,synchronized 会使得因为等待锁的额外开销使得程序变慢。但有些时候,我们可以通过更好的方法避免等待的开销,例如使用原子操作。

  • synchronized 创建的锁只会对于这一个代码块生效,不会影响到其他的代码块。


共用锁

void increase(shared int* i) {
foreach (t; 0 .. 200000) {
synchronized {
*i = *i + 1;
}
}
} void decrese(shared int* i) {
foreach (t; 0 .. 200000) {
synchronized {
*i = *i - 1;
}
}
} void main() {
shared int i = 0; foreach (id; 0 .. 10) {
if (id & 1) spawn(&increase, &i);
else spawn(&decrese, &i);
} thread_joinAll();
writeln("after i to ", i);
}

期望输出 0 但是实际输出……不知道。所以我们需要共用锁:

synchronized (lock_object) {
// ...
}

修改后的代码

class Lock {}
shared Lock lock = new Lock(); void increase(shared int* i) {
foreach (t; 0 .. 200000) {
synchronized (lock) {
*i = *i + 1;
}
}
} void decrese(shared int* i) {
foreach (t; 0 .. 200000) {
synchronized (lock) {
*i = *i - 1;
}
}
}

现在就可以得到正确的答案了。


同步类

我们可以使用 synchronized 修饰一个类。这相当于在每一个代码块里面嵌套一个 synchronzied

synchronized class Cls {
void func() {
// ...
}
}

上面的等价于:

class Cls {
void func() {
synchronized (this) {
// ...
}
}
}

同步初始化

我们考虑这份代码:

static this() {
writeln("executing static this()");
} void worker() {
}
void main() {
spawn(&worker);
thread_joinAll();
}

最终会输出两次 executing static this()

如果我们修改为 shared static this() { ... },那么最终只会输出一次。


原子操作

需要用到 core.atomic 库。

有代码:

atomic!"+="(var, x);
atomic!"-="(var, x);
// ... like *= /= ^= ...

这些都是原子操作。

有方法:

shared(int) *value;
bool is_mutated = cas(value, currentValue, newValue);

如果返回 true,那么值会改变,否则没有。

原子操作一般来说快于 synchronized

同时,原子操作也可以作用于结构体上,这里不作为讲解。

更多操作可以参考标准库:

  • core.sync.barrier

  • core.sync.condition

  • core.sync.config

  • core.sync.exception

  • core.sync.mutex

  • core.sync.rwmutex

  • core.sync.semaphore

Dlang 并行化的更多相关文章

  1. Hawk 4.6 并行化

    并行化 Hawk支持单机并行化,也就是使用多线程获取数据.它可以控制目前所有任务的数量,为了不给网站造成过大的压力,仅当任务池中的任务数量小于一定值后,才会插入新的任务. 你可以在数据清洗的 执行面板 ...

  2. Java 8函数编程轻松入门(五)并行化(parallel)

    1.并发与并行的区别 并发: 一个时间段内有几个程序都处于已启动到运行完毕之间,且这几个程序都是在同一个处理机上运行.但在任一个时刻点只有一个程序在处理机上运行 并行: 在同一个时刻,多核处理多个任务 ...

  3. kmeans算法并行化的mpi程序

    用c语言写了kmeans算法的串行程序,再用mpi来写并行版的,貌似参照着串行版来写并行版,效果不是很赏心悦目~ 并行化思路: 使用主从模式.由一个节点充当主节点负责数据的划分与分配,其他节点完成本地 ...

  4. go:多核并行化问题

    分别用串行和并行实现了一个NUM次加法的程序,代码如下: package main import ( "fmt" //"runtime" //执行并行段时需要引 ...

  5. [Java 8] (10) 使用Lambda完成函数组合,Map-Reduce以及并行化

    好文推荐!!!!! 原文见:http://blog.csdn.net/dm_vincent/article/details/40856569 Java 8中同时存在面向对象编程(OOP)和函数式编程( ...

  6. Parallel并行化编程

    在很多场景中我们需要通过并行化的方式来提高程序运行的速度,比较典型的需求就是并行下载.前期遇到一个需求是要批量下载瓦片,每次大概下载上百万个瓦片,要想提高瓦片的下载速度,只能通过并行化的方式,下面把我 ...

  7. Bash脚本实现批量作业并行化

    http://jerkwin.github.io/2013/12/14/Bash%E8%84%9A%E6%9C%AC%E5%AE%9E%E7%8E%B0%E6%89%B9%E9%87%8F%E4%BD ...

  8. Intel® Threading Building Blocks (Intel® TBB) Developer Guide 中文 Parallelizing Data Flow and Dependence Graphs并行化data flow和依赖图

    https://www.threadingbuildingblocks.org/docs/help/index.htm Parallelizing Data Flow and Dependency G ...

  9. 【转】Bash脚本实现批量作业并行化

    首先附上自己常用的代码 ---------------------------------------------------------------------------------------- ...

  10. 梯度下降之随机梯度下降 -minibatch 与并行化方法

    问题的引入: 考虑一个典型的有监督机器学习问题,给定m个训练样本S={x(i),y(i)},通过经验风险最小化来得到一组权值w,则现在对于整个训练集待优化目标函数为: 其中为单个训练样本(x(i),y ...

随机推荐

  1. 有一个公网IP地址

    这几天在家里拉了一条300M+的宽带,但是遇到了一些坑,本文就简单说明一下如下: 突发此次需求是这样的:阿里云有台服务器公网带宽是1M的,虽说带宽小,但是数据中心的服务器显然是稳定的,只是带宽太小,有 ...

  2. 【SpringMVC】(三)

    HTTPMessageConverter HttpMessageConverter报文信息转换器,将请求报文转换为java对象,或将java对象转换为响应报文. 1 @ResquestBody Res ...

  3. 香,一套逻辑轻松且智能解决PyQt中控件数值验证的问题

    在PyQt开发中,时常需要对控件的值进行校验,如需要校验QCheckBox是否被选中,QLabel是否校验值是否为空等等.在复杂的业务场景下,这类控件如果数量很多,逐个校验就显得麻烦,需要一一获得控件 ...

  4. Docker MariaDB配置主从复制

    编写主节点配置文件master.cnf: [client] # 默认字符集 default-character-set=utf8mb4 [mysqld] # 字符集 character-set-ser ...

  5. 微服务为什么要用到 API 网关?

    本文介绍了 API 网关日志的价值,并以知名网关 Apache APISIX 为例,展示如何集成 API 网关日志. 作者程小兰,API7.ai 技术工程师,Apache APISIX Contrib ...

  6. Uniswap V2 — 从代码解释 DeFi 协议

    Uniswap V2 - 从代码解释 DeFi 协议 为了理解我们在分析代码时将要经历的不同组件,首先了解哪些是主要概念以及它们的作用是很重要的.所以,和我一起裸露吧,因为这是值得的. 我在 5 个段 ...

  7. 玩一玩 Ubuntu 下的 VSCode 编程

    一:背景 1. 讲故事 今天是五一的最后一天,想着长期都在 Windows 平台上做开发,准备今天换到 Ubuntu 系统上体验下,主要是想学习下 AT&T 风格的汇编,这里 Visual S ...

  8. 加速 AI 训练,如何在云上实现灵活的弹性吞吐

    AI 已经成为各行各业软件研发的基础,带来了前所未有的效率和创新.今天,我们将分享苏锐在AWS量化投研行业活动的演讲实录,为大家介绍JuiceFS 在 AI 量化投研领域的应用经验,也希望为其他正在云 ...

  9. 【Docker】容器管理

    一.容器生命周期及启动过程 1.容器生命周期 2.容器启动过程 二.容器管理命令 Usage: docker [OPTIONS] COMMAND A self-sufficient runtime f ...

  10. 2023-01-12:一个n*n的二维数组中,只有0和1两种值, 当你决定在某个位置操作一次, 那么该位置的行和列整体都会变成1,不管之前是什么状态。 返回让所有值全变成1,最少的操作次数。 1 <

    2023-01-12:一个n*n的二维数组中,只有0和1两种值, 当你决定在某个位置操作一次, 那么该位置的行和列整体都会变成1,不管之前是什么状态. 返回让所有值全变成1,最少的操作次数. 1 &l ...