线程概念
线程实际上是应用层的概念,在Linux内核中,所有的调度实体都被称为任务(task),他们之间的区别是:有些任务自己拥有一套完整的资源,而有些任务彼此之间共享一套资源。
进程:进程是操作系统资源分配的基本实体
线程:线程是cpu调度的基本单位,是进程的内部资源,是linux中最小的资源单位
基本接口
2.1线程的创建
<br class="Apple-interchange-newline"><div></div>
#include <pthread.h>
int pthread_create(pthread_t *thread,
const pthread_attr_t *attr,
void *(*start_routine) (void *),
void *arg);
线程传参
demo
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
void *func(void *arg)
{
//printf("%s\n",(char *)arg);
int data = *((int *)arg);
printf("%d\n",data);
}
int main(int argc, char const *argv[])
{
pthread_t tid;
//pthread_create(&tid, NULL, func, (void *)"hello child");
int data = 100;
pthread_create(&tid, NULL, func, &data);
sleep(1);
return 0;
}
并发性
线程最重要的特性是并发,线程函数 doSomething()
会与主线程 main()
同时运行,这是它与普通函数调用的根本区别。需要特别提醒的是,由于线程函数的并发性,在线程中访问共享资源需要特别小心,因为这些共享资源会被多个线程争抢,形成“竞态”。
2.2线程的退出
#include <pthread.h>
void pthread_exit(void *retval);
主线程退出后,其余线程可以继续运行,但请注意,上述代码中如果主线程不调用 pthread_exit() 的话,那么相当于退出了整个进程,则子线程也会被迫退出。
2.3线程的接合
#include <pthread.h>
int pthread_join(pthread_t tid, void **val);
包括主线程在内,所有线程的地位是平等的,任何线程都可以先退出,任何线程也可以接合另外一条线程。
2.4其它
2.4.1获取线程TID
#include <pthread.h>
pthread_t pthread_self(void);
2.4.2线程错误码
线程函数对系统错误码的处理跟标准C库函数的处理方式有很大不同,标准C库函数会对全局错误码 errno
进行设置,而线程函数发生错误时会直接返回错误码。
2.4.3函数单例(一个实例)
许多时候,我们希望某个函数只被严格执行一次,这种需求在一些初始化功能模块中尤为常见。
,由于线程的并发特性,我们无法预先知晓哪条线程会对信号量进行初始化,于是就希望有一种只执行一遍的函数单例,可以被众多的并发线程放心去调用。
#include <pthread.h>
// 函数单例控制变量
pthread_once_t once_control = PTHREAD_ONCE_INIT;
// 函数单例启动接口
int pthread_once(pthread_once_t *once_control, void (*init_routine)(void));
线程的属性
1.1查看线程属性
1.2属性变量发使用
由于线程属性众多,因此需要的时候不直接设置,而是先将它们置入一个统一的属性变量中,然后再以此创建线程。属性变量是一种内置数据类型,需要用如下函数接口专门进行初始化和销毁:
#include <pthread.h>
int pthread_attr_init(pthread_attr_t *attr);
int pthread_attr_destroy(pthread_attr_t *attr);
2.分离属性
2.1僵尸线程
默认情况下,线程启动后处于可接合状态(即未分离),此时的线程可以在退出时让其他线程接合以便释放资源,但若其他线程未及时调用 pthread_join()
去接合它,它将成为僵尸线程,浪费系统资源。
2.2分离与接合
2.3线程启动后强制分离
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
void *func(void *arg)
{
// 分离线程属性
pthread_detach(pthread_self());
int count = 10;
while (count--)
{
printf("count : %d\n",count);
sleep(1);
}
pthread_exit("子线程退出");
}
int main(int argc, char const *argv[])
{
pthread_t tid;
pthread_create(&tid, NULL, func, NULL);
//sleep(1);
// 接合子线程
// 如果先执行接合函数后,那么设置分离属性则无效
void *val;
int ret = pthread_join(tid, &val);
if(ret != 0)
{
perror("接合失败:");
}
else
{
printf("接合成功:%s\n",(char *)val);
}
// 主线程退出,也会等待子线程先退出
pthread_exit(NULL);
return 0;
}
3.基本概念
互斥与同步是最基本的逻辑概念:
-
互斥指的是控制两个进度使之互相排斥,不同时运行。
-
同步指的是控制两个进度使之有先有后,次序可控。
4.互斥锁
4.1基本逻辑
使得多线程间互斥运行的最简单办法,就是增加一个互斥锁。任何一条线成要开始运行互斥区间的代码,都必须先获取互斥锁,而互斥锁的本质是一个二值信号量,因此当其中一条线程抢先获取了互斥锁之后,其余线程就无法再次获取了,效果相当于给相关的资源加了把锁,直到使用者主动解锁,其余线程方可有机会获取这把锁。
4.2函数接口
定义
互斥锁是一个特殊的变量,定义如下:
#include <pthread>
pthread_mutex_t m;
初始化与销毁 未经初始化的互斥锁是无法使用的,初始化互斥锁有两种办法:
静态初始化:pthread_mutex_t m = PTHREAD_MUTEX_INITIALIZER;
动态初始化:
#include <pthread.h>
// 初始化互斥锁
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
const pthread_mutexattr_t *restrict attr);
// 销毁互斥锁
int pthread_mutex_destroy(pthread_mutex_t *mutex);
加锁与解锁
#include <pthread.h>
pthread_mutex_t m = PTHREAD_MUTEX_INITIALIZER;
// 加锁
pthread_mutex_lock( &m );
// 解锁
pthread_mutex_unlock( &m );
5.读写锁
5.1基本逻辑
将对资源的读写操作加以区分:读操作可以多任务并发执行,只有写操作才进行恰当的互斥。
定义
与互斥锁类似,读写锁也是一种特殊的变量:
pthread_rwlock_t rw;
初始化:
#include <pthread.h>
// 静态初始化:
pthread_rwlock_t rw = PTHREAD_RWLOCK_INITIALIZER;
// 动态初始化与销毁:
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,
const pthread_rwlockattr_t *restrict attr);
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
加锁
-
如果只对数据进行读操作,那么就加 → 读锁。
-
如果要对数据进行写操作,那么就加 → 写锁。
// 读锁
// 1,阻塞版本
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
// 2,非阻塞版本
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
// 写锁
// 1,阻塞版本
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
// 2,非阻塞版本
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
//解锁
#include <pthread.h>
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
编译的时候记得加 -DRDLOCK 或者 -DWRLOCK
gcc 读写锁.c -pthread -DRDLOCK
./a.out
POSIX信号量
1.基本概念
POSIX信号量与IPC信号量组中的信号量元素的逻辑完全一样,但POSIX信号量操作更加简便,接口更加易用。在多进程多线程中运用广泛。
POSIX信号量分成两种:
-
POSIX匿名信号量
-
通常用在线程间
-
只存在于内存,在文件系统中不可见
-
-
POSIX具名信号量
-
通常用在进程间
-
存在于文件系统/dev/shm中,可被不同进程操作
-
2.POSIX匿名信号量
2.1定义
#include <semaphore.h>
sem_t s;
2.2初始化
#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);
2.3P/V操作
#include <semaphore.h>
int sem_wait(sem_t *sem); // P操作
int sem_post(sem_t *sem); // V操作
条件量的基本概念
在许多场合中,程序的执行通常需要满足一定的条件,条件不成熟的时候,任务应该进入睡眠阻塞等待,条件成熟时应该可以被快速唤醒。另外,在并发程序中,其他任务同时具备访问该条件,因此任何时候都必须以互斥的方式对条件进行访问。条件量就是专门解决上述场景的逻辑机制。
注意,上述表述中,条件和条件量是两个不同的东西,所谓条件就是指程序要继续运行所需要的前提条件,比如文件是否读完、内存是否清空等具体的场景限定,而条件量(即pthread_cond_t)是一种同步互斥变量,专用于解决上述逻辑场景。
说明:
-
在进行条件判断前,先加锁(防止其他任务并发访问)
-
成功加锁后,判断条件是否允许
-
若条件允许,则直接操作临界资源,然后释放锁
-
若条件不允许,则进入条件量的等待队列中睡眠,并同时释放锁
-
-
在条件量中睡眠的任务,可以被其他任务唤醒,唤醒时重新判定条件是否允许程序继续执行,当然也是必须先加锁。
2.条件量的使用
条件量一般要跟互斥锁(或二值信号量)配套使用,互斥锁提供锁住临界资源的功能,条件量提供阻塞睡眠和唤醒的功能。
// 单个唤醒,唤醒第一个进入条件量中睡眠的任务
pthread_cond_signal(&v);
// 集体唤醒,唤醒进入条件量中睡眠的所有任务
pthread_cond_broadcast(&v);
3.线程取消
#include <pthread.h>
int pthread_cancel(pthread_t thread);
注意:线程收到取消请求,就等价于提前退出。
设置线程响应取消的状态
include <pthread.h>
int pthread_setcancelstate(int state, int *oldstate);
3.取消点,设置线程响应取消类型
只有线程遇到取消点时才会退出,取消点接口 查阅 man 7 pthreads
fprintf()
fputc()
fputs()
sleep()
printf()
usleep()
设置取消点响应
#include <pthread.h>
int pthread_setcanceltype(int type, int *oldtype);
2.死锁概念
死锁指的是由于某种逻辑问题,导致等待一把永远无法获得的锁的困境。比如最简单的是同一线程,连续对同一锁资源进行加锁,就进入了死锁。