进程与线程
进程是一个运行程序的实例;线程像一个轻量级的进程;在一个共享内存系统中,一个进程可以有多个线程。
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 声明
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*/);
第二个参数一般用 NULL 就行
第三个参数表示该线程将要运行的函数。
最后一个参数也是一个指针,指向传给函数start_routine 的参数列表。
for (thread = 0; thread < thread_count; thread++) {
pthread_create(&thread_handles[thread], NULL, Hello, (void*) thread);
}
传入函数
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()
释放资源.
创建互斥锁有两种方式:
- 静态方式和动态方式. 静态方式是使用宏
PTHREAD_MUTEX_INITIALIZER
来初始化锁, 如下所示:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
- 动态方式是调用
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进行并行编程
版权属于:KevinBean
本文链接:https://www.kevinbean.top/index.php/default/389.html
转载时须注明出处及本声明