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模型的实现;
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等冲突问题;(实现方法,共享数据加锁恢复)