1 划分线程的典型原则
《The Art of Concurrency》第四章 Eight Simple Rules for Designing Multithread Applications.
(1)区分真正独立的计算任务;
(2)尽可能地在最高级别并发;
for..// 并发
for …
for …
(3)尽早考虑多核个数;
(4)尽可能利用已有的线程安全库;
许多的库,例如 Intel Math Kernel Library(Intel MKL)和 Intel Integrated Performance Primitives (Intel IPP),提供了能更好的利用多核处理器的并行版本的函数。
(5)使用合适的多线程模型;
有 pthread, OpenMP 等,另外一本书
https://maxim.int.ru/bookshelf/PthreadsProgram/htm/r_19.html
Chapter 2 - Designing Threaded Programs
讲解基于 pthread 实现的多线程模型,难得的是 1996 年提出的模型,现今依然适用;
(6)永远不要假设多线程的执行顺序;
(7)尽可能使用线程本地数据,或者对特定的数据加锁
数据越独立,更容易降低锁粒度;
(8)敢于更换更易并行化的算法
并发:逻辑上的同时发出;
并行:物理上,实际同时执行;
以上规则,适合 CPU 型任务分解;
补充:
(9)让 I / O 型任务与 CPU 计算型任务,同时运行而不是互相等待.
2 典型的多线程模型
2.1 老板 - 工人 (boss-worker) 模型, w/ o 线程池;
(1)一个 boss 线程负责分派工作(delegation),遇到具体任务,创建线程去干活;
main()
/* The boss */
{
forever {
get a request
switch request:
case X: pthread_create(...taskX)
case Y: pthread_create(...taskY)
}
}
taskX() /*Workers processing requests of type X*/
{
perform the task, synchronize as needed if accessing shared resources
done
}
taskY() /*Workers processing requests of type Y*/
{perform the task, synchronize as needed if accessing shared resources done}
(2)先创建好 n 个线程(对应 n 个 worker),遇到具体任务,放入队列,唤醒等待队列,准备好的线程干活;
main()
/* The boss */
{
for the numnber of workers
pthread_create(...poll_base)
forever {
get a request
place request in work queue;
signal sleeping threads that work is available
}
}
poll_base()
{while(1) {
sleep until awoken by boss
dequeuea work request
switch (case)
case request X: taskX()
case request Y: taskY()}
}
2.2 PEER 模型:
没有 Boss 分派工作的过程,每个线程都知道自己要做什么,有自己有输入(workcrew)
main()
{pthread_create(...thread1 ... task1)
pthread_create(...thread2 ... task2)
...
signal all workers to start
wait for all workers fo finish
do any clean up
}
taks1()
{
wait for start
perform task, synchronize as needed if accessing shared resources
done
}
taks2()
{
wait for start
perform task, synchronize as needed if accessing shared resources
done
}
2.3 线程池模型
避免频繁创建撤销,或者不确定数量的创建;线程的切换介于忙 / 闲之间,而非生死之间;
解偶作用:线程的创建与执行完全分开,方便维护;
放入一个池子中,可以给其他任务机型复用;
boss/worker 模型,常与线程池模型结合实现;
code:
multiThread.cpp
2.4 流水线模型:
流水线的每一个阶段应尽可能相等时间;
pthread_t Thread[N]
Queues[N]
//initial thread
{
place all input into stage1 is queue
pthread_create(&(Thread[1]...stage1...))
pthread_create(&(Thread[2]...stage1...))
pthread_create(&(Thread[3]...stage1...))
//...
}
void *stageX(void *X)
{
loop
suspend until input unit is in queue
loop while XQueue is not empty
dequeue intput unit
process input unit
enqueue input unit into next stage s queu
end loop
until done
return NULL;
}
pipeline, 类似 ARM 流水线;
比如放电影过程:
读文件,解码,渲染,显示
./vlc
cd /proc/pid/
ls task // 看到有多个线程
查看每个线程的系统调用
sudo cat /proc/pid/task/ppid/stack
3 Amdahl 定律
α: 串行化比例;
β 多核间通信消耗;
当 β = 0 时,就是 Amdahl 定律
4 多线程与 I /O
4.1 异步 IO
Aio_read 函数会立马返回,自动创建一个线程去干活;类似 boos-worker 模型的实现;
#include <aio.h>
int aio_read(struct aiocb *aiocbp);
int aio_write(struct aiocb *aiocbp);
4.2 多路复用
(1)select
因为存在一个唤醒文件查询过程,每调用一次,还需要重新设置观察文件集,当文件数量过大时,效率会地下;
while(1)死循环内添加 fd 到 fdset, 等待 select, 检查谁 read-FD_ISSET()
while(1){FD_ZERO(&rset);
for (int i=0; i <5; i++){FD_SET(fds[i],&rset);
}
puts("round again");
select(max+1, &rset, NULL, NULL, NULL);
for (int i=0; i < 5; i++) {if (FD_ISSET(fds[i],&rset)) {memset(buffer,0,MAXBUF);
read(fds[i], buffer, MAXBUF);
puts(buffer);
}
}
}
(2)epoll
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);
epoll 在循环体外添加 FD, 循环体内 epoll_wait, 返回值知道 ready 的文件 fd
for (int i=0; i <5; i++){
static struct epoll_event ev;
memset(&client,0,sizeof(client));
addrlen = sizeof(client)
ev.data.fd = accept(sockfd, (struct sockaddr*)&client, &addrlen);
en.events = EPOLLIN;
epoll_ctl(epfd, EPOLL_CTL_ADD, ev.data.fd, &ev);
}
while(1){puts("round again");
nfds = epoll_wait(epfd, events, 5, 10000);
for (int i=0; i < nfds; i++) {memset(buffer,0,MAXBUF);
read(events[i].data.fd,buffer,MAXBUF);
puts(buffer);
}
}
4.3 多线程文件冲突问题
文件是进程级的,一个线程访问文件,会导致另外一个线程访问文件的偏移位置等不可控;
C 库提供的 pread,pwrite 函数解决多线程 offset 等冲突问题;(实现方法,共享数据加锁恢复)