进程与线程

进程是一个运行程序的实例;线程像一个轻量级的进程;在一个共享内存系统中,一个进程可以有多个线程。

Pthreads,是一个 Unix 系统标准;一个可以用于 C 语言的库;是多线程编程的一个 API 接口。

一个简单的 Pthread 程序

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

int thread_count;
void* Hello(void* rank);

int main(int argc, char *argv[]) {
    long thread;
    pthread_t* thread_handles;
    thread_count = strtol(argv[1], NULL,10);

    thread_handles = malloc(thread_count*sizeof(pthread_t));

    for (thread = 0; thread < thread_count; thread++) {
        pthread_create(&thread_handles[thread], NULL, Hello, (void*) thread);
    }
    printf("Hello from the main thread\n");

    for (thread = 0; thread < thread_count; thread++) {
        pthread_join(thread_handles[thread], NULL);
    }

    free(thread_handles);
    return 0;

}

void* Hello(void* rank){
    long my_rank = (long) rank;
        printf("Hello from thread %ld of %d \n", my_rank, thread_count);
    return NULL;
}

启动线程

Pthread 是由程序来启动线程的,这样就需要在程序中添加相应的代码来显式启动线程,并构造能够储存信息的数据结构。

thread_count = strtol(argv[1], NULL,10);
thread_handles = malloc(thread_count*sizeof(pthread_t));
//为每个线程的 pthread_t 分配内存,pthread_t 数据结构用来存储线程的专有信息,它由 pthread.h 声明

1. C 库函数 void *malloc(size_t size)分配所需的内存空间,并返回一个指向它的指针。
2. C 库函数 long int strtol(const char *str, char **endptr, int base) 把参数 str 所指向的字符串根据给定的 base 转换为一个长整数(类型为 long int 型),base 必须介于 2 和 36(包含)之间,或者是特殊值 0。

pthread_t 对象是一个不透明对象。对象存储的数据都是由系统决定的,用户级代码无法直接访问;Pthreads 标准保证 pthread_t 能够存储足够信息来标识唯一线程。

创建线程

int pthread_create (
    pthread_t* thread_p /* out */ , 
    const pthread_attr_t* attr_p /* in */ , 
    void* (*start_routine ) ( void ) /* in */ , 
    void* arg_p /*in*/);

第一个参数是一个指针,指向对应的 pthread_t 对象。
第二个参数一般用 NULL 就行
第三个参数表示该线程将要运行的函数。
最后一个参数也是一个指针,指向传给函数start_routine 的参数列表。

for (thread = 0; thread < thread_count; thread++) {
        pthread_create(&thread_handles[thread], NULL, Hello, (void*) thread);
    }

pthread_t 对象不是由 pthread_create 函数分配的,必须在调用 pthread_create 函数前就为 pthread_create 函数前就为 pthread_t 对象分配内存空间。

传入函数

void* Hello(void* rank){
    long my_rank = (long) rank;
        printf("Hello from thread %ld of %d \n", my_rank, thread_count);
    return NULL;
}

void* 可以转为任意 C 类型;args_p 可以指向任何参数;函数返回值可以是任何内容。 需要注意的是:我们为每一个线程分配不同的编号只是为了方便使用。事实上,pthread_create 创建线程并没有要求必须传递线程号,也没有要求必须要分配线程号给一个线程。

运行线程

运行 main 函数的线程一般称为主线程。所以在线程启动后有一句这样的打印:

printf("Hello from the main thread\n");

同时,调用 pthread_create 所生成的线程也在运行。所以这一句的打印出现在中间。

在 pthread 中,程序员不直接控制线程在哪个核上运行。在 pthread_create 函数中,没有参数用于指定在哪个核上运行线程。线程的调度是由操作系统来做的。

停止线程

pthread_join 可以用于线程之间的同步, 当一个线程对另一个线程调用了join操作之后, 该线程会处于阻塞状态, 直到另外一个线程执行完毕.

for (thread = 0; thread < thread_count; thread++) {
        pthread_join(thread_handles[thread], NULL);
}

第二个参数可以接受任意由 pthread_t 对象所关联的线程的那个线程产生的返回值。

下面是两个示意图:

编译运行

$ gcc -g -Wall -o pth_hello pth_hello.c -lpthread
$ ./pth_hello 8

Mutex 互斥锁

问题引入

下面我们一起来看一段有问题的pthread程序:

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#define M 3

int num = 10;

void* fun(void* args){
    while (num > 0) {
        sleep(1);
        num--;
        printf("Current num: %d \n", num);
    }
    return NULL;
}

int main(int argc, char *argv[]) {
    pthread_t id[M];
    int i;
    for (i = 0; i < M; i++) {
        pthread_create(&id[i], NULL, fun, NULL);
    }
    for (i = 0; i < M; i++) {
        pthread_join(id[i], NULL);
    }
}

输出结果:

正常情况下,程序只会打印出 0~9 这十个数字,但是为什么会出现-1-2 呢?原因就出在sleep(1) 这行代码上,我们一起来看一下下面这张图:
详细解释请看视频,我懒得打字了

互斥锁

互斥锁用来保护共享变量, 它可以保证某个时间内只有一个线程访问共享变量, 下面是使用互斥锁的具体步骤

  • 声明 pthread_mutex_t (互斥锁类型) 类型的变量
  • 调用 pthread_mutex_init() 来初始化变量
  • 在访问共享变量之前, 调用 pthread_mutex_lock() 获得互斥锁, 如果互斥锁被其他线程占用, 该线程会处于等待状态
  • 访问完共享变量之后, 调用 pthread_mutex_unlock() 释放互斥锁, 以便其他线程使用
  • 程序执行完后调用 pthread_mutex_destroy()释放资源.

创建互斥锁有两种方式:

  1. 静态方式和动态方式. 静态方式是使用宏 PTHREAD_MUTEX_INITIALIZER 来初始化锁, 如下所示:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
  1. 动态方式是调用 pthread_mutex_init 函数动态初始锁, 下面是该函数原型
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t * attr)

下面是使用互斥锁的一个示例:

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#define M 3
pthread_mutex_t mutex; //声明互斥锁类型的变量
int num = 10;

void* fun(void* args){

    /* 临界区开始 */
    pthread_mutex_lock(&mutex); //获得互斥锁
    while (num > 0) {
        sleep(1);
        num--;
        printf("Current num: %d \n", num);
    }
    pthread_mutex_unlock(&mutex); //释放互斥锁
    /* 临界区结束 */

    return NULL;
}

int main(int argc, char *argv[]) {
    pthread_t id[M];
    int i;
    pthread_mutex_init(&mutex,NULL); //初始化变量
    for (i = 0; i < M; i++) {
        pthread_create(&id[i], NULL, fun, NULL);
    }
    for (i = 0; i < M; i++) {
        pthread_join(id[i], NULL);
    }
    pthread_mutex_destroy(&mutex); //释放资源
}

这里有几个使用互斥量的几个注意点:

  • 使用 lock 和 unlock 一个互斥锁时, 一定要先初始化该互斥锁
  • 释放互斥锁的线程必须是获得互斥锁的那个线程
  • 当 destroy 互斥锁的时候, 不该有线程还在使用这个互斥锁

Semaphores 信号量

信号量本质上可以看做是一个计数器, 它主要有两种操作, 第一类操作为 down 或者 wait – sem_wait(...), 目的是为了减小计数器(将信号俩减1), 另一类为 up 或者 signal – sem_post(...) , 目的是为了增大计数器(将信号量加1). 当线程调用 sem_wait() 时, 如果信号量的值大于0, 那么只会把信号量减1, 线程会继续往下执行. 如果信号量的值为0, 那么线程就会进入阻塞状态, 直到另外一个线程执行了 sem_post() 操作, 对信号量进行了增操作, 该线程才会继续往下执行.

信号量主要用于对一些稀缺资源的同步, 什么叫做稀缺资源, 就是说这个资源只有有限的几个, 但是又多于一个, 在某一个时刻, 可以供有限的几个线程使用, 但又不是全部线程使用. 如果将信号量初始化为1, 那么该信号量就等同于互斥锁了, 因此一次只能有一个线程获得信号量的资源, 如果其他线程想要获得, 必须等该线程对信号量进行增操作. 举个例子说: 有10个人去银行办理业务, 但是银行只有4个窗口(信号量初始化为4), 所以前4个人到了银行就可以办理业务, 但是第5个人之后就必须要等待, 等前面的某个人办理完业务(增加信号量), 空出窗口来. 而当第5个人去办理业务时, 空出的窗口又被占用了(减小信号量), 剩下的人还是要等待. 信号量在执行过程中和上述例子不同的一点是, 当有空余的资源出现时, 线程并不一定按照 FIFO(先进先出) 的顺序来获取资源, 而有可能是随机一个线程获得资源.

下面是信号量相关的函数

类型

信号量的类型是 sem_t, 需要引入头文件 #include <semaphore.h>

初始化和销毁

int sem_init(sem_t *sem, int pshared, unsigned int value);
int sem_destroy(sem_t *sem);

init 函数的第二个参数用来标识信号量的范围: 0 表示一个进程中线程间共享, 非0 表示进程间共享. 第三个参数就是信号量的可用数量.

wait和signal

int sem_wait(sem_t *sem);
int sem_post(sem_t *sem);

下面是一个使用示例

int sem_share_data = 0;
// use like a mutex
sem_t binary_sem; 

void * p_sem(void * arg) {
    sem_wait(&binary_sem);     // 减少信号量
    // 在这里使用共享数据; 
    sem_post(&binary_sem);     // 增加信号量
}

void test_sem() {
    sem_init(&binary_sem, 0, 1);   // 信号量初始化为1, 当初互斥锁使用
    // 在这里创建线程
    sem_wait(&binary_sem);
    // 在这里使用共享变量
    sem_post(&binary_sem);
    // 在这里join线程
    sem_destroy(&binary_sem);
}

参考文章

pthread Tutoriaed Tutorial
POSIX Threads Programming
Linux线程-互斥锁pthread_mutex_t
Pthread:POSIX 多线程程序设计
POSIX 多线程程序设计
Pthreads多线程编程指南
Pthreads基本使用
使用pthread进行并行编程

最后修改:2022 年 04 月 12 日
如果觉得我的文章对你有用,请随意赞赏