首页 > 编程语言 >第四章:并发编程

第四章:并发编程

时间:2023-10-29 22:55:06浏览次数:27  
标签:编程 并发 死锁 线程 pthread 进程 执行 第四章 row

第四章:并发编程
4.1 并行计算导论
在过去,大多数计算机只有一个处理组件,称为处理器或中央处理器(CPU)。由于这个硬件限制,计算机程序通常是为串行计算编写的。为了解决问题,算法被设计为逐步解决问题的步骤,并以串行指令流的形式在计算机程序中实现。在只有一个CPU的情况下,一次只能执行一个指令和步骤。然而,基于分治原则(如二叉树搜索和快速排序)的算法经常具有高度的并行性,可以通过并行或并发执行来提高计算速度。并行计算是一种计算方案,利用多个执行并行算法的处理器来更快地解决问题。在过去,由于并行计算对计算资源的大量需求,普通程序员很少能进行并行计算。然而,随着多核处理器的出现,大多数操作系统(如Linux)都支持对称多处理(SMP)。即使对于普通程序员来说,现在也可以实现并行计算。显然,并行计算是计算的未来发展方向。

4.1.1 顺序算法与并行算法
顺序算法中的begin-end代码块可能包含多个步骤。所有步骤都是通过单个任务依次执行的,每次只执行一个步骤。当所有步骤执行完成时,算法结束。相反,对于并行算法的描述,它使用cobegin-coend代码块来指定并行算法的独立任务。在cobegin-coend块中,所有任务都是并行执行的,紧随cobegin-coend代码块的下一个步骤将只在所有这些任务完成之后执行。

4.1.2 并行性与并发性
通常,并行算法只识别可并行执行的任务,但不规定如何将任务映射到处理组件。在理想情况下,并行算法中的所有任务都应同时实时执行。然而,真正的并行执行只能在具有多个处理组件的系统中实现,如多处理器或多核系统。在单个CPU系统中,一次只能执行一个任务。在这种情况下,不同的任务只能并发执行,即在逻辑上并行执行。在单CPU系统中,并发性通过多任务处理来实现。

4.2 线程
4.2.1 线程的原理
一个操作系统(OS)包含许多并发进程。在进程模型中,进程是独立的执行单元。所有进程都在内核模式或用户模式下执行。在内核模式下,进程在唯一地址空间上执行,与其他进程是分开的。虽然每个进程都是一个独立的单元,但它只有一个执行路径。当某个进程必须等待某个事件发生时,比如I/O完成事件,它就会暂停,整个进程都停止执行。线程是在同一个地址空间中执行的进程的独立执行单元。创建一个进程就是在唯一地址空间中创建一个主线程。当一个进程开始时,它执行该进程的主线程。如果只有一个主线程,那么进程和线程实际上是没有区别的。然而,主线程可能会创建其他线程,每个线程又可以创建更多的线程,以此类推。进程的所有线程都在同一个地址空间中执行,但每个线程是独立的执行单元。在线程模型中,如果一个线程被挂起,其他线程可以继续执行。除了共享相同的地址空间之外,线程还共享进程的许多其他资源,如用户ID、打开的文件描述符和信号等。可以将其类比为进程是一个有房屋管理员(主线程)的房子,线程是住在进程房子里的人。房子里的每个人都可以独立执行自己的任务,但他们会共用一些公共设施,比如同一个邮箱、厨房和浴室等。过去,大多数计算机供应商都在自己的专有操作系统中支持线程。不同系统之间的实现有很大的差异。目前,几乎所有的操作系统都支持 Pthread,它是IEEE POSIX 1003.1c的线程标准(POSIX 1995)。

4.2.2 线程的优点
线程的创建和切换速度更快。
当在进程中创建线程时,操作系统不需要为新的线程分配内存和创建页表,因为线程与进程共享同一个地址空间。因此,创建线程比创建进程更快。
线程的响应速度更快。
一个进程只有一个执行路径。当一个进程被挂起时,整个进程都会停止执行。相反,当一个线程被挂起时,同一进程中的其他线程可以继续执行。
线程更适合并行计算。
并行计算的目标是利用多个执行路径更快地解决问题。基于分治原则(如二叉树搜索和快速排序)的算法经常表现出高度的并行性,可以通过并行或并发执行来提高计算速度。

4.2.3 线程的缺点
由于地址空间共享,线程需要明确同步来避免冲突。
许多库函数可能不是线程安全的,例如传统的strtok()函数将一个字符串分成一系列标记。通常,任何使用全局变量或依赖静态内存内容的函数都是线程不安全的。为了使库函数适应线程环境,需要进行大量的工作。
在单个CPU系统上,使用线程解决问题实际上比使用顺序程序更慢,这是由于运行时创建线程和切换上下文的系统开销造成的。

4.3 线程操作
线程的执行轨迹与进程类似。线程可以在内核模式或用户模式下执行。在用户模式下,线程在进程的相同地址空间中执行,但每个线程都有自己的执行堆栈。线程是独立的执行单元,可以根据操作系统内核的调度策略对内核进行系统调用,挂起和激活以继续执行等。

4.4 线程管理函数
4.4.1 创建线程
使用pthread_create()函数创建线程:
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void *), void *arg);
如果成功则返回0,如果失败则返回错误代码。pthread_create()函数的参数为:

thread是指向pthread_t类型变量的指针,用于存储线程的ID。
attr是指向线程属性的指针,可以为NULL表示使用默认属性。
start_routine是新线程执行的函数的入口地址。
arg是指向线程函数参数的指针。
线程属性参数attr最复杂,可以使用pthread_attr_init()函数进行初始化,并设置属性,最后通过pthread_attr_destroy()函数释放资源。通常情况下,可以使用NULL作为属性参数,以使用默认属性。

4.4.2 线程ID
线程ID是一个不透明的数据类型,取决于具体实现。因此,不应直接比较线程ID。如果需要,可以使用pthread_equal()函数进行比较:
int pthread_equal(pthread_t t1, pthread_t t2);

4.4.3 线程终止
线程函数执行结束后,线程即终止。或者,线程可以显式调用函数
void pthread_exit(void *value_ptr);
进行终止,其中value_ptr是线程的退出状态。通常,退出值为0表示正常终止,非0值表示异常终止。

4.4.4 线程连接
一个线程可以等待另一个线程终止,通过调用pthread_join()函数进行连接,并将线程的退出状态作为value_ptr返回。

4.5 线程示例程序
计算一个N×N整数矩阵中所有元素的和:

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#define N 4
int A[N][N], sum[N];
void *func(void *arg)
{
// 线程函数
int j, row;
pthread_t tid = pthread_self(); // 获取线程ID号
row = (int)arg;
printf(“Thread %d [%lu] computes sum of row %d\n”, row, tid, row);

// 计算A[row]的和并存入全局变量sum[row]
for (j = 0; j < N; j++)
    sum[row] += A[row][j];

printf("Thread %d [%lu] done: sum[%d] = %d\n", row, tid, row, sum[row]);

pthread_exit((void *)0); // 线程退出,0表示正常终止

}

int main(int argc, char *argv[])
{
pthread_t thread[N]; // 线程ID
int i, j, r, total = 0;
void *status;

printf("Main: initialize A matrix\n");

// 初始化A矩阵
for (i = 0; i < N; i++) {
    sum[i] = 0;
    for (j = 0; j < N; j++) {
        A[i][j] = i * N + j + 1;
        printf("%4d", A[i][j]);
    }
    printf("\n");
}

printf("Main: create %d threads\n", N);

// 创建并发执行的线程
for (i = 0; i < N; i++) {
    pthread_create(&thread[i], NULL, func, (void *)i);
}

printf("Main: try to join with threads\n");

// 等待线程终止并获取退出状态
for (i = 0; i < N; i++) {
    pthread_join(thread[i], &status);
    printf("Main: joined with %d [%lu]: status = %d\n", i, thread[i], (int)status);
}

printf("Main: compute and print total sum: ");

// 计算并打印所有行的和
for (i = 0; i < N; i++)
    total += sum[i];
printf("total = %d\n", total);

pthread_exit(NULL);

}

4.6 线程同步
由于线程在进程的同一地址空间中执行,它们共享所有全局变量和数据结构。当多个线程尝试修改同一个共享变量或数据结构时,如果修改的结果取决于线程的执行顺序,就称为竞态条件。在并发程序中,竞态条件是绝对不能发生的,否则结果可能不一致。除了连接操作之外,并发执行的线程通常需要相互协作。为了避免竞态条件并支持线程协作,需要进行线程同步。通常,同步是一种机制和规则,用于确保共享数据对象的完整性和并发实体的协调性。它可以应用于内核模式下的进程,也可以应用于用户模式下的线程。

4.6.1 互斥量
在Pthread中,锁被称为互斥量,意味着它们彼此互斥。

静态方式:
pthread_mutex_t m = PTHREAD_MUTEX_INITIALIZER;

动态方式:
pthread_mutex_t mutex;
pthread_mutex_init(&mutex, NULL);

4.6.2 避免死锁
死锁是一种状态,在该状态下,许多执行实体相互等待,因此无法继续进行。有多种方法可以解决潜在的死锁问题,包括死锁预防、死锁规避、死锁检测和死锁恢复等。在实际系统中,唯一可行的方法是死锁预防,即试图在设计并发算法时防止死锁的发生。

4.6.3 条件变量
条件变量提供线程协作的一种方法。在Pthread中,使用pthread_cond_t类型来声明条件变量,必须在使用之前进行初始化。条件变量可以通过静态或动态方式进行初始化。

4.6.4 生产者-消费者问题
一组生产者进程和一组消费者进程共享一个初始为空、大小为n的缓冲区。只有当缓冲区为空时,生产者才能将消息放入缓冲区;只有当缓冲区不为空时,消费者才能从中取出消息。由于缓冲区是一个临界资源,它只允许一个生产者放入消息或一个消费者取出消息。

4.6.5 信号量
信号量和条件变量之间的主要区别在于,信号量包含一个计数器,并且可以对计数器进行操作、测试计数器的值并做出决策等,所有这些操作都是临界区的原子操作或基本操作,而条件变量则需要特定的互斥量来执行临界区。在Pthreads中,互斥量严格用于互斥保护。条件变量可用于线程协作。相反,可以将使用初始值1初始化的信号量视为锁。带有其他初始值的信号量可以用于协作。因此,信号量比条件变量更通用、更灵活。

4.6.6 屏障
创建一个屏障对象:
pthread_barrier_t barrier;
调用pthread_barrier_init(&barrier, NULL, nthreads);
在线程中使用barrier对其进行初始化,然后主线程创建工作线程来执行任务。工作线程使用pthread_barrier_wait(&barrier)进行同步后继续执行。

 

标签:编程,并发,死锁,线程,pthread,进程,执行,第四章,row
From: https://www.cnblogs.com/20211205ZX/p/17796748.html

相关文章

  • 第四章学习笔记
    并发编程本章论述了并发编程,介绍了并行计算的概念,指出了并行计算的重要性,比较了顺序算法与并行算法,以及并行性与并发性,解释了线程的原理及相对于进程的优势。通过示例介绍了Pthread中的线程操作,句括线程管理函数。互斥量、连接、条件变量和屏障等线程同步工具;通过具体示例演......
  • 学习笔记7——并发编程与线程同步
    学习笔记7——并发编程与线程同步本文将深入探讨并发编程的概念,介绍了并行计算的重要性,比较了顺序算法与并行算法,解释了线程的原理和相对于进程的优势,并通过示例介绍了在Pthread中进行线程操作。我们还将讨论线程同步工具,如互斥量、信号量和屏障,以及如何避免并发程序中的死锁问题......
  • 一种编程思想——利用settings文件实现功能的增减
    一.正常函数版本的思路1.notify.pydefwechat(content):print('微信通知:%s'%content)defqq(content):print('qq通知:%s'%content)defemail(content):print('邮箱通知:%s'%content) 2.start.pyfromnotifyimport*defsend_all(c......
  • 2023-2024-1 20211211 第四章读书笔记
    第四章读书笔记一、知识点归纳(思维导图)二、收获总结并行线程的主要挑战有:线程同步、死锁、资源竞争、上下文切换开销等问题。线程级别的并行是指在多核处理器上同时执行多个线程,每个线程独立执行不同的任务。指令级别的并行是指在单个核心的处理器上同时执行多条指令。并发......
  • python两个乒乓球队进行比赛,各出三人。甲队为a,b,c三人,乙队为x,y,z三人。已抽签决定比
    两个乒乓球队进行比赛,各出三人。甲队为a,b,c三人,乙队为x,y,z三人。已抽签决定比赛名单。有人向队员打听比赛的名单。a说他不和x比,c说他不和x,z比,请编程序找出三队赛手的名单实例#!/usr/bin/python#-*-coding:UTF-8-*-foriinrange(ord('x'),ord('z')+1):forjinra......
  • EDA工具使用+GIT操作+python编程+C语言编程+Riscv相关+TCL操作
    EDA工具使用Verdi覆盖率转网页urg-full64-dirsimv.vdbVerdi加载sessionverdi-ssrsessionFileVcs分部编译额外选项-partcomp:自动分块编译。-fastpartcomp:使用多核计算系统并行部分编译。-pcmakeprof:查看每部分编译占用的时间,方便对时间更久的进行拆分。-partc......
  • C++多线程编程——线程的基本概念和使用方法
    什么是线程?在计算机科学中,线程是进程中的一个执行控制单元,也被称为执行路径。每个进程可以包含多个线程,每条线程并行执行不同的任务。线程是操作系统可识别的最小执行和调度单位。进程和线程的区别进程是程序在某个数据集合上的一次运行活动,也是操作系统进行资源分配和保护的......
  • Unix/Linux系统编程自学笔记-第四章:并发编程
    1、并行计算并行计算并行计算是一种计算方法,通过使用多个执行并行算法的处理器相较串行计算更快地解决问题。现代多核处理器的结构能很好的实现并行计算。计算机的发展未来也是并行计算。顺序算法与并行计算顺序算法一般代码块格式如下,顺序算法的每个代码块可能包含多......
  • Linux shell编程学习笔记16:bash中的关联数组
    上一节我们探讨了普通的数组,即使用数字下标来索引数组中不同的元素的数组,也可以称之为索引数组。相比纯粹的数字,字符串不仅能表明含义,也更便于记忆使用,于是就有了关联数组。一、关联数组概述bash从4.0开始支持关联数组,关联数组可以使用可以使用任意的字符串、或者整数作为下标来......
  • 【Elasticsearch】es脚本编程使用详解
    目录一、es脚本语言介绍1.1什么是es脚本1.2es脚本支持的语言1.3es脚本语言特点1.4es脚本使用场景二、环境准备2.1docker搭建es过程2.1.1拉取es镜像2.1.2启动容器2.1.3配置es参数2.1.4重启es容器并访问2.2docker搭建kibana过程2.2.1拉取kibana镜像2.2.2启动kibana容器2.......