1 线程基础
1.1 线程概念
线程实际上是应用层的概念,在Linux内核中,所有的调度实体都被称为任务(task),他们之间的区别是:有些任务自己拥有一套完整的资源,而有些任务彼此之间共享一套资源。
1.2 重要API
[1] 创建一条新线程
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
参数1:新的线程ID
参数2:线程属性
参数3:线程例程
参数4:线程例程的参数
返回值:成功0 失败errno
备注:线程属性设置为NULL,则会创建一个标准属性的线程
POSIX线程库的所有API对返回值的处理都一样:成功返回0,失败返回错误码errno。
[2] 跟线程属性相关的API
pthread_attr_getaffinity_np( ) 获取 CPU 亲和度
pthread_attr_getdetachstate( ) 获取分离属性---》pthread_join()
pthread_attr_getguardsize( ) 获取栈警戒区大小
pthread_attr_getinheritsched( ) 获取继承策略
pthread_attr_getschedparam( ) 获取调度参数-->优先级
pthread_attr_getschedpolicy( ) 获取调度策略-->轮转调度 队列(先进先出)
pthread_attr_getscope( ) 获取竞争范围
pthread_attr_getstack( ) 获取栈指针和栈大小
pthread_attr_getstackaddr( ) 已弃用
pthread_attr_getstacksize( ) 获取系统的默认线程栈大小
pthread_attr_setaffinity_np( ) 设置 CPU 亲和度
pthread_attr_setdetachstate( ) 设置分离属性
pthread_attr_setguardsize( ) 设置栈警戒区大小
pthread_attr_setinheritsched( ) 设置继承策略
pthread_attr_setschedparam( ) 设置调度参数
pthread_attr_setschedpolicy( ) 设置调度策略
pthread_attr_setscope( ) 设置竞争范围
pthread_attr_setstack( ) 设置栈的位置和栈大小(慎用)
pthread_attr_setstackaddr( ) 已弃用
pthread_attr_setstacksize( ) 设置栈大小
线程属性变量的使用步骤:
- 定义线程属性变量,并且使用pthread_attr_init()初始化
- 使用pthread_attr_setxxxx()来设置相关的属性
- 使用该线程属性变量创建相应的线程
- 使用pthread_attr_destroy()来销毁该线程属性变量
设置线程的分离属性
int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);
参数1:线程属性变量
参数2:PTHREAD_CREATE_DETACHED --->分离
PTHREAD_CREATE_JOINABLE --->接合(默认)
一条线程如果是可接合的,意味着这条线程在退出时不会自动释放自身资源,而会成为僵尸线程,同时意味着该线程的退出值可以被其他线程获取。因此,如果不需要某条线程的退出值的话,最好将线程设置为分离状态,以保证该线程不会成为僵尸线程。
[3] 退出线程
return--->如果main函数调用,则所有线程被退出
exit()
void pthread_exit(void *retval);
参数:退出值
备注:单独退出该线程,如果main线程调用,并不会退出其他的线程
[4] 线程的接合
int pthread_join(pthread_t thread, void **retval);
参数1:线程ID
参数2:保存退出值,设置为NULL
int pthread_tryjoin_np(pthread_t thread, void **retval);
区别:pthread_join()指定的线程在运行,则等待阻塞
pthread_tryjoin_np()指定的线程在运行,直接退出
[5] 给指定的线程发一个取消请求
int pthread_cancel(pthread_t thread);
2 线程安全
2.1 POSIX信号量
2.1.1 POSIX有名信号量(named-sem)
2.1.1.1 POSIX有名信号量的使用步骤
使用步骤:
- 使用sem_open()来创建或者打开一个有名信号量
- 使用sem_wait()和sem_post()来分别进行P、V操作
- 使用sem_close()关闭
- 使用sem_unlink()来删除,并释放系统资源
2.1.1.2 POSIX有名信号量API
[1] 创建、打开一个POSIX有名信号量
sem_t *sem_open(const char *name, int oflag); //信号量已经存在 则使用
sem_t *sem_open(const char *name, int oflag,
mode_t mode, unsigned int value); //信号量不存在 则使用
参数1:信号量的名字
参数2:O_CREATE---->如果该名字对应的信号量不存在,则创建
O_EXCL------>如果该名字对应的信号量存在,则报错
参数3:八进制读写权限
参数4:初始值
返回值:成功-->信号量的地址 失败-->SEM_FAILED
[2] 对POSIX有名信号进行P、V操作
int sem_wait(sem_t *sem);
int sem_post(sem_t *sem);
参数:信号量指针
返回值:成功0 失败-1
[3] 关闭POSIX有名信号量
int sem_close(sem_t *sem);
[4] 删除POSIX有名信号量
int sem_unlink(const char *name);
sem_name.c
#define SHMSIZE 100 //共享内存的大小
#define SEMNAME "sem" //有名信号量的名字
int main(int argc,char *argv[])
{
int shmid = shmget(ftok(".",1),SHMSIZE,IPC_CREAT | 0666);
if(-1 == shmid)
{
perror("shmget");
exit(-1);
}
//映射共享内存
char *shmaddr = shmat(shmid,NULL,0);
if((void *)-1 == shmaddr)
{
perror("shmat");
exit(-1);
}
//创建打开信号量
sem_t *sem = sem_open("sem",O_CREAT,0777,0);
if(SEM_FAILED == sem)
{
perror("sem_open");
exit(-1);
}
#ifdef SND
//每向共享内存写入数据,信号量的值+1
while(1)
{
fgets(shmaddr,SHMSIZE,stdin);
sem_post(sem);
if(strncmp(shmaddr,"quit",4) == 0) break;
}
#elif defined(RCV)
while(1)
{
sem_wait(sem);
puts(shmaddr);
if(strncmp(shmaddr,"quit",4) == 0) break;
}
#endif
sem_close(sem);
sem_unlink("sem");
return 0;
}
2.1.2 POSIX无名信号量(unnamed-sem)
2.1.2.1 POSIX无名信号量的使用步骤
使用步骤:
- 在这些线程都能访问的区域定义sem_t变量
- 在任何线程使用它之前,初始化sem_init()
- 分别使用sem_wait()和sem_post()来进行P、V操作
- 使用sem_destroy()销毁
2.1.2.2 POSIX无名信号量API
[1] 信号量的初始化
int sem_init(sem_t *sem, int pshared, unsigned int value);
参数1:信号量指针
参数2:信号量的作用范围
0为线程间,非0为进程间
参数3:初始值
返回值:成功0 失败-1
[2] 信号量的销毁
int sem_destroy(sem_t *sem);
返回值:成功0 失败-1
- 无名信号量一般用于进程的线程之间。
- POSIX有名信号量属于系统范围内的资源,进程结束,依旧存在。
- POSIX无名信号量属于进程范围的资源,进程结束,跟随消失。
- System-V信号量的PV操作对信号量元素加减大于1的数值,而POSIX信号量的PV操作对信号量元素加减1。
- System-V和POSIX有名信号量适用于进程间的同步互斥,而POSIX无名信号量适用于线程间的同步互斥。
2.2 互斥锁与读写锁
2.2.1 互斥锁
2.2.1.1 互斥锁概念
- 多个线程对同一个资源进行访问,会相互干扰,需要将这些进程对共享资源的访问隔离起来,使这些线程具有互斥关系。
- 特点:多个线程访问共享资源的时候是串行的。
- 缺点:效率低。
- 步骤:创建互斥锁、初始化、加锁、操作共享资源、解锁。
2.2.1.2 互斥锁API
[1] 初始化互斥锁
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
const pthread_mutexattr_t *restrict attr);
参数1:锁变量的地址
参数2:NULL
返回值:成功0 失败非0
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
等价于:pthread_mutex_init(& mutex,NULL);
[2] 加互斥锁
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);(不带阻塞)
[3] 解互斥锁
int pthread_mutex_unlock(pthread_mutex_t *mutex);
[4] 销毁互斥锁
int pthread_mutex_destroy(pthread_mutex_t *mutex);
返回值:成功0 失败非0
mutex.c
pthread_mutex_t mutex;
void output(const char *string)
{
const char *p =string;
while(*p != '\0')
{
printf("%c",*p);
usleep(100);
p++;
}
printf("\n");
}
void *routine(void *arg)
{
pthread_mutex_lock(&mutex);
output("hello,zix");
pthread_mutex_unlock(&mutex);
pthread_exit(NULL);
}
int main(int argc,char *argv[])
{
pthread_mutex_init(&mutex,NULL);
pthread_t tid;
pthread_create(&tid,NULL,routine,NULL);
pthread_mutex_lock(&mutex);
output("hello,my LAY");
pthread_mutex_unlock(&mutex);
pthread_join(tid,NULL);
pthread_mutex_destroy(&mutex);
return 0;
}
2.2.2 读写锁
2.2.2.1 读写锁概念
- 互斥锁把试图进入临界区的所有其他线程都阻塞,然而有时我们希望在读某个数据与修改某个数据之间做区分。
- 特点:读共享、写独占、读写不能同时,写的优先级高。
- 场景:互斥锁:读写串行。读写锁:读并行,写串行。
2.2.2.2 读写锁API
[1] 初始化读写锁
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,
const pthread_rwlockattr_t *restrict attr);
[2] 加读、写锁
int pthread_rwlock_rdlock(pthread_rwlock_t *restrict rwlock);
int pthread_rwlock_tryrdlock(pthread_rwlock_t *restrict rwlock);(不带阻塞)
int pthread_rwlock_wrlock(pthread_rwlock_t *restrict rwlock);
int pthread_rwlock_trywrlock(pthread_rwlock_t *restrict rwlock);(不带阻塞)
[3] 解读写锁
int pthread_rwlock_unlock(pthread_rwlock_t *restrict rwlock);
[4] 销毁读写锁
int pthread_rwlock_destroy(pthread_rwlock_t *restrict rwlock);
返回值:成功0 失败非0
rwlock.c
pthread_rwlock_t rwlock;
char buf[100];
void output(const char *buf)
{
const char *p = buf;
while(*p != '\0')
{
printf("%c",*p);
usleep(1000);
p++;
}
}
void *routine(void *arg)
{
int i=10;
while(i--)
{
pthread_rwlock_rdlock(&rwlock);
output(buf);
pthread_rwlock_unlock(&rwlock);
usleep(6000);
}
pthread_exit(NULL);
}
int main(int argc,char *argv[])
{
pthread_rwlock_init(&rwlock,NULL);
pthread_t tid[2];
pthread_create(&tid[0],NULL,routine,NULL);
pthread_create(&tid[1],NULL,routine,NULL);
int i=100;
while(i--)
{
pthread_rwlock_wrlock(&rwlock);
strcpy(buf,"123456789\n");
pthread_rwlock_unlock(&rwlock);
usleep(2000);
pthread_rwlock_wrlock(&rwlock);
strcpy(buf,"abcdefjhi\n");
pthread_rwlock_unlock(&rwlock);
usleep(2000);
}
pthread_join(tid[0],NULL);
pthread_join(tid[1],NULL);
pthread_rwlock_destroy(&rwlock);
return 0;
}
2.3 条件变量
2.3.1 条件变量概念
- 互斥锁用于上锁,条件变量用于等待。
- 条件不满足,阻塞线程。条件满足,通知阻塞的线程开始工作。
- 条件变量引起阻塞,互斥锁保护临界区。
为什么条件变量需要和互斥锁搭配使用?
1.我们需要等待的满足条件是很多线程都可以访问的,因此对这个线程的保护要用到条件锁。
2.一旦一个线程进行了加锁,那么其他线程就无法进入临界区,也就没有办法去改变所等待的条件,而条件变量可以解锁。
2.3.2 条件变量API
[1] 初始化条件变量
int pthread_cond_init(pthread_cond_t *restrict cond,
const pthread_condattr_t *restrict attr);
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
参数1:条件变量
参数2:条件变量的属性,一般设置为0
返回值:成功0 失败-1
[2] 阻塞等待一个条件变量
int pthread_cond_wait(pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex);
参数1:条件变量
参数2:互斥锁
备注:内部处理:1.线程被阻塞,然后解开互斥锁
2.等待条件,直到有线程向它发起通知
3.重新对互斥锁进行加锁操作
int pthread_cond_timewait(pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex,
const struct timespec *restrict abstime);
参数3:超时时间限制
返回值:成功0 失败-1
[3] 唤醒一个条件变量等待队列中的线程
int pthread_cond_signal(pthread_cond_t *cond);
参数1:条件变量
返回值:成功0 失败-1
[4] 唤醒所有条件变量等待队列中的线程
int pthread_cond_broadcast(pthread_cond_t *cond);
[5] 销毁条件变量
int pthread_cond_destroy(pthread_cond_t *cond);
返回值:成功0 失败-1
cond.c
#define FIFO_PATH "/home/gec/fifo_file"
int fd_fifo;
pthread_mutex_t mutex;
pthread_cond_t cond;
void *routine(void *arg)
{
pthread_cond_wait(&cond,&mutex);
char buf[100];
read(fd_fifo,buf,sizeof(buf));
puts(buf);
pthread_exit(NULL);
}
int main(int argc,char *argv[])
{
pthread_cond_init(&cond,NULL);
pthread_mutex_init(&mutex,NULL);
/*********create&&open fifo********/
if(access(FIFO_PATH,F_OK))
{
mkfifo(FIFO_PATH,O_CREAT|0666);
}
fd_fifo = open(FIFO_PATH,O_RDWR);
if(fd_fifo == -1)
{
perror("open");
exit(-1);
}
/*********create thread********/
pthread_t tid[5];
for(int i=0;i<5;i++)
{
pthread_create(&tid[i],NULL,routine,NULL);
}
/*******do something**********/
sleep(2);
/******write fifo && send cond***/
char buf[100] = "zzzz\niiii\nxxxx\n";
write(fd_fifo,buf,strlen(buf));
puts("write is ok");
pthread_cond_broadcast(&cond);
/*******destroy**************/
for(int i=0;i<5;i++)
{
pthread_join(tid[i],NULL);
}
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&cond);
close(fd_fifo);
return 0;
}
2.4 其他锁了解
2.4.1 自旋锁
- 自旋锁与互斥锁的区别:自旋锁阻塞后不会让出CPU,而是继续等待。
- 存在问题:1.过多占据CPU的事件。2.产生死锁问题。
- 自旋锁和互斥锁:
1 自旋锁和互斥锁都是为了实现保护资源共享的机制。
2 无论是自旋锁还是互斥锁,在任意时刻,最多只有一个保持者。
3 获得互斥锁的线程,如果锁已经被占用,则该线程将进入休眠状态;获取自旋锁的线程则不会睡眠,而是一直循环等待锁的释放。
2.4.2 递归锁
在同一线程该锁是可重入的,对于不同线程相当于是普通的互斥锁。
2.4.3 乐观锁和悲观锁
悲观锁适合于多写的场合。
乐观锁适合于多读的场合。