操作系统笔记(10)-并发
操作系统笔记(10)-并发
简介
操作系统除了支持多进程之外,对单个运行的进程也提供了多任务运行的支持,即
线程
,每个线程
类似于独立的进程,与多进程不同的是,它们之间共享地址空间
,从而能够访问相同的数据。另一个不同的地方在于栈
,单线程中只有一个栈而多线程中每个线程都有一个栈每个线程都有自己的一组用于计算的寄存器,因此,如果在单核机器上运行两个线程,从一个线程切换到另一个线程时,必定发生
上下文切换
线程的
上下文切换
类似于进程的上下文切换
,需要一个或多个线程控制块(TCB)
来保存每个线程的状态
共享数据的难点
使用
多线程
访问共享数据时,很容易产生一个问题就是当多个线程访问同一个地址空间时会出现竞态
,到底谁先访问谁后访问,很多时候可能会产生不同的结果,也因此有了一些并发术语:临界区(critical section) : 是访问共享资源的一段代码,资源通常是一个变量或数据结构
竞态条件(race condition) : 出现在多个执行线程大致同时进入临界区时,它们都试图更新共享的数据结构,导致了令人惊讶的结果
不确定性(indeterminate) : 程序由一个或多个竞态条件组成,程序的输出因运行而异,具体取决于哪些线程在何时运行。这导致结果是不确定的
互斥执行(mutual exclusion) : 为了解决这个问题,需要一些互斥操作,保证当一个线程访问临界区时另外的线程不能进入
POSIX 线程 API
#include <pthread.h>
// 1. 创建一个线程
// pthread_t 是 unsigned long int 类型
// attr 设置线程属性, 一般为 NULL
// start_routine 是函数指针, 指向线程将要执行的任务
// arg 是函数参数
int pthread_create(pthread_t* thread, const pthread_attr_t* attr,
void* (*start_routine)(void*), void* arg);
// 2.线程结束(执行完之后不会反回到调用者, 而且永远不会失败)
void pthread_exit(void* retval);
// 3.等待其它线程结束
int pthread_join(pthread_t thread, void** retval);
// 4.异常终止一个线程
int pthread_cancel(pthread_t thread);
一个用户可以打开的线程数量不能超过软资源限制。此外,系统上所有用户能创建的线程总数也不得超过
/proc/sys/kernel/threads-max
内核参数所定义的值线程函数在结束时最好调用此函数,以确保安全、干净地退出
注意这里传入的不是指针
该函数成功时返回0,失败则返回错误码
测试
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>
typedef struct myarg_t {
int a;
int b;
} myarg_t;
typedef struct myret_t {
int x;
int y;
} myret_t;
void* work(void *arg) {
myarg_t *m = (myarg_t*)arg;
printf("%d %d\n", m->a, m->b);
// 注意这里不能返回栈上定义的值, 因为执行完后会释放线程栈
myret_t *r = (myret_t*)malloc(sizeof(myret_t));
r->x = 1, r->y = 2;
return (void*)r;
}
int main(int argc, char* argv[]) {
pthread_t t;
myarg_t arg;
arg.a = 10, arg.b = 20;
myret_t *ret;
pthread_create(&t, NULL, work, (void*)&arg);
pthread_join(t, (void**)&ret);
printf("returned %d %d\n", ret->x, ret->y);
free(ret);
return 0;
}
// 编译指令:gcc -o p1 p1.c -lpthread
// output:
// 10 20
// returned 1 2
POSIX 互斥锁 API
- 互斥锁是为了帮我我们正确地访问
临界区
而创建的,它保证当多个线程访问某一临界区时,只有获得锁的那个线程进入
#include <pthread.h>
// 初始化互斥锁
int pthread_mutex_init(pthread_mutex_t* mutex, const pthread_mutexattr_t* mutexattr);
// 清理互斥锁的数据结构
int pthread_mutex_destroy(pthread_mutex_t* mutex);
// 尝试获得锁,若此时其它线程获得,就阻塞此线程
int pthread_mutex_lock(pthread_mutex_t* mutex);
// 尝试获得锁,若此时其它线程获得,就直接返回
int pthread_mutex_trylock(pthread_mutex_t* mutex);
// 释放已经获得的锁,让其它线程有机会获得
int pthread_mutex_unlock(pthread_mutex_t* mutex);
测试
#include <stdio.h>
#include <pthread.h>
int counter = 0;
pthread_mutex_t m;
void* work(void *arg) {
for (int i = 0; i < 10000; i++) {
pthread_mutex_lock(&m);
++counter; // 临界区
pthread_mutex_unlock(&m);
}
return NULL;
}
int main(int argc, char* argv[]) {
pthread_t p1, p2;
pthread_mutex_init(&m, NULL);
pthread_create(&p1, NULL, work, NULL);
pthread_create(&p2, NULL, work, NULL);
pthread_join(p1, NULL);
pthread_join(p2, NULL);
printf("counter : %d\n", counter);
pthread_mutex_destroy(&m);
return 0;
}
// output:
// counter : 20000
POSIX 条件变量 API
- 如果一个线程在等待另一个线程继续执行某些操作,条件变量就很有用
#include <pthread.h>
// 初始化条件变量
int pthread_cond_init(pthread_cond_t* cond, const pthread_condattr_t* cond_attr);
// 释放条件变量数据结构
int pthread_cond_destroy(pthread_cond_t* cond);
// 相当于c++的 notify_all(), 唤醒所有睡眠的线程
int pthread_cond_broadcast(pthread_cond_t* cond);
// 相当于c++的 notify_one(), 唤醒一个睡眠的线程
int pthread_cond_signal(pthread_cond_t* cond);
// (调用之前保证获取锁) 使线程睡眠, 然后释放锁,等待唤醒,唤醒后重新获得锁
int pthread_cond_wait(pthread_cond_t* cond, pthread_mutex_t* mutex);
测试
#include <stdio.h>
#include <pthread.h>
int ready = 0;
pthread_mutex_t m;
pthread_cond_t cond;
void* Wait(void *arg) {
pthread_mutex_lock(&m);
int flag = 0;
while (ready == 0) {
printf("释放锁并等待线程被唤醒...\n");
pthread_cond_wait(&cond, &m);
flag = 1;
printf("线程被唤醒,此时重新获得锁\n");
}
if (flag == 1) {
printf("Wait 线程先执行!\n");
}
else {
printf("Weak 线程先执行!\n");
}
pthread_mutex_unlock(&m);
return NULL;
}
void* Weak(void *arg) {
pthread_mutex_lock(&m);
ready = 1;
pthread_cond_signal(&cond);
pthread_mutex_unlock(&m);
}
int main(int argc, char* argv[]) {
pthread_t p1, p2;
pthread_mutex_init(&m, NULL);
pthread_cond_init(&cond, NULL);
// 注意哪个线程先执行是很重要的
pthread_create(&p1, NULL, Wait, NULL);
pthread_create(&p2, NULL, Weak, NULL);
pthread_join(p1, NULL);
pthread_join(p2, NULL);
pthread_mutex_destroy(&m);
pthread_cond_destroy(&cond);
return 0;
}
建议
保持简洁 :线程之间的锁和信号的代码应该尽可能简洁
让线程交互减到最少 :尽量减少线程之间的交互
初始化锁和条件变量 :未初始化可能会导致奇怪的结果
检查返回值 :应该检查
API
的返回值来确定函数是否调用成功注意传给线程的参数和返回值 :如果传递在栈上分配的变量的引用,就会出问题
每个线程都有自己的栈 :线程局部变量是线程私有的,要在线程之间共享数据,值要在堆或其他全局可访问的位置
线程之间总是通过条件变量发送信号 :切记不要标记变量(自旋)来同步
多查手册 :关于
API
的细节,可以查询man
手册来了解