20201317 第四章学习总结 LYX
第四章 linux并发编程
核心思路
本章论述了并发编程,介绍了并行计算的概念。指出了并行计算的重要性∶比较了顺序算法与并行算法,以及并行性与并发性;解释了线程的原理及其相对于进程的优势;介绍了Pthread中的线程操作,包括线程管理函数,互斥量、连接、条件变量和屏障等线程同步工具;解释了死锁问题,并说明了如何防止并发程序中的死锁问题;讨论了信号量,并论证了它们相对于条件变量的优点;还解释了支持Linux中线程的独特方式。
并发编程针对什么问题?
在早期,大多数计算机只有一个处理组件,成为处理器或中央处理器(CPU),受这种硬件条件的限制,计算机程序通常是为串行计算编写的。(计算机程序以串行指令流的形式实现该算法,在只有一个CPU的情况下,每次只能某算法的一个指令和步骤)
基于分治原则(如二叉树查找和快速排序)的算法经常表现出高度的并行性,可通过使用并行或者并发执行来提高计算速度。
顺序算法和并行算法
在描述顺序算法时,常用的方法是用一个begin-end代码块列出算法,如下方所示
--- 顺序算法 ---|--- 并行算法 ---
begin | cobegin
step_1 | task_1
step_2 | task_2
... | ...
step_n | task_n
end | coend
//next step | //next step
-----------------------------------------------------
begin-end代码块中的顺序算法可能包含多个步骤。所有步骤都是通过单个任务依次执行的,每次执行一个步骤。当所有步骤执行完成时,算法结束。相反,并行算法使用cobegin-coend代码块来指定并行算法的独立任务。在cobegin-coend块中,所有任务都是并行执行的。紧接着cobegin-coend代码块的下一个步骤将只在所有这些任务完成之后执行。
并行性与并发性
通常,并行算法只识别可并行执行的任务,但是它没有规定如何将任务映射到处理组件。在理想情况下,并行算法中的所有任务都应该同时实时执行。然而,真正的并行执行只能在有多个处理组件的系统中实现,比如多处理器或多核系统。在单 CPU 系统中—次只能执行一个任务。在这种情况下,不同的任务只能并发执行,即在逻辑上并行执行。在单CPU 系统中,并发性是通过多任务处理来实现的。
线程
- 线程的原理
一个操作系统(OS)包含许多并发进程。
在进程模型中,进程是独立的执行单元。
所有进程均在内核模式或用户模式下执行。在内核模式下,各进程在唯一地址空间上执行,与其他进程是分开的。虽然每个进程都是一个独立的单元,但是它只有一个执行路径。
当某进程必须等待某事件时,例如 I/O完成事件,它就会暂停,整个进程会停止执行。线程是某进程同一地址空间上的独立执行单元。创建某个进程就是在一个唯一地址空间创建一个主线程。当某进程开始时,就会执行该进程的主线程。如果只有一个主线程,那么进程和线程实际上并没有区别。但是,主线程可能会创建其他线程。每个线程又可以创建更多的线程等。某进程的所有线程都在该进程的相同地址空间中执行,但每个线程都是一个独立的执行单元。在线程模型中,如果一个线程被挂起,其他线程可以继续执行。除了共享共同的地址空间之外,线程还共享进程的许多其他资源,如用户id、打开的文件描述符和信号等。打个简单的比方,进程是一个有房屋管理员(主线程)的房子。线程是住在进程房子里的人。房子里的每个人都可以独立做自己的事情,但是他们会共用一些公用设施,比如同一个信箱、厨房和浴室等。过去,大多数计算机供应商都是在自己的专有操作系统中支持线程。不同系统之间的实现有极大的区别。
- 线程的优点
与进程相比,线程有许多优点。
(1)线程创建和切换速度更快∶进程的上下文复杂而庞大。其复杂性主要来自管理进程映像的需要。例如,在具有虚拟内存的系统中。进程映像可能由叫作页面的许多内存单元组成。在执行过程中,有些页面在内存中,有些则不在内存中。操作系统内核必须使用多个页表和多个级别的硬件辅助来跟踪每个进程的页面。要想创建新的进程,操作系统必须为进程分配内存并构建页表。若要在某个进程中创建线程,操作系统不必为新的线程分配内存和创建页表。因为线程与进程共用同一个地址空间。所以创建线程比创建进程更快。
另外,由于以下原因,线程切换比进程切换更快。进程切换涉及将一个进程的复杂分页环境替换为另一个进程的复杂分页环境,需要大量的操作和时间。相比之下。同一个进程中的线程切换要简单得多、也快得多,因为操作系统内核只需要切换执行点,而不需要更改进程映像。
(2)线程的响应速度更快:一个进程只有一个执行路径。当某个进程被挂起时、整个进程都将停止执行。相反,当某个线程被挂起时,同一进程中的其他线程可以继续执行。这使得有多个线程的程序响应速度更快。例如,在一个多线程的进程中.当一个线程被阻塞以等待I/O时,其他线程仍可在后台进行计算。在有线程的服务器中,服务器可同时服务多个客户机。
(3)线程更适合并行计算∶并行计算的目标是使用多个执行路径更快地解决问题。基于分治原则(如二叉树查找和快速排序等)的算法经常表现出高度的并行性。可通过使用并行或并发执行来提高计算速度。这种算法通常要求执行实体共享公用数据。在进程模型中,各进程不能有效共享数据,因为它们的地址空间都不一样。为了解决这个问题,进程必须使用进程间通信(IPC)来交换数据或使用其他方法将公用数据区包含到其地址空间中。相反. 同一进程中的所有线程共享同一地址空间中的所有(全局)数据。因此,使用线程编写并行执行的程序比使用进程编写更简单、更自然。 - 线程的缺点
(1)由于地址空间共享,线程需要来自用户的明确同步。
(2)许多库函数可能对线程不安全,例如传统 strtok()函数将一个字符串分成一连串令牌。通常,任何使用全局变量或依赖于静态内存内容的函数,线程都不安全。为了使库函数适应线程环境,还需要做大量的工作。
(3)在单CPU系统上,使用线程解决问题实际上要比使用顺序程序慢,这是由在运行时创建线程和切换上下文的系统开销造成的。
线程操作
线程的执行轨迹与进程类似。
线程可在内核模式或用户模式下执行。
在用户模式下,线程在进程的相同地址空间中执行,但每个线程都有自己的执行堆栈。线程是独立的执行单元,可根据操作系统内核的调度策略,对内核进行系统调用,变为挂起、激活以继续执行等。为了利用线程的共享地址空间,操作系统内核的调度策略可能会优先选择同一进程中的线程,而不是不同进程中的线程。
截至目前,几乎所有的操作系统都支持 POSIX Pthread,定义了一系列标准应用程序编程接口(API)来支持线程编程。
基本概念
- 什么是线程?
首先线程是包含在进程内。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务,线程之间资源是共享的。
- 多线程的优势?
如果只有一个线程,那么任务就是顺序执行的,你必须等待前面的任务完成,才能执行下一个任务。引入多线程则可以在你执行某个任务的过程中,执行其他任务。所以在耗时多任务中,应用非常广泛。
函数介绍
pthread_create
简述:创建线程。
第一个参数为指向线程标识符的指针。
第二个参数用来设置线程属性。
第三个参数是线程运行函数的起始地址。
最后一个参数是运行函数的参数。
int pthread_create(pthread_t *tidp,const pthread_attr_t *attr,
(void*)(*start_rtn)(void*),void *arg);
pthread_exit
简述:调用这个函数可以显示得退出线程
void pthread_exit(void *retval);
pthread_join
简述:用来等待一个线程的结束,使一个线程等待另一个线程结束,主要于线程间同步的操作。不使用的话,该线程结束后并不会释放其内存空间,这会导致该线程变成了“僵尸线程”。
thread: 线程标识符,即线程ID,标识唯一线程。
retval: 用户定义的指针
int pthread_join(pthread_t thread, void **retval);
pthread_detach
简述:主线程与子线程分离,子线程结束后,资源自动回收。pthread_join()函数的替代函数。如果tid尚未终止,pthread_detach()不会终止该线程。
int pthread_join(pthread_t thread, void **retval);
线程实践
创建进程
#include <stdio.h>
#include <pthread.h>
#include <time.h>
//#include <windows.h>//使用Sleep的头
int g_number = 0;
#define MAX_COUNT 100
//pthread_mutex_t mut;
void *counter3(void* args) {
int i = 1;
while (i <= MAX_COUNT / 4) {
//pthread_mutex_lock(&mut);
g_number++;
//pthread_mutex_unlock(&mut);
printf("hi,i am pthread 3, my g_number is [%d]\n", g_number);
//Sleep(1);// 单位ms
i++;
}
}
void *counter4(void* args) {
int j = 1;
while (j <= MAX_COUNT / 4) {
//pthread_mutex_lock(&mut);
g_number++;
//pthread_mutex_unlock(&mut);
printf("hi,i am pthread 4, my g_number is [%d]\n", g_number);
//Sleep(1);
j++;
}
}
int main() {
//pthread_mutex_init(&mut, NULL);
pthread_t t3;
pthread_t t4;
pthread_create(&t3, NULL, counter3, NULL);
pthread_create(&t4, NULL, counter4, NULL);
getchar();
return 0;
}
多线程计算矩阵的和
多线程快速排序
#include <pthread.h>
#include <stdio.h>
#include<stdlib.h>
#define SIZE 10
#define NUMBER_OF_THREADS 3
void* sorter(void* params); /* thread that performs basic sorting algorithm */
void* merger(void* params); /* thread that performs merging of results */
int list[SIZE] = { 7,12,19,3,18,4,2,6,15,8 };
int result[SIZE];
typedef struct
{
int from_index;
int to_index;
} parameters;
int main(int argc, const char* argv[])
{
parameters* data_1 = (parameters*)malloc(sizeof(parameters));
parameters* data_2 = (parameters*)malloc(sizeof(parameters));
pthread_t tid_sort1;
pthread_t tid_sort2;
pthread_t tid_merge;
data_1->from_index = 0; /*数组前一半数值*/
data_1->to_index = 4;
pthread_create(&tid_sort1, NULL, sorter, (void*)data_1);
data_2->from_index = 5; /*数组后一半*/
data_2->to_index = 9;
pthread_create(&tid_sort2, NULL, sorter, (void*)data_2);
pthread_join(tid_sort1, NULL);
pthread_join(tid_sort2, NULL);
pthread_create(&tid_merge, NULL, merger, NULL);
pthread_join(tid_merge, NULL);
printf("The sorted array is :\n");
int p = 0;
for (; p < 10; p++)
printf("%d ", result[p]);
return 0;
}
void* sorter(void* params)
{
parameters* index;
index = (parameters*)params;
int temp,len=(index->to_index)-(index->from_index)+1;
int i, j, k, gap; // gap 为步长
for (gap = len / 2; gap > 0; gap /= 2) { // 步长初始化为数组长度的一半,每次遍历后步长减半,
for (i = index->from_index;( i-index->from_index )< gap; ++i) { // 变量 i 为每次分组的第一个元素下标
for (j = i + gap; j <=index->to_index; j += gap) { //对步长为gap的元素进行直插排序,当gap为1时,就是直插排序
temp = list[j]; // 备份a[j]的值
k = j - gap; // j初始化为i的前一个元素(与i相差gap长度)
while (k >= index->from_index && list[k] > temp) {
list[k + gap] = list[k]; // 将在a[i]前且比tmp的值大的元素向后移动一位
k -= gap;
}
list[k + gap] = temp;
}
}
}
i=index->from_index;
printf("The shellsorted array is:");
for(;i<index->to_index;i++)
printf("%d ",list[i]);
printf("\n");
}
void* merger(void* params) {
int start = 0, end = 9, mid = 4;
int i = start;
int j = mid + 1;
int temp[end + 1];
int k = 0;
while (i <= mid && j <= end) {
if (list[i] < list[j]) {
temp[k++] = list[i++];
}
else {
temp[k++] = list[j++];
}
}
if (i == mid + 1) {
while (j <= end)
temp[k++] = list[j++];
}
if (j == end + 1) {
while (i <= mid)
temp[k++] = list[i++];
}
for (j = 0, i = start; j < k; i++, j++) {
result[i] = temp[j];
}
pthread_exit(0);
}
问题与解决方案
1.如何正确理解线程和进程的关系,怎样认识这两个关键词?
解决方案:通过查找网络资料,利用形象的比喻,进行理解。
简单的比喻:进程=火车,线程=车厢
- 线程在进程下行进(单纯的车厢无法运行)
- 一个进程可以包含多个线程(一辆火车可以有多个车厢)
- 不同进程间数据很难共享(一辆火车上的乘客很难换到另外一辆火车,比如站点换乘)
- 同一进程下不同线程间数据很易共享(A车厢换到B车厢很容易)
- 进程要比线程消耗更多的计算机资源(采用多列火车相比多个车厢更耗资源)
- 进程间不会相互影响,一个线程挂掉将导致整个进程挂掉(一列火车不会影响到另外一列火车,但是如果一列火车上中间的一节车厢着火了,将影响到所有车厢)
- 进程可以拓展到多机,进程最多适合多核(不同火车可以开在多个轨道上,同一火车的车厢不能在行进的不同的轨道上)
- 进程使用的内存地址可以上锁,即一个线程使用某些共享内存时,其他线程必须等它结束,才能使用这一块内存。(比如火车上的洗手间)-"互斥锁"
- 进程使用的内存地址可以限定使用量(比如火车上的餐厅,最多只允许多少人进入,如果满了需要在门口等,等有人出来了才能进去)-“信号量
将线程更加深刻的理解。