Linux系统编程学习笔记--第七章

7 线程

本节对应APUE第十一、十二章内容  

7.1 线程概念

线程本质:一个正在运行的函数。

 进程本质:加载到内存的程序。  

进程是操作系统分配资源的单位,线程是调度的基本单位,线程之间共享进程资源。  

典型的UNIX进程可以看成只有一个控制线程:一个进程在某一时刻只能做一件事情。有了多个控制线程以后,在程序设计时就可以把进程设计成在某一时刻能够做不止一件事,每个线程处理各自独立的任务。  

每个线程都包含有表示执行环境所必需的信息,其中包括进程中标识线程的线程ID、一组寄存器值、栈、调度优先级和策略、信号屏蔽字、errno变量以及线程私有数据。  

一个进程的所有信息对该进程的所有线程都是共享的,包括可执行程序的代码、程序的全局内存和堆内存、栈以及文件描述符。  

7.1.1 POSIX线程接口

POSIX线程(英语:POSIX Threads,常被缩写为Pthreads)是POSIX的线程标准,定义了创建和操纵线程的一套API。  

Pthreads定义了一套C语言的类型、函数与常量,它以pthread.h头文件和一个线程库实现。Pthreads API中大致共有100个函数调用,全都以pthread_开头,并可以分为四类:  

线程管理,例如创建线程,等待(join)线程,查询线程状态等。

互斥锁(Mutex):创建、摧毁、锁定、解锁、设置属性等操作

条件变量(Condition Variable):创建、摧毁、等待、通知、设置与查询属性等操作

使用了互斥锁的线程间的同步管理

因此在编译时需要makefile的编译和链接选项:

 CFLAGS+=-pthread # 编译选项 
 LDFLAGS+=-pthread # 链接选项
cc -pthread  -pthread  create.c   -o create

7.1.2 进程和线程

① 进程控制块

进程控制块是用于保存一个进程信息的结构体,称之为PCB(process control block),相当于进程的身份证;PCB是进程存在的唯一标识。  

在Linux中PCB的信息存放在task_struct结构体中。结构体的主要成员如下:  

进程状态

进程执行时,它会根据具体情况改变状态。进程状态是调度和对换的依据。Linux中的进程主要有如下状态:  

字段名含义
TASK_RUNNING可运行
TASK_INTERRUPTIBLE可中断的等待状态
TASK_UNINTERRUPTIBLE不可中断的等待状态
TASK_ZOMBIE僵死
TASK_STOPPED暂停
TASK_SWAPPING换入/换出

进程调度信息

调度程序利用这部分信息决定系统中哪个进程最应该运行,并结合进程的状态信息保证系统运转的公平和高效。这一部分信息通常包括进程的类别(普通进程还是实时进程)、进程的优先级等等。

字段名含义
need_resched调度标志
Nice静态优先级
Counter动态优先级
Policy调度策略
rt_priority实时优先级

  标识符

每个进程有进程标识符、用户标识符、组标识符。  

域名含义
Pid进程标识符
Uid、gid用户标识符、组标识符
Euid、egid有效用户标识符、有效组标识符
Suid、sgid备份用户标识符、备份组标识符
Fsuid、fsgid文件系统用户标识符、文件系统组标识符

进程通信有关信息

为了使进程能在同一项任务上协调工作,进程之间必须能进行通信即交流数据。  Linux支持多种不同形式的通信机制。它支持典型的Unix通信机制(IPC Mechanisms):信号(Signals)、管道(Pipes),也支持System V通信机制:共享内存(Shared Memory)、信号量和消息队列(Message Queues)  

进程链接信息

程序创建的进程具有父/子关系。因为一个进程能创建几个子进程,而子进程之间有兄弟关系,在task_struct结构中有几个成员来表示这种关系。  

时间和定时器信息

一个进程从创建到终止叫做该进程的生存期(lifetime)。进程在其生存期内使用CPU的时间,内核都要进行记录,以便进行统计、计费等有关操作。  

进程耗费CPU的时间由两部分组成:一是在用户模式(或称为用户态)下耗费的时间,一是在系统模式(或称为系统态)下耗费的时间。每个时钟滴答,也就是每个时钟中断,内核都要更新当前进程耗费CPU的时间信息,例如将进程的时间片减一。  

域名含义
Start_time进程创建时间
Per_cpu_utime进程在某个CPU上运行时在用户态下耗费的时间
Per_cpu_stime进程在某个CPU上运行时在系统态下耗费的时间
Counter进程剩余的时间片

进程中的定时器如下图所示:    其余略

 ② 线程共享和私有内容

私有资源:所属线程的栈区、程序计数器、栈指针以及函数运行使用的寄存器是线程私有的。这些资源又称为线程上下文。  除此之外的所有资源均由线程共享。  

7.2 线程标识

就像每个进程有一个进程ID一样,每个线程也有一个线程ID。进程ID在整个系统中是唯一的,但线程ID不同,线程ID只有在它所属的进程上下文中才有意义。  

进程ID是用pid_t数据类型来表示的,是一个非负整数。线程ID是用pthread_t 数据类型来表示的,实现的时候可以用一个结构来代表pthread_t数据类型,所以可移植的操作系统实现不能把它作为整数处理。

 因此需要一个函数来对两个线程ID进行比较:

#include <pthread.h> 

 // 相等返回非零,不等返回0 
int pthread_equal(pthread_t t1, pthread_t t2);

获取自身的线程id:  

#include <pthread.h>  pthread_t pthread_self(void); 

7.3 线程创建

在传统 UNIX进程模型中,每个进程只有一个控制线程。从概念上讲,这与基于线程的模型中每个进程只包含一个线程是相同的。在POSIX线程(pthread)的情况下,程序开始运行时,它也是以单进程中的单个控制线程启动的。在创建多个控制线程以前,程序的行为与传统的进程并没有什么区别。  

新增的线程可以通过调用pthread_create函数创建。

 #include <pthread.h>
 // 成功返回0,失败返回errno 
 int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) 
 (void *), void *arg); 

thread:事先创建好的pthread_t类型的参数。成功时thread指向的内存单元被设置为新创建线程的线程ID。

attr:用于定制各种不同的线程属性。APUE的12.3节讨论了线程属性。通常直接设为NULL。

start_routine:新创建线程从此函数开始运行,无参数时arg设为NULL即可。形参是函数指针(该函数返回值和形参均为void*),因此需要传入函数地址。

arg:start_rtn函数的参数。无参数时设为NULL即可。有参数时输入参数的地址。当多于一个参数时应当使用结构体传入。

代码示例

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>

// 线程执行的函数
static void* func(void* p) {
    puts("Thread is working!");
    return NULL;
}

int main(void) {
    pthread_t tid;
    int err;

    puts("Begin!");

    err = pthread_create(&tid, NULL, func, NULL);
    if(err) {
        fprintf(stderr, "pthread_create():%s\n", strerror(err));
        exit(1);
    }

    puts("End!");
    exit(0);
}

打印结果:

[root@zoukangcheng pthread]# ./create 
Begin!
End!

7.4 线程终止

7.4.1 终止方式

如果进程中的任意线程调用了 exit、_Exit 或者 _exit,那么整个进程就会终止。与此相类似,如果默认的动作是终止进程,那么,发送到线程的信号就会终止整个进程。

单个线程可以通过3种方式退出,因此可以在不终止整个进程的情况下,停止它的控制流:

线程可以简单地从启动例程中返回,返回值是线程的退出码
线程可以被同一进程中的其他线程取消
线程调用pthread_exit

#include <pthread.h>

void pthread_exit(void *rval_ptr);

例如:

static void *func(void *p) {
    puts("Thread is working!");
    pthread_exit(NULL);
    // return NULL;
}

函数pthread_join用来等待一个线程的结束。相当于进程控制中的wait。

#include <pthread.h>

int pthread_join(pthread_t thread, void **retval);

thread:为被等待的线程标识符
retval:为用户定义的指针,它可以用来存储被等待线程的返回值,即pthread_exit的参数。这是一个二级指针,因此传入的参数为一级指针的地址,如果不关心返回值则用NULL

代码示例1

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>

static void *func(void *p) {
    puts("Thread is working!");
    return NULL;
}


int main(void) {

    pthread_t tid;
    int err;

    puts("Begin!");

    err = pthread_create(&tid, NULL, func, NULL);
    if(err) {
        fprintf(stderr, "pthread_create():%s\n", strerror(err));
        exit(1);
    }
    // join
    pthread_join(tid, NULL);
    puts("End!");
    exit(0);
}

执行结果:

[root@zoukangcheng pthread]# ./create 
Begin!
Thread is working!
End!

 代码示例2

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>

static void *thr_fn1(void *arg) {
    printf("thread1 is working!\n");
    pthread_exit((void *)1);
}

static void *thr_fn2(void *arg) {
    printf("thread2 is working!\n");
    pthread_exit((void *)2);
}


int main(void) {

    printf("main thread is working!\n");

    int err;
    pthread_t tid1, tid2;
    void *tret;

    err = pthread_create(&tid1, NULL, thr_fn1, NULL);
    if(err) {
        fprintf(stderr, "pthread_create():%s\n", strerror(err));
        exit(1);
    }

    err = pthread_create(&tid2, NULL, thr_fn2, NULL);
    if(err) {
        fprintf(stderr, "pthread_create():%s\n", strerror(err));
        exit(1);
    }

    pthread_join(tid1, &tret);
    printf("thread1 exit code %ld\n", (long)tret);

    pthread_join(tid2, &tret);
    printf("thread2 exit code %ld\n", (long)tret);

    printf("main thread exit!\n");

    exit(0);
}

执行结果:

[root@zoukangcheng pthread]# ./test
main thread is working!
thread1 is working!
thread1 exit code 1
thread2 is working!
thread2 exit code 2
main thread exit!

补充:void*

void*是一个未指定跳跃力的指针。

void*可以指向任何类型的地址。

float f = 5.5;
float* pf = &f; // pf是指向float的指针
void* pv = pf; // pv可以指向float类型的地址
float* pf2 = pv; //编译错误,有类型的指针变量不能指向void*变量

void*指针只有强制类型转换以后才可以解引用

int main() {
    
    float f = 5.5;
    float* pf = &f;
    void* pv;
    
    pv = pf; //这句是可以的
    
    cout<<*pv<<endl;  //编译错误,这样直接对pv取值是错误的
    cout<<*(float*)pv<<endl;  //强制类型转换后可以取值
    
    return 0;
}

void*指针变量和普通指针一样可以通过等于0或者NULL来初始化,表示一个空指针

void* pv = 0; 
void* pv2 = NULL; // 指针保存的地址为空或0

当void*指针作为函数的输入和输出时,表示可以接受任意类型的输入指针和输出任意类型的指针

#include <stdio.h>
#include <stdlib.h>

void* test(void* a) {
    printf("test: %d\n", *(int *)a);
    return a;
}

int main() {
    int a = 5;
    int* pi = &a;
    
    printf("%p\n", pi);
    printf("%p\n", test(pi));
    printf("main: %d\n", *(int *)test(pi));

    exit(0);
}

执行结果:

0x7ffcd39fa0d4
test: 5
0x7ffcd39fa0d4
test: 5
main: 5

7.4.2 栈的清理

线程可以安排它退出时需要调用的函数,这与进程在退出时可以用atexit函数安排退出是类似的。这样的函数称为线程清理处理程序(thread cleanup handler)。一个线程可以建立多个清理处理程序。处理程序记录在栈中,也就是说,它们的执行顺序与它们注册时相反。

#include <pthread.h>

void pthread_cleanup_push(void (*routine)(void *), void *arg);
void pthread_cleanup_pop(int execute);

当线程执行以下动作时,清理函数routine是由pthread_cleanup_push函数调度的,调用时只有一个参数arg:

调用pthread_exit时;
响应取消请求时;
用非零execute参数调用pthread_cleanup_pop 时。如果 execute 参数设置为0,清理函数将不被调用。不管发生上述哪种情况,pthread_cleanup_pop都将删除上次 pthread_cleanup_push调用建立的清理处理程序。

注意:这些函数有一个限制,由于它们可以实现为宏,所以必须在与线程相同的作用域中以匹配对的形式使用。pthread_cleanup_push 的宏定义可以包含字符{,这种情况下,在 pthread cleanup_pop 的定义中要有对应的匹配字符}。

代码示例

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>

static void cleanup_func(void *p) {
    puts(p);
}

static void *func(void *p) {
    puts("Thread is working!");
    pthread_cleanup_push(cleanup_func, "cleanup:1");
    pthread_cleanup_push(cleanup_func, "cleanup:2");
    pthread_cleanup_push(cleanup_func, "cleanup:3");
    puts("push over!");
    // 成对出现
    pthread_cleanup_pop(1);
    pthread_cleanup_pop(0);
    pthread_cleanup_pop(0);
    pthread_exit(NULL);
}

int main(void) {
    pthread_t tid;
    int err;

    puts("Begin!");

    err = pthread_create(&tid, NULL, func, NULL);
    if(err) {
        fprintf(stderr, "pthread_create(): %s\n", strerror(err));
        exit(1);
    }
    pthread_join(tid, NULL);
    puts("End!");
    exit(0);
}

执行结果:

Begin!
Thread is working!
push over!
cleanup:3
End!

7.4.3 线程的取消

多线程程序中,一个线程可以借助 pthread_cancel() 函数向另一个线程发送“终止执行”的信号,从而令目标线程结束执行。

pthread_cancel调用并不等待线程终止,它只提出请求。线程在取消请求发出后会继续运行,直到到达某个取消点(CancellationPoint)。取消点是线程检查是否被取消并按照请求进行动作的一个位置。

与线程取消相关的函数有:

// 发送终止信号给thread线程,如果成功则返回0,否则为非0值。发送成功并不意味着thread会终止。
int pthread_cancel(pthread_t thread);

// 设置本线程取消动作的执行时机
int pthread_setcanceltype(int type, int *oldtype)  

// 在不包含取消点,但是又需要取消点的地方创建一个取消点,以便在一个没有包含取消点的执行代码线程中响应取消请求
void pthread_testcancel(void)

代码示例

#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>

//线程执行的函数
void * thread_Fun(void * arg) {
    printf("新建线程开始执行\n");
    sleep(10);
}

int main(void) {
    pthread_t myThread;
    void * mess;
    int value;
    int res;
    //创建 myThread 线程
    res = pthread_create(&myThread, NULL, thread_Fun, NULL);
    if (res != 0) {
        printf("线程创建失败\n");
        return 0;
    }
    sleep(1);
    //向 myThread 线程发送 Cancel 信号
    res = pthread_cancel(myThread);
    if (res != 0) {
        printf("终止 myThread 线程失败\n");
        return 0;
    }
    //获取已终止线程的返回值
    res = pthread_join(myThread, &mess);
    if (res != 0) {
        printf("等待线程失败\n");
        return 0;
    }
    //如果线程被强制终止,其返回值为 PTHREAD_CANCELED
    if (mess == PTHREAD_CANCELED) {
        printf("myThread 线程被强制终止\n");
    }
    else {
        printf("error\n");
    }
    return 0;
}

7.5 线程同步

7.5.1 概念和例子

线程竞争的实例

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>

#define LEFT  30000000
#define RIGHT 30000200
#define THRNUM (RIGHT-LEFT+1) // 线程个数

static void *thr_prime(void *p) {
    int i, j, mark;
    i = *(int *)p;
    mark = 1;
    for(j = 2; j < i/2; j++) {
        if(i % j == 0) {
            mark = 0;
            break;
        }
    }
    if(mark)
        printf("%d is a primer.\n", i);
	pthread_exit(NULL);
}

int main(void) {
	int i, err;
    pthread_t tid[THRNUM];
    for(i = LEFT; i <= RIGHT; i++) {
        err = pthread_create(tid+(i-LEFT), NULL, thr_prime, &i);
        if(err) {
            fprintf(stderr, "pthread_create(): %s\n", strerror(err));
            exit(1);
        }
    }

    for (i = LEFT; i <= RIGHT; i++)
        pthread_join(tid[i-LEFT], NULL);

    exit(0);
}

执行结果:发现每次都不一样,并且每次的结果都是相同的。

原因:线程发生了竞争。

创建线程时,main线程传递给函数thr_prime的参数&i是同一个地址,但是地址保存的值不相同:

err = pthread_create(tid+(i-LEFT), NULL, thr_prime, &i);

后面线程执行时,会对这个地址进行解引用:

i = *(int *)p;

注意:main线程和创建的线程的调度是由调度算法决定,因此会出现,在线程解引用之前,main线程就将该地址上的i值进行了修改,所以后面线程得到的i值都是同一个值。

解决竞争

 定义一个结构体,成员为要计算判断的数,然后每次动态分配内存,将地址作为线程函数的参数即可。

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>

#define LEFT  30000000
#define RIGHT 30000200
#define THRNUM (RIGHT-LEFT+1)

struct thr_arg_st {
    int n;
};

static void *thr_prime(void *p) {
    int i, j, mark;
    // 先将void*强转为struct thr_arg_st *
    i = ((struct thr_arg_st *)p) -> n;
    mark = 1;
    for(j = 2; j < i/2; j++) {
        if(i % j == 0) {
            mark = 0;
            break;
        }
    }
    if(mark)
        printf("%d is a primer.\n", i);
    // 将p作为返回值
	pthread_exit(p);
}

int main(void) {
	int i, err;
    pthread_t tid[THRNUM];
    struct thr_arg_st *p;
    void * ret;
    for(i = LEFT; i <= RIGHT; i++) {
        // 动态分配内存
        p = malloc(sizeof(*p));
        if(p == NULL) {
            perror("malloc()");
            exit(1);
        }
        p -> n = i; 
        err = pthread_create(tid+(i-LEFT), NULL, thr_prime, p);
        if(err) {
            fprintf(stderr, "pthread_create(): %s\n", strerror(err));
            exit(1);
        }
    }

    for (i = LEFT; i <= RIGHT; i++) {
        // 用ret来接收(一级指针的地址)
        pthread_join(tid[i-LEFT], &ret);
        // 释放动态分配的内存
        free(ret);
    }
    exit(0);
}

当多个控制线程共享相同的内存时,需要确保每个线程看到一致的数据视图。如果每个线程使用的变量都是其他线程不会读取和修改的,那么就不存在一致性问题。同样,如果变量是只读的,多个线程同时读取该变量也不会有一致性问题。但是,当一个线程可以修改的变量,其他线程也可以读取或者修改的时候,我们就需要对这些线程进行同步,确保它们在访问变量的存储内容时不会访问到无效的值。

当一个线程修改变量时,其他线程在读取这个变量时可能会看到一个不一致的值。在变量修改时间多于一个存储器访问周期的处理器结构中,当存储器读与存储器写这两个周期交叉时,这种不一致就会出现。当然,这种行为是与处理器体系结构相关的,但是可移植的程序并不能对使用何种处理器体系结构做出任何假设。

图11-7描述了两个线程读写相同变量的假设例子。在这个例子中,线程A读取变量然后给这个变量赋予一个新的数值,但写操作需要两个存储器周期。当线程B在这两个存储器写周期中间读取这个变量时,它就会得到不一致的值。

                                        

为了解决这个问题,线程不得不使用锁,同一时间只允许一个线程访问该变量。图11-8描述了这种同步。如果线程B希望读取变量,它首先要获取锁。同样,当线程A更新变量时,也需要获取同样的这把锁。这样,线程B在线程A释放锁以前就不能读取变量。

        

                                         

7.5.2 互斥量

可以使用 pthread 的互斥接口来保护数据,确保同一时间只有一个线程访问数据。互斥量(mutex)从本质上说是一把锁,在访问共享资源前对互斥量进行设置(加锁),在访问完成后释放(解锁)互斥量。

对互斥量进行加锁以后,任何其他试图再次对互斥量加锁的线程都会被阻塞直到当前线程释放该互斥锁。如果释放互斥量时有一个以上的线程阻塞,那么所有该锁上的阻塞线程都会变成可运行状态,第一个变为运行的线程就可以对互斥量加锁,其他线程就会看到互斥量依然是锁着的,只能回去再次等待它重新变为可用。在这种方式下,每次只有一个线程可以向前执行。

只有将所有线程都设计成遵守相同数据访问规则的,互斥机制才能正常工作。操作系统并不会为我们做数据访问的串行化。如果允许其中的某个线程在没有得到锁的情况下也可以访问共享资源,那么即使其他的线程在使用共享资源前都申请锁,也还是会出现数据不一致的问题。

互斥变量是用 pthread_mutex_t 数据类型表示的。在使用互斥变量以前,必须首先对它进行初始化,可以把它设置为常量PTHREAD_MUTEX_INITIALIZER(只适用于静态分配的互斥量),也可以通过调用pthread_mutex_init函数进行初始化。如果动态分配互斥量(例如,通过调用malloc函数),在释放内存前需要调用pthread_mutex_destroy。

相关函数:

初始化和销毁:

#include <pthread.h>
// 销毁互斥量
int pthread_mutex_destroy(pthread_mutex_t *mutex);

int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);

// 静态分配互斥量
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

加锁和解锁

#include <pthread.h>

// 阻塞加锁
int pthread_mutex_lock(pthread_mutex_t *mutex);
// 非阻塞加锁
int pthread_mutex_trylock(pthread_mutex_t *mutex);
// 解锁
int pthread_mutex_unlock(pthread_mutex_t *mutex);

代码示例——20个线程读写一个文件

 先向/tmp/out下入1,然后创建20个线程来读这个文件内容并加1,然后写入。期望内容为21

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>

#define FNAME "/tmp/out"
#define THRNUM 20
#define LINESIZE 1024

static void *thr_add(void *p) {
    FILE *fp;
    char linebuf[LINESIZE];

    fp = fopen(FNAME, "r+");
    if(fp == NULL) {
        perror("fopen()");
        exit(1);
    }

    fgets(linebuf, LINESIZE, fp);
    // 读完后将文件指针指向文件起始处
    fseek(fp, 0, SEEK_SET);
    sleep(1);
    // 向文件写入内容
    fprintf(fp, "%d\n", atoi(linebuf) + 1);
    fclose(fp);

    pthread_exit(NULL);
}

int main(void) {
    pthread_t tid[THRNUM];
    int i, err;

    for(i = 0; i < THRNUM; i++) {
        err = pthread_create(tid + i, NULL, thr_add, NULL);
        if(err) {
            fprintf(stderr, "pthread_create(): %s\n", strerror(err));
            exit(1);
        }
    }

    for(i = 0; i < THRNUM; i++)
        pthread_join(tid[i], NULL);

    exit(0);
}

执行结果:

[root@zoukangcheng posix]# echo 1 > /tmp/out
[root@zoukangcheng posix]# cat /tmp/out
1
[root@zoukangcheng posix]# ./add
[root@zoukangcheng posix]# cat /tmp/out
2

分析:由于调度和竞争,线程读到文件内容1后,休眠1s,然后一起写入2,所以结果就为2;

 代码示例——互斥量的使用

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>

#define FNAME "/tmp/out"
#define THRNUM 20
#define LINESIZE 1024

// 互斥锁
static pthread_mutex_t mut = PTHREAD_MUTEX_INITIALIZER;

static void *thr_add(void *p) {
    FILE *fp;
    char linebuf[LINESIZE];

    fp = fopen(FNAME, "r+");
    if(fp == NULL) {
        perror("fopen()");
        exit(1);
    }
    
    // 在进入临界区前加上互斥锁
    pthread_mutex_lock(&mut);
    fgets(linebuf, LINESIZE, fp);
    // 读完后将文件指针指向文件起始处
    fseek(fp, 0, SEEK_SET);
    sleep(1);
    // 向文件写入内容
    fprintf(fp, "%d\n", atoi(linebuf) + 1);
    fclose(fp);
    // 退出临界区后解锁
    pthread_mutex_unlock(&mut);

    pthread_exit(NULL);
}

int main(void) {
    pthread_t tid[THRNUM];
    int i, err;

    for(i = 0; i < THRNUM; i++) {
        err = pthread_create(tid + i, NULL, thr_add, NULL);
        if(err) {
            fprintf(stderr, "pthread_create(): %s\n", strerror(err));
            exit(1);
        }
    }

    for(i = 0; i < THRNUM; i++)
        pthread_join(tid[i], NULL);

    // 销毁互斥锁
    pthread_mutex_destroy(&mut);

    exit(0);
}

 执行结果:

[root@zoukangcheng posix]# echo 1 > /tmp/out
[root@zoukangcheng posix]# cat /tmp/out
1
[root@zoukangcheng posix]# ./add
[root@zoukangcheng posix]# cat /tmp/out
21

代码示例3——使用互斥锁实现线程同步

 需求:四个线程依次打印abcd

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>

#define THRNUM 4

static pthread_mutex_t mut[THRNUM];

// 返回下一个线程的编号0~3,3的下一个为0
static int next(int n) {
    return (n + 1) == THRNUM ? 0 : n + 1;
}

static void *thr_func(void *p) {
    int n = (int)p; // 线程编号
    int c = 'a' + (int)p; // 线程打印的字符

    while(1) {
        // 加锁
        pthread_mutex_lock(mut + n);
        // 向终端打印字符
        write(1, &c, 1);
        // 释放下一个线程的锁
        pthread_mutex_unlock(mut + next(n));
    }

    pthread_exit(NULL);
}

int main(void) {
    int i, err;
    pthread_t tid[THRNUM];

    for(i = 0; i < THRNUM; i++) {
        // 初始化锁
        pthread_mutex_init(mut + i, NULL);
        // 加锁
        pthread_mutex_lock(mut + i);
        err = pthread_create(tid + i, NULL, thr_func, (void *)i);
        if(err) {
            fprintf(stderr, "pthread_create(): %s\n", strerror(err));
            exit(1);
        }
    }
    // 释放打印a的线程的锁
    pthread_mutex_unlock(mut + 0);
    // 运行3s终止进程
    alarm(3);
    for(i = 0; i < THRNUM; i++)
        pthread_join(tid[i], NULL);

    exit(0);
}

7.5.3 线程池

线程数是有一定限制的,8.5.1节用201个线程来检测质数,本节利用线程池来解决。

假设线程池提供4个线程来检测201个质数。设置临界区资源num,当:

num = 0:当前没有任务
num = -1:当前任务已经全部完成
num = 300000~3000200:当前有一个任务,需要一个线程来接受任务
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>

#define LEFT  30000000
#define RIGHT 30000200
#define THRNUM 4 // 假设线程池中有4个线程

// 临界区资源
static int num = 0;
// 互斥锁
static pthread_mutex_t mut_num = PTHREAD_MUTEX_INITIALIZER;

static void *thr_prime(void *p) {
    int i, j, mark;
    // 死循环:领取任务
    while(1) {
        pthread_mutex_lock(&mut_num);
        // 循环检测是否有任务
        while(num == 0) { // 当前没有任务
            pthread_mutex_unlock(&mut_num);
            sched_yield();
            pthread_mutex_lock(&mut_num);
        }
        // 拿到任务,判断是否为-1,即任务已经结束
        if(num == -1) {
            // 要释放锁,防止其他线程死锁
            pthread_mutex_unlock(&mut_num);
            break;
        }
        // 拿到任务
        i = num;
        // 将num设置为0,即没有任务
        num = 0;
        pthread_mutex_unlock(&mut_num);
        //---------线程做任务:检测是否为质数-------
        mark = 1;
        for(j = 2; j < i/2; j++) {
            if(i % j == 0) {
                mark = 0;
                break;
            }
        }
        if(mark)
            printf("[thread-%d]%d is a primer.\n", (int)p, i);
    }
    pthread_exit(NULL);
}

int main(void) {
	int i, err;
    pthread_t tid[THRNUM];
    // 启动线程池
    for(i = 0; i < THRNUM; i++) {
        err = pthread_create(tid+i, NULL, thr_prime, (void *)i);
        if(err) {
            fprintf(stderr, "pthread_create(): %s\n", strerror(err));
            exit(1);
        }
    }
	
    // 主线程下发任务
    for(i = LEFT; i <= RIGHT; i++) {
        pthread_mutex_lock(&mut_num);
        // 循环检测任务是否被线程领走
        while(num != 0) { // 没有领走任务
            pthread_mutex_unlock(&mut_num);
            // 让出CPU
            sched_yield();
            pthread_mutex_lock(&mut_num);
        }
        // 设置num,即下发任务
        num = i;
        pthread_mutex_unlock(&mut_num);
    } // 下发任务完毕
    
    // 设置num为-1,代表任务全部结束
    pthread_mutex_lock(&mut_num);
    // 循环检测最后一个任务是否被完成
    while(num != 0) { // 没有完成
        pthread_mutex_unlock(&mut_num);
        sched_yield();
        pthread_mutex_lock(&mut_num);
    }
    // 已完成,则设置为-1
    num = -1;
    pthread_mutex_unlock(&mut_num);
	// 收尸
    for (i = 0; i < THRNUM; i++) {
        pthread_join(tid[i], NULL);
    }
	// 销毁互斥锁
    pthread_mutex_destroy(&mut_num);
    exit(0);
}

打印结果:

[thread-0]30000001 is a primer.
[thread-2]30000041 is a primer.
[thread-3]30000023 is a primer.
[thread-1]30000037 is a primer.
[thread-1]30000079 is a primer.
[thread-0]30000059 is a primer.
[thread-3]30000071 is a primer.
[thread-2]30000049 is a primer.
[thread-0]30000083 is a primer.
[thread-1]30000109 is a primer.
[thread-3]30000137 is a primer.
[thread-2]30000133 is a primer.
[thread-3]30000163 is a primer.
[thread-1]30000149 is a primer.
[thread-0]30000167 is a primer.
[thread-2]30000169 is a primer.
[thread-0]30000199 is a primer.
[thread-3]30000193 is a primer.

不足:该程序存在盲等,即查询法的不足,上游main线程一直在循环查看任务是否被领走,而下游一直在循环查看是否有任务。

通知法:上游将设置任务后,唤醒下游来处理任务。如果没有领走任务,则阻塞自己,等待下游来唤醒。

下游发现有任务,则领走任务,并唤醒上游;没有任务,则阻塞,等待上游来唤醒。

7.5.4 线程令牌桶

暂略

7.5.5 条件变量

条件变量是线程可用的另一种同步机制。条件变量给多个线程提供了一个会合的场所。条件变量与互斥量一起使用时,允许线程以无竞争的方式等待特定的条件发生。

条件本身是由互斥量保护的。线程在改变条件状态之前必须首先锁住互斥量。其他线程在获得互斥量之前不会察觉到这种改变,因为互斥量必须在锁定以后才能计算条件。

在使用条件变量之前,必须先对它进行初始化。由pthread_cond_t数据类型表示的条件变量可以用两种方式进行初始化。可以把常量PTHREAD_COND_INITTALIZER赋给静态分配的条件变量但是如果条件变量是动态分配的,则需要使用pthread_cond_init函数对它进行初始化。在释放条件变量底层的内存空间之前,可以使用pthread_cond_destroy函数对条件变量进行反初始化。

相关函数和作用:

初始化条件变量

#include <pthread.h>
// 销毁
int pthread_cond_destroy(pthread_cond_t *cond);

// 动态初始化
int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);

// 静态初始化
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

阻塞当前线程,等待条件成立

#include <pthread.h>

// 有时间的条件等待
int pthread_cond_timedwait(pthread_cond_t *restrict cond,
                           pthread_mutex_t *restrict mutex,
                           const struct timespec *restrict abstime);

// 条件等待
int pthread_cond_wait(pthread_cond_t *restrict cond,
                      pthread_mutex_t *restrict mutex);

cond:已初始化好的条件变量
mutex:与条件变量配合使用的互斥锁
abstime:阻塞线程的时间

调用两个函数之前,我们必须先创建好一个互斥锁并完成加锁操作,然后才能作为实参传递给 mutex 参数。两个函数会完成以下两项工作:

阻塞线程,直至接收到条件成立的信号
当线程被添加到等待队列上时,将互斥锁解锁,即释放mutex

也就是说,函数尚未接收到“条件成立”的信号之前,它将一直阻塞线程执行。注意,当函数接收到“条件成立”的信号后,它并不会立即结束对线程的阻塞,而是先完成对互斥锁的“加锁”操作,然后才解除阻塞。

两个函数都以“原子操作”的方式完成“阻塞线程+解锁”或者“重新加锁+解除阻塞”这两个过程。

解除线程的“阻塞”状态

#include <pthread.h>
// 唤醒所有的阻塞线程
int pthread_cond_broadcast(pthread_cond_t *cond);

// 唤醒所有正在的至少一个线程
int pthread_cond_signal(pthread_cond_t *cond);

对于被上面两个函数阻塞的线程,我们可以借助如上两个函数向它们发送“条件成立”的信号,解除它们的“阻塞”状态。

由于互斥锁的存在,解除阻塞后的线程也不一定能立即执行。当互斥锁处于“加锁”状态时,解除阻塞状态的所有线程会组成等待互斥锁资源的队列,等待互斥锁“解锁”。

代码示例1——查询法转通知法

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>

#define LEFT  30000000
#define RIGHT 30000200
#define THRNUM 4

static int num = 0;
static pthread_mutex_t mut_num = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t cond_num = PTHREAD_COND_INITIALIZER;

static void *thr_prime(void *p) {
    int i, j, mark;
    while(1) {
        pthread_mutex_lock(&mut_num);
        // 查看是否有任务
        while(num == 0) { // 没有任务
            // 则阻塞自己,释放互斥锁
            pthread_cond_wait(&cond_num, &mut_num);
        }
        if(num == -1) {
            pthread_mutex_unlock(&mut_num);
            break;
        }
        // 领走任务
        i = num;
        num = 0;
        // 唤醒所有阻塞线程
        pthread_cond_broadcast(&cond_num);
        pthread_mutex_unlock(&mut_num);
        mark = 1;
        for(j = 2; j < i/2; j++) {
            if(i % j == 0) {
                mark = 0;
                break;
            }
        }
        if(mark)
            printf("[thread-%d]%d is a primer.\n", (int)p, i);
    }
    pthread_exit(NULL);
}

int main(void) {
	int i, err;
    pthread_t tid[THRNUM];
    for(i = 0; i < THRNUM; i++) {
        err = pthread_create(tid+i, NULL, thr_prime, (void *)i);
        if(err) {
            fprintf(stderr, "pthread_create(): %s\n", strerror(err));
            exit(1);
        }
    }

    for(i = LEFT; i <= RIGHT; i++) {
        pthread_mutex_lock(&mut_num);
        // 查看任务是否领走
        while(num != 0) { // 任务没有领走
            // 阻塞自己,并释放互斥锁
            pthread_cond_wait(&cond_num, &mut_num);
        }
        // 已经被领走,则设置任务
        num = i; // 设置任务
        // 唤醒任何一个阻塞的线程
        pthread_cond_signal(&cond_num);
        pthread_mutex_unlock(&mut_num);
    }
    pthread_mutex_lock(&mut_num);
    while(num != 0) {
        pthread_mutex_unlock(&mut_num);
        sched_yield();
        pthread_mutex_lock(&mut_num);
    }
    num = -1;
    // 唤醒所有线程
    pthread_cond_broadcast(&cond_num);
    pthread_mutex_unlock(&mut_num);

    for (i = 0; i < THRNUM; i++) {
        pthread_join(tid[i], NULL);
    }

    pthread_mutex_destroy(&mut_num);
    pthread_cond_destroy(&cond_num);
    exit(0);
}

代码示例2——打印abcd

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>

#define THRNUM 4

static int num = 0;
static pthread_mutex_t mut = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

static int next(int n) {
    return (n + 1) == THRNUM ? 0 : n + 1;
}

static void *thr_func(void *p) {
    int n = (int)p;
    int c = 'a' + (int)p;

    while(1) {
        pthread_mutex_lock(&mut);
        // 向终端打印字符
        while(num != n) {
            // 如果num不是自己的编号,则阻塞并释放锁
            pthread_cond_wait(&cond, &mut);
        }
        write(1, &c, 1);
        num = next(num); // 将num修改为下一个线程的编号
        // 广播唤醒所有阻塞的线程
        pthread_cond_broadcast(&cond);
        pthread_mutex_unlock(&mut);
    }

    pthread_exit(NULL);
}

int main(void) {
    int i, err;
    pthread_t tid[THRNUM];

    for(i = 0; i < THRNUM; i++) {
        err = pthread_create(tid + i, NULL, thr_func, (void *)i);
        if(err) {
            fprintf(stderr, "pthread_create(): %s\n", strerror(err));
            exit(1);
        }
    }
    alarm(3);
    for(i = 0; i < THRNUM; i++)
        pthread_join(tid[i], NULL);
    pthread_mutex_destroy(&mut);
    pthread_cond_destroy(&cond);
    exit(0);
}

 7.5.6 信号量

使用互斥量和条件变量可以实现信号量的功能。

需求:产生4个线程(4个资源)来筛选质数,任务完成后线程退出,然后又产生线程。这里的共享资源就是4个线程。

mysem.h

#ifndef __MYSEM_H__
#define __MYSEM_H__

// 为void取别名为mysem_t
typedef void mysem_t;

mysem_t *mysem_init(int initval);

int mysem_add(mysem_t *, int);

int mysem_sub(mysem_t *, int);

int mysem_destroy(mysem_t *);

#endif

mysem.c

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include "mysem.h"

// 信号量的结构体
struct mysem_st {
    int value; // 现有资源数(相当于信号量)
    pthread_mutex_t mut; // 互斥量
    pthread_cond_t cond; // 条件变量
};

// 信号量初始化,initval为资源总数
mysem_t *mysem_init(int initval) {
    struct mysem_st *me;
    me = malloc(sizeof(*me));
    if(me == NULL) {
        return NULL;
    }
    me->value = initval;
    pthread_mutex_init(&me->mut, NULL);
    pthread_cond_init(&me->cond, NULL);

    return me;
}

// 增加信号量
int mysem_add(mysem_t *ptr, int n) {
    // 将void*的指针指向mysem_st*
    struct mysem_st *me = ptr; 
    pthread_mutex_lock(&me->mut);
    me->value += n;
    // 广播唤醒所有等待线程
    pthread_cond_broadcast(&me->cond);
    pthread_mutex_unlock(&me->mut);
    return n;
}

// 减少信号量,索要n个资源
int mysem_sub(mysem_t *ptr, int n) {
    struct mysem_st *me = ptr;
    pthread_mutex_lock(&me->mut);
    while(me->value < n) { // 当资源总数少于需要的n
        // 条件等待
        pthread_cond_wait(&me->cond, &me->mut);
    }
    me->value -= n;
    pthread_mutex_unlock(&me->mut);
    return n;
}

// 销毁信号量
int mysem_destroy(mysem_t *ptr) {
    struct mysem_st *me = ptr;
    pthread_mutex_destroy(&me->mut);
    pthread_cond_destroy(&me->cond);
    free(me);
    return 0;
}

main.c

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include "mysem.h"

#define LEFT  30000000
#define RIGHT 30000200
#define THRNUM (RIGHT-LEFT+1)
#define N 4 // 资源总量(可创建的线程总数)

static mysem_t *sem;

static void *thr_prime(void *p) {
    int i, j, mark;
    i = (int)p;
    mark = 1;
    for(j = 2; j < i/2; j++) {
        if(i % j == 0) {
            mark = 0;
            break;
        }
    }
    if(mark)
        printf("%d is a primer.\n", i);
   	// 假设任务需要5s完成
    sleep(5);
    // 增加信号量
    mysem_add(sem, 1);
	pthread_exit(NULL);
}

int main(void) {
	int i, err;
    pthread_t tid[THRNUM];
    
    sem = mysem_init(N);
    if(sem == NULL) {
        fprintf(stderr, "mysem_init() failed!\n");
        exit(1);
    }
    for(i = LEFT; i <= RIGHT; i++) {
        // 减少信号量
        mysem_sub(sem, 1);
        err = pthread_create(tid+(i-LEFT), NULL, thr_prime, (void *)i);
        if(err) {
            fprintf(stderr, "pthread_create(): %s\n", strerror(err));
            exit(1);
        }
    }

    for (i = LEFT; i <= RIGHT; i++) {
        pthread_join(tid[i-LEFT], NULL);
    }
    mysem_destroy(sem);
    exit(0);
}

makefile

CFLAGS+=-pthread
LDFLAGS+=-pthread

all: mysem

mysem:main.o mysem.o
	gcc $^ -o $@ $(CFLAGS) $(LDFLAGS)

clean:
	rm -rf *.o mysem

执行结果

使用命令,查看线程关系:

ps ax -L

 由下图所示,主线程main[3134]创建了4个线程来筛选质数:        ​​​​​​​        ​​​​​​​        ​​​​​​​        ​​​​​​​        ​​​​​​​                ​​​​​​​        

当某个线程完成任务后,增加信号量,并唤醒阻塞线程,下图表示原来的4个线程已经完成任务退出,主线程又创建了4个新的线程来筛选质数:


同理,图表示原来的4个线程已经完成任务退出,主线程又创建了4个新的线程来筛选质数:

7.6 线程属性

pthread 接口允许我们通过设置每个对象关联的不同属性来细调线程和同步对象的行为。通常,管理这些属性的函数都遵循相同的模式。

在所有调用pthread_create函数的实例中,传入的参数都是空指针,而不是指向pthread_attr_t结构的指针。可以使用pthread_attr_t结构修改线程默认属性,并把这些属性与创建的线程联系起来。

可以使用pthread_attr_init函数初始化 pthread_attr_t 结构。在调用 pthread attr_init 以后,pthread_attr_t 结构所包含的就是操作系统实现支持的所有线程属性的默认值。

初始化和销毁

#include <pthread.h>

int pthread_attr_init(pthread_attr_t *attr);
int pthread_attr_destroy(pthread_attr_t *attr);

下图总结了 POSIX.1 定义的线程属性。POSIX.1 还为线程执行调度(Thread Execution Scheduling)选项定义了额外的属性,用以支持实时应用。下图同时给出了各个操作系统平台对每个线程属性的支持情况。


线程分离状态属性

线程分离:在我们使用默认属性创建一个线程的时候,线程是 joinable 的。 joinable 状态的线程,必须在另一个线程中使用 pthread_join() 等待其结束, 如果一个 joinable 的线程在结束后,没有使用 pthread_join() 进行操作, 这个线程就会变成”僵尸线程”。可以使用pthread_detach函数让线程分离。

当线程被设置为分离状态后,线程结束时,它的资源会被操作系统自动的回收, 而不再需要在其它线程中对其进行 pthread_join() 操作。

#include <pthread.h>
// 设置状态
int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);
// 获取状态
int pthread_attr_getdetachstate(pthread_attr_t *attr, int *detachstate);

detachstate:可以设置为以下属性
PTHREAD_CREATE_DETACHED:线程分离状态
PTHREAD_CREATE_JONINABLE:线程可joinable状态


代码示例:

static void *fn(void *p) {
    // ...
}

int main(void) {
    int err;
    pthread_t tid;
    pthread_attr_t attr;
    // 初始化
    pthread_attr_init(&attr);
    // 设置创建的线程为分离状态
    pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
    // 创建线程
    err = pthread_create(&tid, &attr, fn, NULL);
    if(err) {
        //...
        exit(1);
    }
    // 销毁
    pthread_attr_destroy(&attr);
    exit(0);
}

线程的栈和栈大小

可以使用下列函数设置线程的栈属性。

#include <pthread.h>

int pthread_attr_setstack(pthread_attr_t *attr, void *stackaddr, size_t stacksize);
int pthread_attr_getstack(pthread_attr_t *attr, void **stackaddr, size_t *stacksize);

对于进程来说,虚地址空间的大小是固定的。因为进程中只有一个栈,所以它的大小通常不是问题。但对于线程来说,同样大小的虚地址空间必须被所有的线程栈共享。如果应用程序使用了许多线程,以致这些线程栈的累计大小超过了可用的虚地址空间,就需要减少默认的线程栈大小。另一方面,如果线程调用的函数分配了大量的自动变量,或者调用的函数涉及许多很深的栈帧(stack frame),那么需要的栈大小可能要比默认的大。

如果线程栈的虚地址空间都用完了,那可以使用malloc或者mmap来为可替代的栈分配空间,并用pthread_attr_setstack函数来改变新建线程的栈位置。由stackaddr 参数指定的地址可以用作线程视的内容范围中的最低可寻找地址,该地址与处理器结构相应的边界应对齐。当然,这要假设malloc和mmap所用的虚地址范围与线程栈当前使用的虚地址范围不同。

stackaddr线程属性被定义为栈的最低内存地址,但这并不一定是栈的开始位置。对于一个给定的处理器结构来说,如果栈是从高地址向低地址方向增长的,那么 stackaddr线程属性将是栈的结尾位置,而不是开始位置。

应用程序也可以通过下列函数读取或设置线程属性stacksize。

#include <pthread.h>

int pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize);
int pthread_attr_getstacksize(pthread_attr_t *attr, size_t *stacksize);

如果希望改变默认的栈大小,但又不想自己处理线程栈的分配问题,这时使用pthread_attr_setstacksize函数就非常有用。设置stacksize属性时,选择的stacksize不能小于PTHREAD_STACK_MIN。

代码示例——测试线程数量的上限:

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>

static void *func(void *p) {
    while(1);
    pthread_exit(NULL);
}

int main(void) {
    int i, err;
    pthread_t tid;
    pthread_attr_t attr;
    pthread_attr_init(&attr);
    // 定义线程的栈大小为10M
    pthread_attr_setstacksize(&attr, 1024 * 1024);
    for(i = 0; i ; i++) {
        err = pthread_create(&tid, NULL, func, NULL);
        if(err) {
            fprintf(stderr, "pthread_create(): %s\n", stderror(err));
            break;
        }
    }
    printf("%d\n", i);
    pthread_attr_destroy(&attr);
    exit(0);
}

7.7 同步属性

线程的同步对象也具有属性。

7.7.1 互斥量属性

互斥量属性是用 pthread_mutexattr_t 结构表示的。在8.5.2节每次对互斥量进行初始化时,都是通过使用PTHREAD_MUTEX_INITTALIZER 常量或者用指向互斥量属性结构的空指针作为参数调用 pthread_mutex_init 函数,得到互斥量的默认属性。

对于非默认属性,可以使用下列函数进行初始化和销毁:

#include <pthread.h>

int pthread_mutexattr_destroy(pthread_mutexattr_t *attr);
int pthread_mutexattr_init(pthread_mutexattr_t *attr);

互斥量的三个主要属性:

进程共享属性
健壮属性(略)
类型属性
进程共享

在进程中,多个线程可以访问同一个同步对象。正如在8.5.2节看到的,这是默认的行为。在这种情况下,进程共享互斥量属性需设置为PTHREAD_PROCESS_PRIVATE。

但也存在这样的机制:允许相互独立的多个进程把同一个内存数据块映射到它们各自独立的地址空间中。就像多个线程访问共享数据一样,多个进程访问共享数据通常也需要同步。如果进程共享互斥量属性设置为PTHREAD_PROCESS_SHARED,从多个进程彼此之间共享的内存数据块中分配的互斥量就可以用于这些进程的同步。

相关函数调用:

#include <pthread.h>

int pthread_mutexattr_getpshared(const pthread_mutexattr_t * restrict attr, int *restrict pshared);
int pthread_mutexattr_setpshared(pthread_mutexattr_t *attr, int pshared);

pshared:这里的p指的就是进程process
PTHREAD_PROCESS_PRIVATE:进程独占互斥量
PTHREAD_PROCESS_SHARED:进程共享互斥量

类型属性

类型互斥量属性控制着互斥量的锁定特性。POSIX.1定义了4种类型。

PTHREAD_MUTEX_NORMAL:一种标准互斥量类型,不做任何特殊的错误检查或死锁检测。
PTHREAD_MUTEX_ERRORCHECK:此互斥量类型提供错误检查。
PTHREAD_MUTEX_RECURSIVE :此互斥量类型允许同一线程在互斥量解锁之前对该互斥量进行多次加锁。递归互斥量维护锁的计数,在解锁次数和加锁次数不相同的情况下,不会释放锁。所以,如果对一个递归互斥量加锁两次,然后解锁一次,那么这个互斥量将依然处于加锁状态,对它再次解锁以前不能释放该锁。
PTHREAD_MUTEX_DEFAULT:此互斥量类型可以提供默认特性和行为。操作系统在实现它的时候可以把这种类型自由地映射到其他互斥量类型中的一种。

上图不占用解锁是指解锁不是自己加的锁(解锁别人加的锁),例如打印abcd的程序。

相关函数:

#include <pthread.h>

int pthread_mutexattr_gettype(const pthread_mutexattr_t *restrict attr, int *restrict type);
int pthread_mutexattr_settype(pthread_mutexattr_t *attr, int type);

7.7.2 条件变量属性

相关函数:

#include <pthread.h>

int pthread_condattr_destroy(pthread_condattr_t *attr);
int pthread_condattr_init(pthread_condattr_t *attr);

int pthread_condattr_getpshared(const pthread_condattr_t *restrict attr, int *restrict pshared);
int pthread_condattr_setpshared(pthread_condattr_t *attr,int pshared);

7.8 线程安全IO

 此前介绍过的IO都是线程安全的IO,即在当多个线程操作IO时,都必须对缓冲区进行加锁和解锁,防止出现对缓冲区进行竞争的现象。

例如三个线程,分别向标准输出终端打印连续字符aaa,bbb和ccc,如果使用线程安全的io,例如puts,可能出现的情况有:

aaabbbccc
aaacccbbb
cccbbbaaa
// ...

但绝不会出现下面这种情况:

abcabcabc
aabbccabc
// ... 

原因就在于,puts是线程安全的,在对缓冲区操作前,需要加锁。

也存在线程不安全的IO调用,当考虑到效率问题(省去加锁和解锁的时间),并且确保只有单线程操作缓冲区时,可以使用下面的这些函数,这些函数在后面都加上了_unlocked,表示不加锁。

#include <stdio.h>
int getc_unlocked(FILE *stream);
int getchar_unlocked(void);
int putc_unlocked(int c, FILE *stream);
int putchar_unlocked(int c);

void clearerr_unlocked(FILE *stream);
int feof_unlocked(FILE *stream);
int ferror_unlocked(FILE *stream);
int fileno_unlocked(FILE *stream);
int fflush_unlocked(FILE *stream);
int fgetc_unlocked(FILE *stream);
int fputc_unlocked(int c, FILE *stream);
size_t fread_unlocked(void *ptr, size_t size, size_t n,
                      FILE *stream);
size_t fwrite_unlocked(const void *ptr, size_t size, size_t n,
                       FILE *stream);

char *fgets_unlocked(char *s, int n, FILE *stream);
int fputs_unlocked(const char *s, FILE *stream);

7.9 线程和信号

对多线程的进程而言,只有进程级别的未决信号集pending,没有信号屏蔽字mask,而每个线程都有自己的pending和mask(线程级别)。

进程向进程发送信号,改变的是进程级别的pending,线程向线程发送信号,改变的是线程级别的pending。对于线程级别的信号响应,使用当前线程的pending和mask进行按位与。对于进程级别的信号响应,使用当前工作线程的mask和进程级别的pending进行按位与。

此前讨论了进程如何使用 sigprocmask 函数来阻止信号发送。然而,sigprocmask 的行为在多线程的进程中并没有定义,线程必须使用pthread_sigmask。

#include <signal.h>

// 修改线程级别的信号屏蔽字
int pthread_sigmask(int how, const sigset_t *set, sigset_t *oldset);

// 向指定线程发送信号
int pthread_kill(pthread_t thread, int sig);

线程可以调用sigwait等待一个或多个信号的出现:

#include <signal.h>

// 等待信号集set
int sigwait(const sigset_t *set, int *sig);

7.10 线程与fork

暂略

  • 27
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值