【Operating Systems:Three Easy Pieces 操作系统导论 】 第 26 章 并发:介绍 & 第27章线程 API

【Operating Systems:Three Easy Pieces 操作系统导论 】 并发

在这里插入图片描述

《Operating Systems: Three Easy Pieces》第26章、并发:介绍

1线程:一个程序只有一个执行点(一个程序计数器,用来存放要执行的指令),多线程程序会有多个执行点(多个程序计数器,每个都用于取指令和执行)。每个线程由一个程序计数器(记录哪里获取指令),每个线程有自己的一组用于计算寄存器。线程之间的切换类似于进程间的上下文切换,对于进程,将状态保存在PCB中,对于线程,保存在线程控制块(TCB),但是地址空间保持不变。在传统单线程中,只有一个栈,在多线程中,由多个栈
2线程与进程的关系
进程=火车,线程=车厢
• 线程在进程下行进(单纯的车厢无法运行)
• 一个进程可以包含多个线程(一辆火车可以有多个车厢)
• 不同进程间数据很难共享(一辆火车上的乘客很难换到另外一辆火车,比如站点换乘)
• 同一进程下不同线程间数据很易共享(A车厢换到B车厢很容易)
3并发术语
(1)临界区:访问共享变量的代码片段,一定不能让多个线程同时执行,只能保证一个线程运行
(2)竞争条件:出现多个线程同时进入临界区,都试图更新更新临界区的数据结构
(3)不确定性:程序由一个或多个竞争条件组成,输出因运行而异

4原子方式:会像期望那样执行更新,不能在指令中间中断,但是一般不会由一条指令可以做到这一点,所以必须用一些通用的集合,即同步原语,加上操作系统的帮助,以同步和受控的方式访问临界区(例如包含一些互斥的原语,保证只有一个线程进入临界区)

单个线程的状态与进程状态非常类似。线程有一个程序计数器(PC),记录程序从哪里获取指令。每个线程有自己的一组用于计算的寄存器。所以,如果有两个线程运行在一个处理器上,从运行一个线程(T1)切换到另一个线程(T2)时,必定发生上下文切换(context switch)。线程之间的上下文切换类似于进程间的上下文切换。对于进程,我们将状态保存到进程控制块(Process Control Block,PCB)。现在,我们需要一个或多个线程控制块(Thread Control Block,TCB),保存每个线程的状态。但是,与进程相比,线程之间的上下文切换有一点主要区别:地址空间保持不变(即不需要切换当前使用的页表)。

在多线程的进程中,每个线程独立运行,当然可以调用各种例程来完成正在执行的任何工作。不是地址空间中只有一个栈,而是每个线程都有一个栈。

F26.1

实例:线程创建

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 #include <stdio.h> #include <assert.h> #include <pthread.h> void *mythread(void *arg) {    printf("%s\n", (char *) arg);    return NULL; } int main(int argc, char *argv[]) {    pthread_t p1, p2;    int rc;    printf("main: begin\n");    rc = pthread_create(&p1, NULL, mythread, "A"); assert(rc == 0);    rc = pthread_create(&p2, NULL, mythread, "B"); assert(rc == 0);    // join waits for the threads to finish    rc = pthread_join(p1, NULL); assert(rc == 0);    rc = pthread_join(p2, NULL); assert(rc == 0);    printf("main: end\n");    return 0; } 

为什么更糟糕:共享数据

两个线程递增同一个数,每次运行最终结果都不一样

原因是共享数据未保证操作原子性

后面都是些概念性的东西,不做赘述

《Operating Systems: Three Easy Pieces》第27章 插叙:线程 API

pthread 库介绍

线程创建

#include <pthread.h>
int
pthread_create(
    pthread_t * thread,  
    const pthread_attr_t * attr,
    void * (*start_routine)(void*),
    void * arg
);
  • thread

    指向 pthread_t 结构类型的指针,我们将利用这个结构与该线程交互,因此需要将它传入 pthread_create(),以便将它初始化。相当于该线程的身份证

  • attr

    指定该线程可能具有的任何属性。包括设置栈大小,或关于该线程调度优先级的信息等

  • *start_routine

    一个函数指针(function pointer),指向要运行的函数

  • arg

    要运行的函数的参数

线程完成

通过pthread_join阻塞等待线程完成

pthread_create(&p, NULL, mythread, (void *) 100);
 
pthread_join(p, (void **) &m);  // 第一个是 pthread_t 类型,用于指定要等待的线程
// 第二个参数是一个指针,指向你希望得到的返回值。
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>

#include "common.h"
#include "common_threads.h"

void *mythread(void *arg) {
    
    
    printf("%s\n", (char *) arg);
    return NULL;
}

int main(int argc, char *argv[]) {
    
                        
    if (argc != 1) {
    
    
	fprintf(stderr, "usage: main\n");
	exit(1);
    }
    pthread_t p1, p2;
    printf("main: begin\n");
    Pthread_create(&p1, NULL, mythread, "A"); 
    Pthread_create(&p2, NULL, mythread, "B");
    // join waits for the threads to finish
    Pthread_join(p1, NULL);  // 等待进程p2 并且初始化为 NULL
    Pthread_join(p2, NULL); 
    printf("main: end\n");
    return 0;
}

int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);

//对于 POSIX 线程,有两种方法来初始化锁
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;  // 初始化 1
// 或是int rc = pthread_mutex_init(&lock, NULL); // 初始化 2  常用 !

pthread_mutex_lock(&lock);
x = x + 1; // or whatever your critical section is
pthread_mutex_unlock(&lock);

创建一个临界区

如果另一个线程确实持有该锁,那么尝试获取该锁的线程将不会从该调用返回(阻塞等待),直到获得该锁

int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_timedlock(pthread_mutex_t *mutex,
                            struct timespec *abs_timeout);

这两个调用用于获取锁(非阻塞获取锁)。如果锁已被占用,则 trylock 版本将失败。获取锁的 timedlock 定版本会在超时或获取锁后返回,以先发生者为准。通常应避免使用这两种版本

条件变量(Condition Variables)

不同于信号量(semaphore),信号量应该是条件变量+互斥锁的组合,见此文

当线程之间必须发生某种信号时,如果一个线程在等待另一个线程继续执行某些操作,条件变量就很有用。

int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
int pthread_cond_signal(pthread_cond_t *cond);

要使用条件变量,必须另外有一个与此条件相关的锁。在调用上述任何一个函数时,应该持有这个锁。
第一个函数pthread_cond_wait()使调用线程进入休眠状态,因此等待其他线程发出信号,通常当程序中的某些内容发生变化时,现在正在休眠的线程可能会关心它。典型的用法如下所示:

pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
pthread_mutex_lock(&lock);
while (ready == 0)
    pthread_cond_wait(&cond, &lock);
pthread_mutex_unlock(&lock);

唤醒线程的代码运行在另外某个线程中,调用pthread_cond_signal时也需要持有对应锁。像下面这样:

pthread_mutex_lock(&lock);
ready = 1;
pthread_cond_signal(&cond);
pthread_mutex_unlock(&lock);

pthread_cond_wait有第二个参数,因为它会隐式释放锁,以便在其线程休眠后唤醒线程可以获取锁,之后又会重新获得锁

本例通过 while 判断 ready 的值的变更,而不是通过条件变量唤醒判断 ready 已变更。将唤醒视为某种事物可能已经发生变化暗示,而不是绝对的事实,这样更安全

编译和运行

代码需要包括头文件 pthread.h 才能编译。链接时需要 pthread 库,增加 -pthread 标记。

prompt> gcc -o main main.c -Wall -pthread 

小结

补充:线程 API 指导

当你使用 POSIX 线程库(或者实际上,任何线程库)来构建多线程程序时,需要记住一些小而重 要的事情:

  • 保持简洁。最重要的一点,线程之间的锁和信号的代码应该尽可能简洁。复杂的线程交互容易产生缺陷。
  • 让线程交互减到最少。尽量减少线程之间的交互。每次交互都应该想清楚,并用验证过的、正确的方法来实现(很多方法会在后续章节中学习)。
  • 初始化锁和条件变量。未初始化的代码有时工作正常,有时失败,会产生奇怪的结果。
  • 检查返回值。当然,任何 C 和 UNIX 的程序,都应该检查返回值,这里也是一样。否则会导致古怪而难以理解的行为,让你尖叫,或者痛苦地揪自己的头发。
  • 注意传给线程的参数和返回值。具体来说,如果传递在栈上分配的变量的引用,可能就是在犯错误。
  • 每个线程都有自己的栈。类似于上一条,记住每一个线程都有自己的栈。因此,线程局部变量应该是线程私有的,其他线程不应该访问。线程之间共享数据,值要在堆(heap)或者其他全局可访问的位置。
  • 线程之间总是通过条件变量发送信号。切记不要用标记变量来同步。
  • 多查手册。尤其是 Linux 的 pthread 手册,有更多的细节、更丰富的内容。请仔细阅读!

猜你喜欢

转载自blog.csdn.net/weixin_49486457/article/details/130438460
今日推荐