线程
线程基本知识
什么是线程
轻量级进程(LWP: light weight process),在linux环境下线程的本质仍然是进程.
系统分配资源的最基本单位是——进程
系统调度进程执行的最小单位是——线程
1、多个子线程和主线程共享一个地址空间,有一个PID;
2、通过使用线程号来区分不同的线程;
3、除了栈空间以外,其余资源都可以共享;
4、主线程和子线程是靠抢占CPU时间片来执行的,谁先抢到谁先使用;
查看指定线程的LWP号:ps -Lf pid
实际上,无论是创建进程的fork,还是创建线程的pthread_create,底层实现都是调用同一个内核函数clone:
如果复制对方的地址空间,那么就产生一个进程
如果共享对方的地址空间,就产生一个线程
linux内核是不区分进程和线程的,只在用户层面上进程区分
线程操作是调用库函数,而不是系统调用
线程共享资源
- 文件描述符表
- 信号处理方式
- 用户ID和组ID
- 内存地址空间(.txt,.data,.bss,堆,共享库)
线程非共享资源
- 线程ID
- 栈
- errno变量
- 调度优先级
网上找到一篇大佬对线程资源的总结 线程共享资源 很详细,一定要阅读以下!!!
线程优缺点
优点:
提高程序并发性
开销小
共享数据方便缺点:
库函数,不稳定
gdb调试困难
编写程序困难
线程相关函数
- pthread_create
int pthread_create(pthread_t* thread, const pthread_attr_t* att, void* (*start_routine)(void*), void* arg);
param:
thread: 出参,线程ID
att: 通常传NULL,表示线程属性默认
start_routine: 函数指针,指向线程主函数,该函数运行结束,则线程结束
arg: 线程函数执行期间需要的参数
return:
成功:返回0
失败:返回错误号
强调:
1、由于pthread_create的错误码不保存在errno中,因此不能直接调用perror()打印错误信息,可以先用strerror()把错误码转换成错误信息再打印。
2、如果任意一个线程调用了exit或者_exit,则整个进程的所有线程都终止,因此线程中禁止调用exit函数(除非是类似malloc调用失败)
- pthread_self
int pthread_self(void);
return:
返回线程ID
例1: 循环创建5个子线程
注意:如果使用变量i作为线程函数的参数,那么创造出来的线程得到的i都是5。因为i的内存只有一块,这块内存是以地址形式传给子线程的,所以被5个线程共享的,主线程在一个CPU时间片完成了5个子线程的创建,此时i=5,而其它子线程在主线程让出CPU时间片后才得到执行,所以最后每个线程读到的值都是5。另外,5个子线程也是抢占CPU的,所以执行顺序每次可能都不一样。
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/types.h>
#include<unistd.h>
#include<pthread.h>
void* mythread(void* arg)
{
int i = *(int*)arg;
printf("[%d]\n", i);
}
int main()
{
int ret;
int i=0;
int arr[5];
pthread_t thread[5];
for(i=0;i<5;i++)
{
arr[i]=i;
//ret = pthread_create(&thread[i], NULL, mythread, &i);
ret = pthread_create(&thread[i], NULL, mythread, &arr[i]);
if(ret!=0)
{
printf("pthread_create error, [%s]\n", strerror(ret));
return -1;
}
}
sleep(1);//可以用线程函数pthread_join代替
return 0;
}
- pthread_exit
使一个线程退出,如果主线程调用pthread_exit函数也不会使整个进程退出,不影响其他线程的执行。
void pthread_exit(void* retval);
param:
retval: 表示线程的退出状态,通常传NULL
注意:pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是用malloc分配的,不能在线程函数的栈上分配,因为栈空间会被回收。
- pthread_join
阻塞并等待线程退出,获取线程退出状态。对应进程的waitpid()函数
int pthread_join(pthread_t thread, void** retval);
param:
thread: 线程ID
retval: 存储线程结束状态,整个指针和pthread_exit的参数是同一块内存地址
return:
成功:0
失败:返回错误号
pthread_join中的指针与pthread_exit指向的是同一块地址(最好用全局变量的地址,因为栈会被释放)
例2: 子线程返回给主线程值
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/types.h>
#include<unistd.h>
#include<pthread.h>
typedef struct
{
int a;
char str[10];
}Arg;
void* mythread(void* arg)
{
Arg *r = (Arg*)arg;
r->a = r->a * r->a;
printf("--- [%p] --- \n", r);
//return (void*)r;
pthread_exit((void*)r);
}
int main()
{
pthread_t tid;
Arg r = {10, "abcdef"};
pthread_create(&tid, NULL, mythread, (void*)&r);
int *result;
pthread_join(tid, (void**)&result);
printf("===[%p]===%d %s\n", result, ((Arg*)result)->a, ((Arg*)result)->str);
return 0;
}
例3: 子线程将返回值以地址的形式返回给主线程
typedef struct
{
int a;
char str[10];
}Arg;
void* mythread(void* arg)
{
Arg *r = (Arg*)arg;
r->a = r->a * r->a;
pthread_exit((void*)r->a);
}
int main()
{
pthread_t tid;
Arg r = {10, "abcdef"};
pthread_create(&tid, NULL, mythread, (void*)&r);
int *result;
pthread_join(tid, (void**)&result);
printf("线程返回值:%d\n", (int)result);
return 0;
}
- pthread_detach
设置线程属性为分离线程,一般情况下,线程终止后,其终止状态一直保留到其他线程调用pthread_join,但是线程也可以被设置成detach状态,这样的线程一旦终止就立刻回收它所占用的资源,而不保留终止状态。不能对一个已经处于detach状态的线程调用
phread_join,这样调用将返回错误。分离属性在网络通讯中使用较多。
int pthread_detach(pthread_t thread);
param:
thread: 线程ID
return:
成功:0
失败:错误号
- pthread_cancel
杀死线程,相当于进程中的kill()
注意:线程取消不是实时的,而是有一定的延时,需要达到某个取消点(man 7 pthreads
可以粗略的认为一个系统调用(进入内核)就是一个取消点,比如printf就会调用write进入内核,另外,也可以用pthread_testcancel(void)设置取消点)
int pthread_cancel(pthread_t thread);
param:
thread: 线程ID
return:
成功:0
失败:错误号
线程属性
默认线程属性都是非分离状态
//1、定义线程属性类型的变量
pthread_attr_t attr;
//2、兑线程属性变量进行初始化
int pthread_attr_init(pthread_attr_t* attr);
//3、设置线程分离属性
int pthread_attr_setdetachstate(pthread_attr_t* attr, int detach);
param:
detach:PTHREAD_CREATE_DETACHED(分离)
PTHREAD_CREATE_JOINABLE(非分离)
//前三步都是为了pthread_creat的第二个参数准备
//4、释放线程属性资源(主线程中调用)
int pthread_attr_destroy(pthread_attr_t* attr);
线程同步
线程同步指一个线程发出某一功能调用时,在没有得到结果之前,该调用不返回。同时其他线程为保证数据一致性,不能调用该功能。
互斥锁
线程A和线程B共同访问共享资源,当线程A想访问共享资源的时候,要先获得锁,如果锁被占用,则加锁不成功,需要阻塞并等待对方释放锁;若锁没有被占用,则获得锁成功—加锁,然后操作共享资源,操作完之后,必须解锁,同理线程B也一样。也就是同时不能有两个线程操作共享资源,属于互斥操作。
原子操作:该操作要么不执行,要么就完成,不会出现做一半另一半不做的情况。使用互斥锁其实就是模拟原子操作。
linux提供一把互斥锁mutex(也称为互斥量)。每个线程在对资源操作前都尝试先加锁,成功加锁才能操作,操作结束解锁。
互斥锁函数
1、mutex类型的变量
pthread_mutex_t mutex;
2、初始化一个互斥锁,可以理解成mutex初始值是1
int pthread_mutex_init(pthread_mutex_t* restrict mutex, const pthread_mutexattr_t* restrict attr);
param:
attr: 互斥锁属性,默认为NULL
3、销毁一个互斥锁
int pthread_mutex_destroy(pthread_mutex_t* mutex);
4、加锁,可以理解为mutex--,由1变成0,阻塞函数
int pthread_mutex_lock(pthread_mutex_t* mutex);
5、解锁,可以理解为mutex++,由0变成1
int pthread_mutex_unlock(pthread_mutex_t* mutex);
6、尝试加锁
int pthread_mutex_trylock(pthread_mutex_t* mutex);
使用互斥锁后,两个线程并行操作变成了串行操作,效率降低了,但是数据不一致的问题得到了解决。
死锁
死锁并不是linux提供给用户的一种使用方法,而是用户使用互斥锁不当引起的一种现象。
注意:线程在异常退出的时候也需要解锁
A线程占用着A锁,又想去获得B锁;B线程占用着B锁,又想去获取A锁。两个线程都不释放自己的锁,又想去获得对方的锁,从而造成了死锁。
如何解锁死锁
- 让线程按照一定的顺序去访问共享资源;
- 在访问其他锁的时候,需要先将自己的锁解开;
- 调用pthread_mutex_trylock,如果加锁不成功会立刻返回;
- 避免使用嵌套的锁;
读写锁
读写锁是一把锁
读写锁也叫共享-独占锁,当读写锁以读模式锁住时,它是以共享模式锁住的;当它以写模式锁住时,它是以独占模式锁住的。写独占、读共享,当读和写一起等待锁的时候,写的优先级高。
适用情况:读写锁非常适合于对数据结构读的次数远大于写的情况。
读写锁特性:
- 读写锁是“写模式加锁”时,解锁前,所有对该锁加锁的线程都会被阻塞;
- 读写锁是“读模式加锁”时,如果线程以读模式对其加锁会成功;如果线程以写模式加锁会阻塞;
练习:
1、线程A加写锁成功,线程B请求读锁——线程B阻塞,当线程A解锁后,线程B加锁成功;
2、线程A加读锁成功,线程B请求写锁——线程B阻塞,当线程A解锁后,线程B加锁成功;
3、线程A加读锁成功,线程B请求读锁——线程B请求成功;
4、线程A加读锁成功,线程B请求写锁,线程C请求读锁——线程B和C都阻塞,当线程A释放锁后,线程B先获得锁,C阻塞;当B释放锁后,C获得锁;
5、线程A加写锁成功,线程B请求读锁,线程C请求写锁——线程B和C都阻塞,当线程A释放锁后,线程C先获得锁,B阻塞;当C释放锁后,B获得锁;
读写锁函数
1、定义一把读写锁
pthread_rwlock_t rwlock;
2、初始化一个读写锁
int pthread_rwlock_init(pthread_rwlock_t* restrict rwlock, const pthread_rwlockattr_t* restrict attr);
param:
attr: 读写锁属性,默认为NULL
3、销毁一个读写锁
int pthread_rwlock_destroy(pthread_rwlock_t* rwlock);
4、加读锁数
int pthread_rwlock_rdlock(pthread_rwlock_t* rwlock);
5、加写锁
int pthread_rwlock_wrlock(pthread_rwlock_t* rwlock);
6、解锁
int pthread_rwlock_unlock(pthread_rwlock_t* rwlock);
6、尝试加锁
int pthread_rwlock_tryrdlock(pthread_rwlock_t* rwlock);
int pthread_rwlock_trywrlock(pthread_rwlock_t* rwlock);
例4: 3个线程写数据,5个线程读数据
#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
#include<unistd.h>
#include<pthread.h>
#include<time.h>
int number = 0;
//定义一把读写锁
pthread_rwlock_t rwlock;
//写线程回调函数
void* thread_write(void* arg)
{
int i = *(int*)arg;
int cur;
while(1)
{
//加写锁
pthread_rwlock_wrlock(&rwlock);
cur=number;
cur++;
number=cur;
printf("[%d]-W:[%d]\n", i, cur);
//解锁
pthread_rwlock_unlock(&rwlock);
usleep(500);
}
}
//读线程回调函数
void* thread_read(void* arg)
{
int i=*(int*)arg;
int cur;
while(1)
{
//加读锁
pthread_rwlock_rdlock(&rwlock);
cur=number;
printf("[%d]-R:[%d]\n", i, cur);
//解锁
pthread_rwlock_unlock(&rwlock);
usleep(400);
}
}
int main()
{
//创建子线程
int n = 8;
int i = 0;
int arr[8];
pthread_t thread[8];
//初始化读写锁
pthread_rwlock_init(&rwlock, NULL);
//创建3个写线程
for(i = 0; i < 3; i++)
{
arr[i]=i;
pthread_create(&thread[i], NULL, thread_write, &arr[i]);
}
//创建5个读线程
for(i = 3; i < 8; i++)
{
arr[i]=i;
pthread_create(&thread[i], NULL, thread_read, &arr[i]);
}
//回收子线程
int j=0;
for(; j < n; j++)
{
pthread_join(thread[j], NULL);
}
//销毁读写锁
pthread_rwlock_destroy(&rwlock);
return 0;
}
条件变量
条件变量本身不是锁,但它可以造成线程阻塞,通常和互斥锁配合使用。给多个线程提供了一个会合的场所。
- 使用互斥量保护共享数据
- 使用条件变量可以使线程阻塞,等待某个条件的发生,当条件满足的时候解除阻塞。
条件变量的两个动作:
- 条件不满足,阻塞线程,并解锁
- 条件满足,解除阻塞,并加锁
条件变量函数
1、定义一个条件变量
pthread_cond_t cond;
2、初始化条件变量
int pthread_cond_init(pthread_cond_t* restrict cond, const pthread_condattr_t* restrict attr);
3、销毁条件变量
int pthread_cond_destroy(pthread_cond_t* cond);
4、阻塞等待
int pthread_cond_wait(pthread_cond_t* restrict cond, pthread_mutex_t* restrict mutex);
5、唤醒阻塞的线程
int pthread_cond_signal(pthread_cond_t* cond);
例5: 生产者消费者模型:生产者生产链表,消费者读取链表数据并释放
#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
#include<unistd.h>
#include<pthread.h>
#include<time.h>
typedef struct node
{
int data;
struct node* next;
}NODE;
NODE* head=NULL;
//定义一把锁
pthread_mutex_t mutex;
//定义条件变量
pthread_cond_t cond;
//生产者线程
void* producer(void* arg)
{
NODE* pNode=NULL;
while(1)
{
//生产一个节点
pNode=(NODE*)malloc(sizeof(NODE));
if(pNode==NULL)
{
perror("malloc error");
exit(-1);
}
pNode->data=rand()%1000;
printf("Producer:[%d]\n", pNode->data);
//加锁
pthread_mutex_lock(&mutex);
pNode->next=head;
head=pNode;
//解锁
pthread_mutex_unlock(&mutex);
//唤醒条件
pthread_cond_signal(&cond);
sleep(rand()%3);//避免申请太快
}
}
//消费者线程
void* consumer(void* arg)
{
NODE* pNode=NULL;
while(1)
{
//加锁
pthread_mutex_lock(&mutex);
if(head==NULL)
{
//条件不满足,需阻塞等待
//若条件不满足,则阻塞等待并加锁;条件满足,则接触阻塞并加锁
pthread_cond_wait(&cond, &mutex);
}
printf("Consumer:[%d]\n", head->data);
pNode=head;
head=head->next;
//解锁
pthread_mutex_unlock(&mutex);
free(pNode);
pNode=NULL;
sleep(rand()%3);
}
}
int main()
{
srand(time(NULL));
pthread_t thread1;
pthread_t thread2;
//初始化锁
pthread_mutex_init(&mutex, NULL);
//初始化条件变量
pthread_cond_init(&cond, NULL);
//创建生产者线程
pthread_create(&thread1, NULL, producer, NULL);
//创建生产者线程
pthread_create(&thread2, NULL, consumer, NULL);
//回收子线程
pthread_join(thread1, NULL);
pthread_join(thread2, NULL);
//销毁锁
pthread_mutex_destroy(&mutex);
//销毁条件变量
pthread_cond_destroy(&cond);
return 0;
}
例6: 强化:有5个生产者和5个消费者,共同来创造和消费链表
发生段错误的情况:假如只有一个生产者生产了一个节点,然后通知消费者,此时若有多个消费者被唤醒,那么条件变量满足,但是只有一个消费者获得锁,其余消费者都阻塞在了mutex而不是条件变量,因此只有一个消费者消费了节点,之后释放锁,其他消费者获得锁后,但此时head已经成了NULL,所以发生了段错误。
在使用条件变量的线程中,能够引起线程阻塞的地方有两个:
1、在条件变量处阻塞——这个会被pthread_cond_signal唤醒
2、互斥锁也会使线程阻塞——其它线程解锁会解除阻塞
#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
#include<unistd.h>
#include<pthread.h>
#include<time.h>
typedef struct node
{
int data;
struct node* next;
}NODE;
NODE* head=NULL;
//定义一把锁
pthread_mutex_t mutex;
//定义条件变量
pthread_cond_t cond;
//生产者线程
void* producer(void* arg)
{
NODE* pNode=NULL;
int n=*(int*)arg;
while(1)
{
//生产一个节点
pNode=(NODE*)malloc(sizeof(NODE));
if(pNode==NULL)
{
perror("malloc error");
exit(-1);
}
pNode->data=rand()%1000;
printf("Producer[%d]:[%d]\n", n, pNode->data);
//加锁
pthread_mutex_lock(&mutex);
pNode->next=head;
head=pNode;
//解锁
pthread_mutex_unlock(&mutex);
//唤醒条件
pthread_cond_signal(&cond);
sleep(rand()%3);//避免申请太快
}
}
//消费者线程
void* consumer(void* arg)
{
NODE* pNode=NULL;
int n=*(int*)arg;
while(1)
{
//加锁
pthread_mutex_lock(&mutex);
if(head==NULL)
{
//条件不满足,需阻塞等待
//若条件不满足,则阻塞等待并加锁;条件满足,则解除阻塞并加锁
pthread_cond_wait(&cond, &mutex);
}
if(head==NULL)
{
pthread_mutex_unlock(&mutex);
continue;
}
printf("Consumer[%d]:[%d]\n", n, head->data);
pNode=head;
head=head->next;
//解锁
pthread_mutex_unlock(&mutex);
free(pNode);
pNode=NULL;
sleep(rand()%3);
}
}
int main()
{
srand(time(NULL));
pthread_t thread1[5];
pthread_t thread2[5];
int i;
int arr[5];
//初始化锁
pthread_mutex_init(&mutex, NULL);
//初始化条件变量
pthread_cond_init(&cond, NULL);
for(i=0;i<5;i++)
{
arr[i]=i;
//创建生产者线程
pthread_create(&thread1[i], NULL, producer, &arr[i]);
//创建生产者线程
pthread_create(&thread2[i], NULL, consumer, &arr[i]);
}
for(i=0;i<5;i++)
{
//回收子线程
pthread_join(thread1[i], NULL);
pthread_join(thread2[i], NULL);
}
//销毁锁
pthread_mutex_destroy(&mutex);
//销毁条件变量
pthread_cond_destroy(&cond);
return 0;
}
信号量
信号量相当于多把锁,可以理解为加强版的互斥锁
信号量函数
1、定义信号量
sem_t sem;
2、初始化信号量
int sem_init(sem_t* sem, int pshared, unsigned int value);
param:
pshared: 0表示线程同步;1表示进程同步
value: 表示最多有几个线程操作共享数据
3、调用一次相当于sem--,当sem为0时,引起阻塞
int sem_wait(sem_t* sem);
4、调用一次相当于sem++
int sem_post(sem_t* sem);
5、尝试加锁,直接返回
int sem_trywait(sem_t* sem);
6、销毁信号量
int sem_destroy(sem_t* sem);
例7: 使用信号量实现生产者消费者模型
#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
#include<unistd.h>
#include<pthread.h>
#include <semaphore.h>
#include<time.h>
typedef struct node
{
int data;
struct node* next;
}NODE;
NODE* head=NULL;
//定义信号量
sem_t sem_producer;
sem_t sem_consumer;
//生产者线程
void* producer(void* arg)
{
NODE* pNode=NULL;
while(1)
{
//生产一个节点
pNode=(NODE*)malloc(sizeof(NODE));
if(pNode==NULL)
{
perror("malloc error");
exit(-1);
}
pNode->data=rand()%1000;
printf("Producer:[%d]\n", pNode->data);
//加锁,相当于--
sem_wait(&sem_producer);
pNode->next=head;
head=pNode;
//解锁,相当于++
sem_post(&sem_consumer);
sleep(rand()%3);//避免申请太快
}
}
//消费者线程
void* consumer(void* arg)
{
NODE* pNode=NULL;
while(1)
{
//加锁,相当于--
sem_wait(&sem_consumer);
printf("Consumer:[%d]\n", head->data);
pNode=head;
head=head->next;
//解锁,相当于++
sem_post(&sem_producer);
free(pNode);
pNode=NULL;
sleep(rand()%3);
}
}
int main()
{
srand(time(NULL));
pthread_t thread1;
pthread_t thread2;
//初始化信号量
sem_init(&sem_producer, 0, 5);
sem_init(&sem_consumer, 0, 0);
//创建生产者线程
pthread_create(&thread1, NULL, producer, NULL);
//创建生产者线程
pthread_create(&thread2, NULL, consumer, NULL);
//回收子线程
pthread_join(thread1, NULL);
pthread_join(thread2, NULL);
//销毁信号量
sem_destroy(&sem_producer);
sem_destroy(&sem_consumer);
return 0;
}