线程
与进程(process)类似,线程(thread)是允许应用程序并发执行多个任务的一种机制。一个进程可以包含多个线程,同一个进程中的线程均会独立执行相同的程序,且共享同一份全局内存区域,包括初始化数据段,未初始化数据段,以及堆内存段
进程是CPU分配资源的最小单位,线程是操作系统调度执行的最小单位
线程是轻量级进程(LWP: Light Weight Process),在Linux环境下线程的本质仍是进程
查看指定进程的LWP号:
ps -Lf pid
线程共享资源
- 进程ID和父进程ID
- 进程组ID和会话ID
- 用户ID和用户组ID
- 文件描述符表
- 信号处置
- 文件系统的相关信息:文件权限掩码(umask),当前工作目录
- 虚拟地址空间(除栈、.text)
线程非共享资源
- 线程ID
- 信号掩码
- 线程特有数据
- error变量
- 实时调度策略和优先级
- 栈、本地变量和函数的调用链接信息
查看当前系统使用的线程库
getconf GNU_LIBPTHREAD_VERSION
NPTL 2.27 // Native POSIX Thread Library
线程相关API
创建线程
main
函数所在的线程称为主线程,其余线程称为子线程
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
// 创建一个线程
- thread: 传出参数,子线程id
- attr: 设置线程的属性,默认NULL
- start_routine: 子线程需要处理的逻辑代码
- arg: 给第三个参数使用
// 成功返回0,失败返回错误号,与errno不一样
// 获取错误号信息:char* strerror(int errnum)
#include <string.h>
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
void* callback(void* arg) {
printf("child thread...\n");
printf("arg: %d\n", *(int *)arg);
return NULL;
}
int main() {
pthread_t tid;
int num = 10;
int ret = pthread_create(&tid, NULL, callback, (void *)&num);
if (ret != 0) {
char *errstr = strerror(ret);
printf("%s\n", errstr);
}
for (int i = 0; i < 5; i++) {
printf("%d\n", i);
}
sleep(1);
return 0;
}
0
1
child thread...
arg: 10 // 主线程和子线程交替执行
2
3
4
终止线程
#include <pthread.h>
void pthread_exit(void *retval);
// 线程退出
- retval: 作为返回值,向线程的回收者传递其退出信息,可以在pthread_join()中获取到
=============================================
pthread_t pthread_self(void);
// 返回当前线程id
=============================================
int pthread_equal(pthread_t t1, pthread_t t2);
// 比较两个线程id是否相等
#include <string.h>
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
void* callback(void* arg) {
printf("child thread: %ld\n", pthread_self()); // 获取当前线程id
return NULL;
}
int main() {
pthread_t tid;
int ret = pthread_create(&tid, NULL, callback, NULL);
if (ret != 0) {
char *errstr = strerror(ret);
printf("%s\n", errstr);
}
// 主线程
for (int i = 0; i < 5; i++) {
printf("%d\n", i);
}
printf("tid: %ld, main id: %ld\n", tid, pthread_self());
pthread_exit(NULL);// 主线程退出,对子线程没有影响
return 0;
}
0
1
2
3
4
tid: 140341352228608, main id: 140341360527168
child thread: 140341352228608
连接已终止的线程
和一个已经终止的线程进行连接,来回收子线程的资源
int pthread_join(pthread_t thread, void **retval);
// 阻塞函数,调用一次回收一个子线程
// 主线程调用,等待子线程退出并回收其资源,类似于进程中wait/waitpid
- thread: 需要回收的子线程id
- retval: 接受子线程退出的返回值
// 成功返回0,失败返回错误码
+ EDEADLK: 可能引起死锁
+ EINVAL: 目标线程是不可回收的
+ ESRCH: 目标线程不存在
#include <string.h>
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
int val = 10;
void* callback(void* arg) {
printf("child thread: %ld\n", pthread_self()); // 获取当前线程id
pthread_exit((void *)&val); // return (void *)&val
}
int main() {
pthread_t tid;
int ret = pthread_create(&tid, NULL, callback, NULL);
if (ret != 0) {
char *errstr = strerror(ret);
printf("%s\n", errstr);
}
// 主线程
for (int i = 0; i < 5; i++) {
printf("%d\n", i);
}
printf("tid: %ld, main id: %ld\n", tid, pthread_self());
// 主线程回收子线程的资源
int *thread_retval;
ret = pthread_join(tid, (void **)&thread_retval); // 阻塞直至子线程结束
if (ret != 0) {
char *errstr = strerror(ret);
printf("%s\n", errstr);
}
printf("return value: %d\n", *thread_retval);
printf("recovery success\n");
return 0;
}
0
1
2
3
4
tid: 140608466753280, main id: 140608475051840
child thread: 140608466753280
return value: 10
recovery success
线程分离
分离一个线程,不能多次分离,分离之后不能再连接
该线程结束退出后,系统会自动回收它的资源,不再需要pthread_join
int pthread_detach(pthread_t thread);
- thread: 要分离的线程id
// 成功返回0,失败返回错误号
线程取消
int pthread_cancel(pthread_t thread);
// 取消线程,让线程终止
// 不是立刻终止,而是当子线程执行到一个取消点才会终止
// 取消点:规定好的一些系统调用,例如
+ pthread_join/pthread_testcancel
+ pthread_cond_wait/pthread_cond_timewait
+ sem_wait/sig_wait
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
void* callback(void* arg) {
printf("child thread id: %ld\n", pthread_self());
for (int i = 0; i < 10; i++) {
printf("child: %d\n", i);
}
return NULL;
}
int main() {
pthread_t tid;
int ret = pthread_create(&tid, NULL, callback, NULL);
if (ret != 0) {
char *errstr = strerror(ret);
printf("error: %s\n", errstr);
}
// 取消线程
pthread_cancel(tid);
for (int i = 0; i < 5; i++) {
printf("%d\n", i);
}
// 输出主线程和子线程id
printf("tid: %ld, main id: %ld\n", tid, pthread_self());
pthread_exit(NULL);
return 0;
}
child thread id: 140210217600768
child: 0
child: 1
child: 2
child: 3
child: 3
0
1
2
3
4
tid: 140210217600768, main id: 140210225899328
线程属性
int pthread_attr_init(pthread_attr_t *attr);
// 初始化线程属性变量
---
int pthread_attr_destroy(pthread_attr_t *attr);
// 释放线程属性的资源
---
int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);
// 设置线程分离的状态属性
---
int pthread_attr_getdetachstate(const pthread_attr_t *attr, int *detachstate);
// 获取线程分离的状态属性
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
void* callback(void* arg) {
printf("child thread id: %ld\n", pthread_self());
return NULL;
}
int main() {
// 创建一个线程属性变量
pthread_attr_t attr;
// 初始化属性变量
pthread_attr_init(&attr);
// 设置属性
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
pthread_t tid;
int ret = pthread_create(&tid, &attr, callback, NULL);
if (ret != 0) {
char *errstr = strerror(ret);
printf("error: %s\n", errstr);
}
// 获取线程栈
size_t size;
pthread_attr_getstacksize(&attr, &size);
printf("thread stack size: %ld\n", size);
// 输出主线程和子线程id
printf("tid: %ld, main id: %ld\n", tid, pthread_self());
// 释放线程属性资源
pthread_attr_destroy(&attr);
pthread_exit(NULL);
return 0;
}
线程同步与互斥
线程的优势在于,能够通过全局变量共享信息,但是,必须确保多个线程不会同时修改同一变量,或者某个线程不会去读取正在由其他线程修改的变量
临界区是指访问某一共享资源的代码片段,并且这段代码的执行应为原子操作
线程同步:当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作,其他线程才能操作
线程同步机制:信号量、互斥量和条件变量
互斥量
为避免线程更新共享变量时出现问题,可以使用互斥量(mutex, mutual exclusion)来确保同一时间只有一个线程可以访问共享资源
互斥量有两种状态:已锁定(locked)和未锁定(unlocked),任何时候,至多只有一个线程可以锁定该互斥量
一旦线程锁定互斥量,随即成为该互斥量的所有者,只有所有者才能给互斥量解锁
- 针对共享资源锁定互斥量
- 访问共享资源
- 对互斥量解锁
操作系统负责线程调度,为了实现『锁的状态改变时再唤醒』就需要把锁也交给操作系统管理,所以互斥量的加锁操作通常都需要涉及到上下文切换,开销比自旋锁大
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
// 初始化互斥量
- mutex: 需要初始化的互斥量
- attr: 互斥量的属性,NULL
- restrict: 修饰符,被修饰的指针不能由另外的指针进行操作
int pthread_mutex_destroy(pthread_mutex_t *mutex);
// 释放互斥量资源
int pthread_mutex_lock(pthread_mutex_t *mutex);
// 加锁,阻塞的,一个线程加锁,其他线程阻塞
int pthread_mutex_trylock(pthread_mutex_t *mutex);
// 尝试加锁,如果失败,不会阻塞,直接返回,当互斥锁已经被加锁时,返回错误码EBUSY
int pthread_mutex_unlock(pthread_mutex_t *mutex);
// 解锁
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
// 3个子线程卖100张票
int tickets = 1000;
// 创建互斥量
pthread_mutex_t mutex;
void* sellticket(void *arg) {
while (1) {
// 加锁
pthread_mutex_lock(&mutex);
if (tickets > 0) {
usleep(5000);
printf("%ld is selling %d ticket\n", pthread_self(), tickets);
tickets--;
}
else {
// 这里也要解锁,不然break之后就没解锁了
pthread_mutex_unlock(&mutex);
break;
}
// 解锁
pthread_mutex_unlock(&mutex);
}
return NULL;
}
int main() {
// 初始化互斥量
pthread_mutex_init(&mutex, NULL);
// 创建3个子线程
pthread_t tid1, tid2, tid3;
pthread_create(&tid1, NULL, sellticket, NULL);
pthread_create(&tid2, NULL, sellticket, NULL);
pthread_create(&tid3, NULL, sellticket, NULL);
pthread_join(tid1, NULL); // 阻塞,回收子线程的资源
pthread_join(tid2, NULL);
pthread_join(tid3, NULL);
pthread_mutex_destroy(&mutex); // 释放互斥量资源
pthread_exit(NULL); // 退出主线程
return 0;
}
读写锁
读写锁的特点:
- 如果有其他线程读数据,则允许,但不允许写操作
- 如果有线程写数据,则其他线程不能读数据、写数据
- 写是独占的,写的优先级高
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);
// 初始化读写锁
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
// 加读锁
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
// 加写锁
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
// 解锁
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
// 释放读写锁
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
int num = 1;
pthread_rwlock_t rwlock; // 创建读写锁
void* writeNum(void *arg) {
while (1) {
pthread_rwlock_wrlock(&rwlock); // 加写锁
num++;
printf("++write, tid: %ld, num: %d\n", pthread_self(), num);
pthread_rwlock_unlock(&rwlock); // 解写锁
usleep(100);
}
return NULL;
}
void* readNum(void *arg) {
while (1) {
pthread_rwlock_rdlock(&rwlock); // 加读锁
printf("===read, tid: %ld, num: %d\n", pthread_self(), num);
pthread_rwlock_unlock(&rwlock); // 解读锁
usleep(100);
}
return NULL;
}
int main() {
// 初始化读写锁
pthread_rwlock_init(&rwlock, NULL);
// 创建3个写线程,5个读线程
pthread_t wtids[3], rtids[5];
for (int i = 0; i < 3; i++) {
pthread_create(&wtids[i], NULL, writeNum, NULL);
}
for (int i = 0; i < 5; i++) {
pthread_create(&rtids[i], NULL, readNum, NULL);
}
// 设置线程分离
for (int i = 0; i < 3; i++) {
pthread_detach(wtids[i]);
}
for (int i = 0; i < 5; i++) {
pthread_detach(rtids[i]);
}
pthread_rwlock_destroy(&rwlock); // 释放读写锁
pthread_exit(NULL);
return 0;
}
++write, tid: 140571381163776, num: 21902
++write, tid: 140571364378368, num: 21903
===read, tid: 140571339200256, num: 21903
++write, tid: 140571372771072, num: 21904
===read, tid: 140571347592960, num: 21904
===read, tid: 140571251304192, num: 21904
===read, tid: 140571259696896, num: 21904
===read, tid: 140571355985664, num: 21904
自旋锁(spin)
自旋锁属于busy-waiting
类型的锁,如果线程A是使用pthread_spin_lock
操作去请求锁,如果自旋锁已经被线程B所持有,那么线程A就会一直在core 0上进行忙等待并不停的进行锁请求,检查该自旋锁是否已经被线程B释放,直到得到这个锁为止。因为自旋锁不会引起调用者睡眠,所以自旋锁的效率远高于互斥锁
虽然它的效率比互斥锁高,但是它也有些不足之处:
- 自旋锁一直占用CPU,在未获得锁的情况下,一直进行自旋,所以占用着CPU,如果不能在很短的时间内获得锁,无疑会使CPU效率降低
- 在用自旋锁时有可能造成死锁,当递归调用时有可能造成死锁
自旋锁适用于锁使用者保持锁时间比较短的情况
死锁
两个或两个以上的进程在执行过程中,因争夺共享资源而造成的一种互相等待的现象,若无外力作用,他们都将无法推进下去,此时系统处于死锁状态
死锁只有同时满足四个条件才会发生:
- 互斥条件
- 不可剥夺条件
- 持有并保持条件
- 环路等待条件
死锁的几个场景
- 忘记释放锁
- 重复加锁
- 多线程多锁,抢占锁资源
避免死锁问题只需要破坏其中一个条件就可以,最常见且可行的是使用资源有序分配法,来破坏环路等待条件,即线程总是以相同的顺序申请自己想要的资源