文章目录
- 线程基础
-
- 一个进程中的多个线程共享以下资源
- 每个线程私有的资源如下
- 多线程编程
-
- pthread线程库中提供了如下基本操作
-
- 创建线程 pthread_create()
- 回收线程 pthread_join()
- 结束线程 pthread_detach()
- 线程 – 示例
- 线程间同步和互斥机制
-
- 线程间同步
- 信号量(灯)
- Posix信号量
- 信号量初始化sem_init()
- 信号量–P/V操作sem_wait()/sem_post()
-
- int sem_wait(sem_t *sem); P操作
- int sem_post(sem_t *sem); V操作
- 线程同步示例1
- 线程同步示例2
- 线程间互斥
-
- 互斥锁初始化 – pthread_mutex_init
- 申请锁 – pthread_mutex_lock
- 释放锁 – pthread_mutex_unlock
- 线程互斥 – 示例
线程基础
- 每个用户进程有自己的地址空间
- 系统为每个用户进程创建一个 task_struct来描述该进程
- 该结构体中包含了一个指针指向该进 程的虚拟地址空间映射表
- 实际上task_struct 和地址空间映射表一起用来表示一个进程
- 由于进程的地址空间是私有的,因此在进程间上下文切换时,系统开销比较大
- 为了提高系统的性能,许多操作系统规范里引入了轻量级进程的概念,也被称为线程
- 在同一个进程中创建的线程共享该进程的地址空间
- Linux里同样用task_struct来描述一个线程。线程和进程都参与统一的调度
- 通常线程指的是共享相同地址
空间的多个任务
使用多线程的好处 - 大大提高了任务切换的效率
避免了额外的TLB & cache的刷新
一个进程中的多个线程共享以下资源
- 可执行的指令
- 静态数据
- 进程中打开的文件描述符
- 信号处理函数
- 当前工作目录
- 用户ID
- 用户组ID
每个线程私有的资源如下
- 每个线程私有的资源如下
- 线程ID (TID)
- PC(程序计数器)和相关寄存器
- 堆栈
局部变量
返回地址 - 错误号 (errno)
- 信号掩码和优先级
- 执行状态和属性
多线程编程
pthread线程库中提供了如下基本操作
创建线程 pthread_create()
回收线程 pthread_join()
结束线程 pthread_detach()
这将该子线程的状态设置为detached,则该线程运行结束后会自动释放所有资源。
线程 – 示例
#include <stdio.h>
#include <string.h>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>char message[32] = "Hello World";
void *thread_func(void *arg);int main(void)
{pthread_t a_thread;void *result;//创建线程if (pthread_create(&a_thread, NULL, thread_func, NULL) != 0) {printf("fail to pthread_create\n"); exit(-1);}//回收线程pthread_join(&a_thread, &result);printf("result is %s\n", result);printf("message is %s\n", message);return 0;}void *thread_func(void *arg)
{sleep(1);strcpy(message, "marked by thread");pthread_exit("thank you for waiting for me");
}
result is thank you for waiting for me
message is marked by thread
线程间同步和互斥机制
- 线程共享同一进程的地址空间
- 优点:线程间通信很容易通过全局变量交换数据
- 缺点:多个线程访问共享数据时需要同步或互斥机制
线程间同步
- 同步(synchronization)指的是多个任务按照约定的先后次序相互配合完成一件事情
- 1968年,Edsgar Dijkstra基于信号量的概念提出了一种同步机制
- 由信号量来决定线程是继续运行还是阻塞等待
信号量(灯)
- 信号量代表某一类资源,其值表示系统中该资源的数量
- 信号量是一个受保护的变量,只能通过三种操作来访问
·初始化
·P操作(申请资源):当任务(比如线程)要访问某个资源的时候,因为任务不知道当前系统中有没有这个资源,所以该任务对代表此资源的信号量进行P操作(检查信号量的值):如果信号量的值大于0,任务继续执行,访问资源,如果当前信号量的值等于0,就代表没有资源,则任务阻塞,直到有资源为止。
·V操作(释放资源):如果当前任务不需要访问资源了,或者任务产生了一个资源,就要执行V操作(告诉系统,资源数增加了,系统就可以唤醒等待这些资源的任务了)
Posix信号量
-
posix中定义了两类信号量:
·无名信号量(基于内存的信号量):仅内存中存在,没有实际的文件和信号量一一对应,主要用于进程内部线程之间通信(,也可以用于进程之间但不方便)
·有名信号量:可用于线程间通信,也可用于进程间通信 -
pthread库常用的信号量操作函数如下:
int sem_init(sem_t *sem, int pshared, unsigned int value);
int sem_wait(sem_t *sem); // P操作
int sem_post(sem_t *sem); // V操作
信号量初始化sem_init()
成功时返回0,失败时EOF
sem 指向要初始化的信号量对象
pshared 0 – 线程间 1 – 进程间
val 信号量初值
信号量–P/V操作sem_wait()/sem_post()
- 由信号量来决定线程是继续运行还是阻塞等待
-
P(S) 含义如下:
if (信号量的值大于0) { 申请资源的任务继续运行;信号量的值减一;} else {申请资源的任务阻塞; }
-
V(S) 含义如下:
信号量的值加一; if (有任务在等待资源) {唤醒等待的任务,让其继续运行 }
int sem_wait(sem_t *sem); P操作
int sem_post(sem_t *sem); V操作
线程同步示例1
两个线程同步读写缓冲区(生产者/消费者问题)
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <string.h>
#include <semaphore.h>char buf[32];
sem_t sem;
void *function(void *arg);
int main(void)
{pthread_t a_thread;if (sem_init(&sem,0,0) < 0) //先初始化信号量,再创建线程{perror("sem_init"); exit(-1);}if (pthread_create(&a_thread,NULL,function,NULL) != 0) //先初始化信号量,再创建线程{printf("fail to pthread_create"); exit(-1); } printf("input ‘quit’ to exit\n");do {fgets(buf,32,stdin);sem_post(&sem); }while (strncmp(buf,"quit",4) != 0);return 0;
}void *function(void *arg)
{while (1) {sem_wait(&sem);printf("you enter %d characters\n", strlen(buf));}
}
线程同步示例2
-
读线程在读缓冲区前,P操作检查缓冲区中有没有数据,没有的话阻塞,有的话才读数据
-
实际上对于写线程来说,也要如此。当缓冲区为空的时候,才能去写数据。
-
如果读线程数据处理过程比较长,读线程还没读完,写线程又把新数据覆盖上去了,这样就破坏了数据,是不合理的
#include <stdio.h>#include <pthread.h>#include <stdlib.h>#include <string.h>#include <semaphore.h>char buf[32];sem_t sem_r,sem_w;void *function(void *arg);int main(void) {pthread_t a_thread;if (sem_init(&sem_r,0,0) < 0) //刚开始缓冲区是空的,不可读{perror("sem_r_init"); exit(-1);}if (sem_init(&sem_w,0,1) < 0) //刚开始缓冲区是空的,可写{perror("sem_r_init"); exit(-1);}if (pthread_create(&a_thread,NULL,function,NULL) != 0) //先初始化信号量,再创建线程{printf("fail to pthread_create"); exit(-1); } printf("input ‘quit’ to exit\n");do {sem_wait(&sem_w);//写之前对可写信号量进行P操作,缓冲区非空则阻塞fgets(buf,32,stdin);//缓冲区可写,写线程执行写操作sem_post(&sem_r); //写完之后对可读信号量进行V操作,表示可读信号量增加了}while (strncmp(buf,"quit",4) != 0);return 0;}void *function(void *arg) {while (1) {sem_wait(&sem_r);printf("you enter %d characters\n",strlen(buf));sem_post(&sem_w);} }
线程间互斥
- 临界资源
· 一次只允许一个任务(进程、线程)访问的共享资源 - 临界区
·访问临界资源的代码
·访问非临界资源的代码叫非临界区 - 互斥机制
·临界区互斥:当一个任务在访问临界区的时候,其他任务不能访问该临界区(相同的临界资源)
·mutex互斥锁:互斥锁只能被一个任务所持有。互斥锁有两种状态,空闲/只被一个任务所持有。
·任务访问临界资源前申请锁,访问完后释放锁。没有申请到锁的任务需要阻塞等待,直到持有这个锁为止
互斥锁初始化 – pthread_mutex_init
申请锁 – pthread_mutex_lock
释放锁 – pthread_mutex_unlock
- 成功时返回0,失败时返回错误码
- mutex 指向要初始化的互斥锁对象
- 执行完临界区要及时释放锁
- 如果有多个临界资源,比如一个任务每次都要同时访问两个临界资源,那这两个临界资源只需要一个互斥锁保护就行。如果这两个临界资源访问不是同时的,就需要两个互斥锁来分别保护
线程互斥 – 示例
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>unsigned int count, value1, value2;
pthread_mutex_t lock;void *function(void *arg);
int main(void)
{pthread_t a_thread;if (pthread_mutex_init(&lock, NULL) != 0) {printf("fail to pthread_mutex_init\n"); exit(-1);}if (pthread_create(&a_thread, NULL, function, NULL) != 0) {printf("fail to pthread_create"); exit(-1); }while ( 1 ) {count++;
#ifdef _LOCK_pthread_mutex_lock(&lock);
#endifvalue1 = count;value2 = count;
#ifdef _LOCK_pthread_mutex_unlock(&lock);
#endif}return 0;
}void *function(void *arg)
{while ( 1 ) {
#ifdef _LOCK_pthread_mutex_lock(&lock);
#endifif (value1 != value2) {printf("value1 = %u, value2 = %u\n", value1, value2);usleep(100000);}
#ifdef _LOCK_pthread_mutex_unlock(&lock);
#endif}return NULL;
}
- 主线程中count++,之后会先把count赋给value1,再把count赋给value2;另外一个线程每隔100ms执行一次判断value1!=value2,如果不等于,分别打印value1和value2;
- 如果不上锁,主线程把count赋给value1后,如果还没来得及赋给value2,主线程的时间片用完了,系统自动调度到另外 一个线程,就会出现value1!=value2的情况
- 如果上了互斥锁,就能让两个线程中的临界区代码严格互斥
不使用互斥锁
$ gcc –o test test.c -lpthread$ ./testvalue1 = 19821, value2 = 19820value1 = 14553456, value2 = 14553455value1 = 29196032, value2 = 29196031……
使用互斥锁
$ gcc –o test test.c –lpthread –D_LOCK_$ ./test