多线程1:基础概念、接口介绍、锁
线程概念(了解就行
什么是线程
- 线程是“一个进程内部的控制序列”
- 线程在运行,本质是在进程地址空间内运行
线程和进程
线程接口
POSIX线程库
这些库函数大多以pthread_开头、要包含<pthread.h>头文件、编译的时候要加个 -lpthread 选项
创建线程
功能:创建一个新的线程
原型
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void*), void *arg);
参数
thread:返回线程ID
attr:设置线程的属性,attr为NULL表示使用默认属性
start_routine:是个函数地址,线程启动后要执行的函数
arg:传给线程启动函数的参数
返回值:成功返回0;失败返回错误码
(感觉这个函数的使用很抽象啊,我们来看看怎么用吧)
void *rout(void *arg) {
int i;
for( ; ; ) {
printf("I'am thread 1\n");
sleep(1);
}
}
int main( void )
{
pthread_t tid;
int ret;
if ( (ret=pthread_create(&tid, NULL, rout, NULL)) != 0 ) {
fprintf(stderr, "pthread_create : %s\n", strerror(ret));
exit(EXIT_FAILURE);
}
int i;
for(; ; ) {
printf("I'am main thread\n");
sleep(1);
}
}
主要就是对哪个线程进行操作(tid)、以及操作方法(rout)
线程终止
有三种方法:
- 线程函数中使用return,不能在主线程中使用
- 线程调用pthread_exit 终止自己
- 线程调用pthread_cancel 终止id的进程
功能:线程终止
原型
void pthread_exit(void *value_ptr);
参数
value_ptr:value_ptr不要指向一个局部变量。
返回值:无返回值,跟进程一样,线程结束的时候无法返回到它的调用者(自身)
注意: value_ptr不能指向局部变量,因为这是野指针
功能:取消一个执行中的线程
原型
int pthread_cancel(pthread_t thread);
参数
thread:线程ID
返回值:成功返回0;失败返回错误码
等待线程(感觉比较重要
等待的目的
线程结束后,被结束的这个线程还有资源没有完全释放,并且这块空间新的线程用不到了 (就是没用了,留着就是浪费),所以要自己释放完之后,再使用其他的线程
功能:等待线程结束
原型
int pthread_join(pthread_t thread, void **value_ptr);
参数
thread:线程ID
value_ptr:它指向一个指针,后者指向线程的返回值
返回值:成功返回0;失败返回错误码
调用该函数的线程将挂起等待,直到id为thread的线程终止
对于不同的终止情况,pthread_join获得的value_ptr指向的内容也不同(了解即可
- 如果thread线程通过return返回,value_ ptr所指向的单元里存放的是thread线程函数的返回值。
- 如果thread线程被别的线程调用pthread_ cancel异常终掉,value_ ptr所指向的单元里存放的是常数 PTHREAD_ CANCELED。
- 如果thread线程是自己调用pthread_exit终止的,value_ptr所指向的单元存放的是传给pthread_exit的参 数。
- 如果对thread线程的终止状态不感兴趣,可以传NULL给value_ ptr参数。
分离线程
新线程默认是joinable(可等待的),结束线程之后,需要调用pthread_join
如果不需要等待,可以将线程分离。
joinable和分离是冲突的,一个线程不能既是joinable又是分离的。
int pthread_detach(pthread_t thread);
线程异常
- 单个线程如果出现除零,野指针问题导致线程崩溃,进程也会随着崩溃 (线程崩溃,进程也崩溃)
- 线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该进程内的所有线程也就随即退出
互斥量mutex:锁
什么是锁,锁的作用
共享变量
变量在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完成线程之间的交互。
并发操作共享变量带来的问题
来看一段代码:
int ticket = 100;
void *route(void *arg)
{
char *id = (char*)arg;
while ( 1 ) {
if ( ticket > 0 ) {
usleep(1000);
printf("%s sells ticket:%d\n", id, ticket);
ticket--;
} else {
break;
}
}
}
int main( void )
{
pthread_t t1, t2, t3, t4;
pthread_create(&t1, NULL, route, "thread 1");
pthread_create(&t2, NULL, route, "thread 2");
pthread_create(&t3, NULL, route, "thread 3");
pthread_create(&t4, NULL, route, "thread 4");
pthread_join(t1, NULL);
pthread_join(t2, NULL);
pthread_join(t3, NULL);
pthread_join(t4, NULL);
return 0;
}
最后的运行结果:
thread 4 sells ticket:4
thread 1 sells ticket:3
thread 3 sells ticket:3
thread 2 sells ticket:1
thread 4 sells ticket:0
thread 1 sells ticket:-1
thread 3 sells ticket:-2
诶?我们发现最后ticket为负数,是什么导致了ticket为临界值时的多次减小
有以下几种可能:
- 进入if语句后,切换到其他的线程
- usleep(1000);模拟的是执行业务的过程,可能在执行业务的过程中,有多个进程访问这段代码
- ticket–并不是原子的操作
如果看过前面的文章的话,能快速判断出:ticket–并不是原子的操作!!(即并不是要么已完成,要么还没执行)
减减这个操作对应着至少三条汇编代码
- 将ticket赋值给寄存器
- 寄存器-1
- 将寄存器赋值给ticket
避免的方法
- 代码必须互斥:单个线程在临界区执行代码时,其他线程无法进入临界区
- 多个线程访问临界区的时候,只允许一个线程访问临界区
- 线程没有访问临界区的时候,不能阻止其他进程访问临界区
那么锁就可以满足这三点
锁的接口
初始化锁
- 静态初始化
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
//11中直接使用mutex创建锁即可
//std::mutex _mutex;
- 动态分配
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
//相当于
//pthread_mutex_t *mutex = malloc(sizeof(pthread_mutex_t));
//pthread_mutex_init(mutex, NULL);
参数:
mutex:要初始化的互斥量
attr:NULL
销毁锁
- 静态分配的锁不用手动销毁
- 加锁的锁不能销毁
- 销毁后,要确保之后不会有线程再尝试上锁,即一个线程在未解锁之前只能上一次锁,不然程序会卡死
加锁和解锁
int pthread_mutex_lock(pthread_mutex_t *mutex);
//_mutex.lock();
int pthread_mutex_unlock(pthread_mutex_t *mutex);
//_mutex.unlock();
返回值:成功返回0,失败返回错误号
改进代码
那么上面的ticket代码就可以改进为:
注意:加锁的目的就是将并行代码变为串行执行。
int ticket = 100;
pthread_mutex_t mutex;
void *route(void *arg)
{
char *id = (char*)arg;
while ( 1 ) {
pthread_mutex_lock(&mutex);
if ( ticket > 0 ) {
usleep(1000);
printf("%s sells ticket:%d\n", id, ticket);
ticket--;
pthread_mutex_unlock(&mutex);
} else {
pthread_mutex_unlock(&mutex);
break;
}
}
}
int main( void )
{
pthread_t t1, t2, t3, t4;
pthread_mutex_init(&mutex, NULL);
pthread_create(&t1, NULL, route, "thread 1");
pthread_create(&t2, NULL, route, "thread 2");
pthread_create(&t3, NULL, route, "thread 3");
pthread_create(&t4, NULL, route, "thread 4");
pthread_join(t1, NULL);
pthread_join(t2, NULL);
pthread_join(t3, NULL);
pthread_join(t4, NULL);
pthread_mutex_destroy(&mutex);
return 0;
}
被锁保护起来的代码是临界区
被保护的共享资源是临界资源
补充:RAII风格的锁
std::lock_guard<std::mutex> lock(d->_mutex);
//在d类内创建锁
//利用给的lock_guard来加锁
//就可以做到临时对象随着循环创建和销毁了(RAII)
补充基础
数据在内存中的时候,是共享的,所有线程都可以访问,
但在CPU内部寄存器的时候,数据就是这个线程私有的
锁的底层实现
我们知道++n和–n都是非原子的,所以不能用int类型记录锁。
体系结构中提供了swap和exchange指令,可以将寄存器和内存单元的数据直接交换,这就是原子的了
加锁解锁的伪代码大概是这样:
加索逻辑
- 将寄存器和mutex交换(exchange)
- 判断寄存器是否大于0
- 不满足则一直等待指导可以加锁,回到最开始、重新执行加索
小结
暂时写这么多,下一篇可以准备多线程了~