Linux(线程控制)
一 线程的操作
1. 创建线程:pthread_create
int pthread_create(pthread_t *thread, // 线程 idconst pthread_attr_t *attr, // 线程属性设置void *(*start_routine) (void *), // 回调函数void *arg // 传递给回调函数的参数);
// 返回值0成功,否则返回错误码
2. 等待线程:pthread_join
int pthread_join(pthread_t thread, // pthread_create 的返回值 void **retval // pthread_create 回调函数的返回值);
// 返回值成功0,否则返回错误码
和进程一样,线程执行完,也需要等待回收获取执行结果,否则类似僵尸进程。
示例:
#include <iostream>
#include <pthread.h>void* fun(void *arg)
{const char *s = static_cast<const char *>(arg);std::cout << s << std::endl;return (void *)"正常退出";
}
int main()
{pthread_t pid;pthread_create(&pid, nullptr, fun, (void *)"hello world");void *result;pthread_join(pid, &result);std::cout << static_cast<const char *>(result) << std::endl;return 0;
}
创建多线程,进程内部就有多个执行流,谁先执行不一定。
void* 可以接收任意类型参数,内置类型,自定义类型都可以。
二级指针存放回调函数的返回值(一级指针的地址)
3. 线程终止
#include <pthread.h>void pthread_exit(void *retval // 终止后的信息);
// 哪个线程调用终止哪个线程
如果不想正常return返回,可以调用 pthread_exit() ,提前终止,携带退出信息。
4. 线程分离
#include <pthread.h>int pthread_detach(pthread_t thread // 线程的PID);
// 让这个线程分离,此后不需要join
一般情况线程结束需要被等待,但可以自己分离出进程,但资源仍然共享,只是由系统来回收。
5. 取消线程
int pthread_cancel(pthread_t thread // 线程ID)
// 取消线程
可以主动取消一个线程,一般是由主线程来取消。
6. 封装原生API(简易)
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <functional>// 处理任务
using handler = std::function<void(void)>;class mythread
{
public:// 线程回调函数static void *fun(void *arg){mythread *mythis = static_cast<mythread *>(arg);// 处理任务mythis->_handler();return nullptr;}mythread(const std::string &name, handler ha) : _name(name), _handler(ha), _isdetach(false) {}~mythread() {}// 创建线程初始化tidbool start(){if (pthread_create(&_tid, nullptr, fun, this) != 0)return false;return true;}// 等待线程结束bool join(){if (_isdetach == true)return false;return pthread_join(_tid, nullptr) == 0;}// 分离线程bool setthread_detach(){_isdetach = true;return pthread_detach(_tid) == 0;}// 取消线程bool setthread_cancel(){return pthread_cancel(_tid) == 0;}pthread_t gettid() { return _tid; }private:std::string _name; // 线程名pthread_t _tid; // 线程tidhandler _handler; // 执行任务bool _isdetach; // 分离线程
};
二 用户级线程
前章说过,Linux中的线程是通过pthread库对轻量级进程的封装,使用前必须携带 -lpthread 链接这个库。那么用户级线程是如何封装的?用户级线程包含哪些属性?下面来看看
既然是库,和标准库,第三方库一样,也要映射到共享区,pthread库也不例外。
当调用pthread_create(),pthread库会在内部维护一个用户级线程结构,并和其他相同的结构组织起来。
当获取线程的TID的时候,也就是pthread_create第一个参数,就是用户级线程维护的线程的起始地址,不是内核轻量级进程的LWP字段,所以在线程操作的时候,实际是在对pthread库操作,pthread库封装的轻量级进程,对库做操作,库帮你对轻量级进程做操作。
pthread维护的结构,包含很多字段:PID/LWP,回调函数/函数参数,void*退出信息,独立的栈,TLS线程局部存储.....等。
线程局部存储:线程也可以给自己创建独立的对象,维护在pthread结构中,但仅只支持内置类型,不支持自定义类型: __thread 类型。
独立的栈:进程里的栈由主线程使用,即没有创建线程的那个线程使用,其他的线程也是在pthread结构里独立开辟空间并维护自己的栈,也就不会起冲突了。
三 同步与互斥
如果在多执行流对同一个变量进行操作会有什么问题?
假设对一个变量执行 -- 操作,当减到0结束,那么if()判断和变量--都是操作,假设变量当前值为1,此时有多个执行流同时执行if(),if里的条件在内存中,先从内存拿到CPU,在由CPU执行,已经是2步操作了,当某个执行流对变量进行--操作,此时因某种原因被切换,比如:时间片到了,后面有优先级更高的....等,其他判断if()条件的执行流判断完成,此时if()内的语句已经有多条执行流了,但变量当前值为1,切走的线程又回来了进行--,后面的线程已经进来了,也会--,所以会有数据不一致问题,由并发访问导致数据不一致问题,称为线程安全问题。
上述根本原因是if()是由多种操作和切换等方面导致多执行流执行中有中间状态,称为非原子操作。
1. 概念:
原子性:执行流执行一段代码没有中间过程称为原子的,这段代码可能形成一行汇编语句,也可能是多行,只要没有中间状态,也就是原子的。
共享资源/临界资源:多执行流共享一个对象,对象身为共享资源,对共享资源保护的资源,称为临界资源。
临界区:临界资源的代码,称为临界区,其他称为非临界区。
2. 互斥量/互斥锁:
为了保证在多线程情况下,因为并发允许导致对共享资源修改造成的数据不一致问题,提供了很多互斥机制。
互斥:访问临界区的代码的时候,只有一个执行流能访问,其他等待,所以相对于其他线程,进入临界区的线程是原子的,由原来的并发,在临界区中变成了串行访问。
API:
#include <pthread.h>int pthread_mutex_init(pthread_mutex_t *mutex, // 锁 const pthread_mutexattr_t *mutexattr // 锁的属性);int pthread_mutex_trylock(pthread_mutex_t *mutex // 申请锁失败返回);int pthread_mutex_destroy(pthread_mutex_t *mutex // 释放锁);int pthread_mutex_lock(pthread_mutex_t *mutex // 对锁进行加锁);int pthread_mutex_unlock(pthread_mutex_t *mutex // 对锁进行解锁);
如果锁是局部变量,则需要进行初始化和手动销毁。
全局对象则直接初始化,不用销毁:pthread_mutex_t mut = PTHREAD_MUTEX_INITIALIZER。
互斥锁特征:如果申请成功就往下执行,否则阻塞,等待后续锁被解锁被唤醒重新申请锁。
所以互斥锁加锁期间就是原子的。
互斥锁实现:
当对锁进行加锁的时候,CPU存在一个汇编指令:swap,exchange,作用是交换内存中的值和寄存器中的值,整个下来只有一行汇编语句,也就是原子的,而之前的++/--操作而是3条汇编指令。
首先,CPU会初始化寄存器里的值为0,对应上图第一行代码,执行完这时如果被切走。
第一:内存中的值没变,其他线程可以继续申请,如果申请到,内存中的值变了,切走的线程再回来,恢复自己的寄存器的里的值:0,交换内存中的值,此时假设内存中原来的值为1,因切走被其他线程交换,变为0,此时0和0交换,在进行if()判断,走else 阻塞等待。
第二:执行第二行代码被切走,此时内存中的值由初始1被交换到寄存器,变为0,线程切换保存寄存器的值,后续线程的寄存器初始化为0,和内存中被交换后的值:0,0和0交换,if()不成立,else阻塞。
第三:解锁重新交换内存中的值和寄存器的值,如果被切走,其他线程已经阻塞,新的线程可以申请,没被切走唤醒阻塞的线程继续申请锁。
示例:
#include "thread.hpp"
#include <vector>// pthread_mutex_t mymtu = PTHREAD_MUTEX_INITIALIZER;
int val = 10000;
void xx(std::string s)
{while (1){// pthread_mutex_lock(&mymtu);if (val > 0){std::cout << s << " :" << val-- << std::endl;// pthread_mutex_unlock(&mymtu);}else{// pthread_mutex_unlock(&mymtu);break;}}
}
int main()
{std::vector<mythread> v;for (int i = 0; i < 5; i++)v.emplace_back(std::to_string(i), xx);for (auto &e : v)e.start();for (auto &e : v)e.join();return 0;
}
因显示器本就是共享资源所以打印信息混乱正常,可以看到3号线程打印的val是负数,不加保护必定存在线程安全问题,所以要加锁进行保护。
3 同步/条件变量:
上面说的互斥只是让临界区只有一个线程可以访问。
同步:多个线程访问临界区有一定的顺序。
明显互斥也有顺序,但解锁的线程和阻塞的线程状态不一样,解锁的线程解锁完可以立即去申请锁,而阻塞的线程要先唤醒再去申请锁,这样就导致了一个线程解锁之后又能申请到锁,而后面的锁一直申请不到,导致的问题就是后面的线程干等,线程饥饿问题,也就是同步,但资源竞争不合理。
所以为了让资源竞争合理,又引入了一个锁,条件变量。
API:
// 全局不需要释放
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;// 初始化条件变量
int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *cond_attr);// 唤醒一个线程
int pthread_cond_signal(pthread_cond_t *cond);// 唤醒全部线程
int pthread_cond_broadcast(pthread_cond_t *cond);// 线程挂到条件变量中
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);// 释放条件变量
int pthread_cond_destroy(pthread_cond_t *cond);
使用和互斥锁差不多,条件变量作用就是让线程竞争资源具有有合理性,如果资源不就绪就挂到条件变量里(队列里),唤醒依次从队列头部取一个,也就保证了每个线程能合理竞争到资源。
当调用pthread_cond_wait()的时候,会释放互斥锁,当唤醒的时候,会弹出一个线程,并重新申请锁。
示例:
#include <iostream>
#include <pthread.h>
#include <unistd.h>pthread_mutex_t mymtu=PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t mycond=PTHREAD_COND_INITIALIZER;int val=0;
void* fun(void* arg)
{while(1){pthread_mutex_lock(&mymtu);pthread_cond_wait(&mycond,&mymtu);std::cout<<static_cast<const char*>(arg)<<std::endl;pthread_mutex_unlock(&mymtu);}return nullptr;
}
int main()
{pthread_t t1,t2,t3;pthread_create(&t1,nullptr,fun,(void*)"thread-1");pthread_create(&t2,nullptr,fun,(void*)"thread-2");pthread_create(&t3,nullptr,fun,(void*)"thread-3");while(1){std::cout<<"wake up"<<std::endl;pthread_cond_signal(&mycond);// pthread_cond_broadcast(&mycond);sleep(1);}pthread_join(t1,nullptr);pthread_join(t2,nullptr);pthread_join(t3,nullptr);return 0;
}
当加入条件变量控制线程资源的竞争,明显具有一定的顺序性,也就是同步。