文章目录
![img](https://img-blog.csdnimg.cn/0048350e14a24a86842848cc843ace55.gif#pic_center)
Linux线程
1、线程的概念
- 线程是进程内部的一个执行分支,线程是CPU调度的基本单位(进程是资源分配的基本单位)。
- 一切进程至少都有一个执行线程
- 线程在进程内部运行,本质是在进程地址空间内运行
- 在Linux系统中,在CPU眼中,看到的PCB都要比传统的进程更加轻量化
- 透过进程虚拟地址空间,可以看到进程的大部分资源,将进程资源合理分配给每个执行流,就形成了线程执行流
2、线程的理解(以Linux为例)
先看下面这张图
Linux的设计者认为,进程和线程都是执行流,且具有极度的相似性,没必要设计单独的数据结构和算法。直接服用进程的代码,用进程来模拟线程!
根据上面的图,我们看到使用轻量级进程来模拟线程的,其中轻量级进程并没有创建新的地址空间,页表等,是直接使用PCB指向同一块地址空间(mm_struct)。windows系统是对线程重新设置了数据结构(TCB)和算法。
我们来对比一下进程和线程
从这里我们再认识一下进程:进程是承担资源分配的基本单位。
Linux中,所有的调度执行流都叫做轻量级进程!
结合地址空间,我们怎么理解多个执行流是如何进行代码划分的?
答:给不同的线程分配不同的区域,本质就是让不同的线程,看到各自的页表。
页表的内容看下面的图
3、线程的控制
3.1、线程函数的使用(线程创建)
使用pthread_create函数创建新线程。
参数thread是输出型参数,用来获取新线程的线程id(tid),attr是给新线程的属性(一半设置为nullptr),start_routine是新线程的执行流的入口函数(参数和返回值都是void*),arg是传递给这个入口函数的参数。
需要注意的是:在编译和链接代码的时候,得链接pthread库 — 即加上
-lpthread
。下面是该函数的使用:
#include <iostream> #include <unistd.h> #include <assert.h> #include <pthread.h> using namespace std; // 线程函数使用 void *newThread(void *arg) { while (1) { cout << "I am new thread ... ,pid : " << getpid() << endl; sleep(1); } } int main() { pthread_t tid; int n = pthread_create(&tid, nullptr, newThread, nullptr); assert(n == 0); while (1) { cout << "I am main thread ... ,pid : " << getpid() << endl; sleep(1); } return 0; }
运行结果:我们看到,当代码运行起来,只有一个进程!但是有两个线程(轻量级进程LWP – Light Weight Process)!这就验证了我们之前的所说的,线程是进程的一个执行分支!
查看线程(轻量级进程)的命令行:
ps -aL | head -1 && ps -aL | grep 可执行程序名
3.2、线程等待
使用pthread_join函数对线程进行等待
pthread_join函数对tid为thread的线程进行等待(直到该线程退出),retval是接收线程入口函数的返回值。如果线程入口函数正常退出,则retval就是该返回值,如果异常退出,则返回值是-1。
pthread_ create函数第一个参数指向一个虚拟内存单元,该内存单元的地址即为新创建线程的线程ID,属于NPTL线程库的范畴。线程库的后续操作,就是根据该线程ID来操作线程的。
线程库NPTL提供了pthread_ self函数,可以获得线程自身的ID。pthread_t 到底是什么类型呢?取决于实现。对于Linux目前实现的NPTL实现而言,pthread_t类型的线程ID,本质就是一个进程地址空间上的一个地址。
pthread_self函数获取当前线程ID。
该函数返回当前线程ID,该ID值和pthread_create函数(在使用该线程的时候)的第一个参数thread的值是一样的。
代码:
#include <iostream> #include <unistd.h> #include <pthread.h> using namespace std; string ToHex(pthread_t tid) { char id[64]; snprintf(id, sizeof(id), "0x%lx", tid); return id; } void *newThread(void *arg) { string threadname = static_cast<char *>(arg); int cnt = 5; while (cnt) { cout << "I am new thread, pid : " << getpid() << " , id : " << ToHex(pthread_self()) << endl; sleep(1); --cnt; } return nullptr; } int main() { pthread_t tid; int n = pthread_create(&tid, nullptr, newThread, (void *)"Thread-1"); cout << "I am main thread, pid : " << getpid() << " , tid : " << ToHex(tid) << " , id : " << ToHex(pthread_self()) << endl; // 线程等待 // pthread_join(tid, nullptr); sleep(1); cout << " main thread quit " << endl; return 0; }
运行结果:可以看到,没有使用pthread_join进行线程等待,当主线程执行完的时候,新线程还没运行完就退出了。
使用pthread_join进行线程等待,当新线程运行完之后,主线程才能退出。
为什么要线程等待?
已经退出的线程,其空间没有被释放,仍然在进程的地址空间内。
创建新的线程不会复用刚才退出线程的地址空间。
3.3、线程终止
使用pthread_exit可以使得线程提前退出,不会影响进程。
pthread_exit函数终止当前进程,并且通过retval返回给pthread_join的retval。
需要注意的是pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是用malloc分配,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了。
代码:
#include <iostream> #include <unistd.h> #include <pthread.h> using namespace std; string ToHex(pthread_t tid) { char id[64]; snprintf(id, sizeof(id), "0x%lx", tid); return id; } void *newThread(void *arg) { string threadname = static_cast<char *>(arg); cout << "I am new thread, name : " << threadname << " , pid : "<< getpid() << " , id : " << ToHex(pthread_self()) << endl; pthread_exit((void *)12); // 这里不正常退出,返回的就是(void) -1 } int main() { pthread_t tid; int n = pthread_create(&tid, nullptr, newThread, (void *)"Thread-1"); void *ret = nullptr; n = pthread_join(tid, &ret); // 返回值 cout << " main thread quit , ret : " << (long long)ret << " , n = " << n << endl; return 0; }
还可以通过主进程直接取消新进程,使用pthread_cancel函数
pthread_cancel可以取消线程名为thread的线程,如果该线程正常执行完,则并且会给pthread_join函数的retval设置为正常的返回值,否则就是-1。
代码:
#include <iostream> #include <unistd.h> #include <pthread.h> using namespace std; string ToHex(pthread_t tid) { char id[64]; snprintf(id, sizeof(id), "0x%lx", tid); return id; } void *newThread(void *arg) { string threadname = static_cast<char *>(arg); cout << "I am new thread, name : " << threadname << " , pid : " << getpid() << " , id : " << ToHex(pthread_self()) << endl; sleep(100); return (void *)123; } int main() { pthread_t tid; int n = pthread_create(&tid, nullptr, newThread, (void *)"Thread-1"); sleep(1); pthread_cancel(tid); void *ret = nullptr; n = pthread_join(tid, &ret); // 返回值 cout << " main thread quit , ret : " << (long long)ret << " , n = " << n << endl; sleep(1); return 0; }
运行结果:新线程没有运行完就退出了,返回值被设置为了-1。
如果线程出异常了,那么整个进程都会退出,因为异常会给进程发送信号,信号是进程专属,与线程无关。
代码:
#include <iostream> #include <unistd.h> #include <pthread.h> using namespace std; // 线程异常 string ToHex(pthread_t tid) { char id[64]; snprintf(id, sizeof(id), "0x%lx", tid); return id; } void *newThread(void *arg) { string threadname = static_cast<char *>(arg); cout << "I am new thread, name : " << threadname << " , pid : " << getpid() << " , id : " << ToHex(pthread_self()) << endl; // 异常 int *p = nullptr; *p = 1; return (void *)123; } int main() { pthread_t tid; int n = pthread_create(&tid, nullptr, newThread, (void *)"Thread-1"); sleep(1); pthread_cancel(tid); void *ret = nullptr; n = pthread_join(tid, &ret); // 返回值 cout << " main thread quit , ret : " << (long long)ret << " , n = " << n << endl; sleep(1); return 0; }
运行结果:整个进程都退出了。
3.3.1、创建多线程
代码:
#include <iostream> #include <unistd.h> #include <vector> #include <assert.h> #include <stdio.h> #include <pthread.h> using namespace std; const int N = 5; class Task { public: Task(int x = 1, int y = 1) : _x(x), _y(y) {} int Execute() { return _x + _y; } private: int _x; int _y; }; class ThreadData { public: ThreadData(int x, int y, string threadname) : _t(x, y), _threadname(threadname) {} string threadName() { return _threadname; } int run() { return _t.Execute(); } private: string _threadname; Task _t; }; class Result { public: void setResult(int result, string threadname) { _result = result; _threadname = threadname; } void Print() { cout << " result : " << _result << " , threadname : " << _threadname << endl; } private: int _result; string _threadname; }; void *newThread(void *arg) { ThreadData *td = static_cast<ThreadData *>(arg); string name = td->threadName(); Result *res = new Result(); // 这里得new ! int ret = td->run(); res->setResult(ret, name); sleep(1); delete td; return res; } // 创建多线程 int main() { vector<pthread_t> threads; for (int i = 0; i < N; ++i) { char threadname[64]; snprintf(threadname, sizeof(threadname), "Thread - %d", i); ThreadData *td = new ThreadData(10, i, threadname); pthread_t tid; pthread_create(&tid, nullptr, newThread, td); threads.push_back(tid); } vector<Result *> result_set; void *ret = nullptr; // 等待线程及获取返回值 for (auto &tid : threads) { pthread_join(tid, &ret); result_set.push_back((Result *)ret); } // 打印查看 for (auto &res : result_set) { res->Print(); delete res; } sleep(3); return 0; }
运行结果:
3.4、分离线程
默认情况下,新创建的线程是joinable(主线程需要等待)的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄漏。
如果不关心线程的返回值,joinable是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源。
注意:分离线程并不是该线程和主线程一点关系没有了,还是属于同一个进程,还是共享地址空间,并且主线程退出,新线程也会退出,只是主线程不必等待新线程,新线程自己执行完会主动释放资源。
使用pthread_detach可以对指定ID线程进行分离,当此线程退出时,自动释放线程资源。
代码:
#include <iostream> #include <unistd.h> #include <string> #include <pthread.h> using namespace std; // 线程是可以分离的 string ToHex(pthread_t tid) { char id[64]; snprintf(id, sizeof(id), "0x%lx", tid); return id; } void *newThread(void *arg) { // 线程是可以分离的,默认线程是joinable的 // 分离线程 1 分离线程并不是该线程和主线程一点关系没有了,还是属于同一个进程,还是共享地址空间,并且主线程退出,新线程也会退出, // 只是主线程不必等待新线程,新线程自己执行完会主动释放资源 // pthread_detach(pthread_self()); string threadname = static_cast<char *>(arg); int cnt = 5; while (cnt) { sleep(1); cout << "I am new thread, name : " << threadname << " , pid : " << getpid() << " , id : " << ToHex(pthread_self()) << endl; --cnt; } return (void *)123; } int main() { pthread_t tid; pthread_create(&tid, nullptr, newThread, (void *)"thread 1"); // 分离线程 2 pthread_detach(tid); int cnt = 10; while (cnt) { cout << "I am main thread , pid : " << getpid() << " , tid : " << ToHex(tid) << " , id : " << ToHex(pthread_self()) << endl; sleep(1); --cnt; } // 分离线程后不用等待新线程的死活 // 一般都是主线程永远最后退出或者死循环不退出! 无论线程数join还是detach cout << "main thread quit ..." << endl; return 0; }
运行结果:主线程来使用pthread_detach函数
新线程来使用pthread_detach函数
3.5、线程特性
3.5.1、线程特性
同一个进程下的线程。大部分资源是共享的,地址空间也是共享的。
线程独占的资源:线程的硬件上下文(CPU寄存器的值)(调度);线程的独立栈结构(常规运行)。
线程共享的资源:代码和全局数据,进程文件描述符表。
这里再看一下这个图,可以了解一下线程的独立栈结构和局部存储,后面会讲。
代码:
#include <iostream> #include <unistd.h> #include <pthread.h> using namespace std; // 同一个进程下的线程。大部分资源是共享的,地址空间也是共享的 int g_val = 100; string ToHex(pthread_t tid) { char id[64]; snprintf(id, sizeof(id), "0x%lx", tid); return id; } void *newThread(void *arg) { string threadname = static_cast<char *>(arg); int cnt = 5; while (cnt) { printf("new thread , g_val : %d , &g_val : %p \n", g_val, &g_val); // cout << " I am " << threadname << " thread , pid : " << getpid() << " , id :" << ToHex(pthread_self()) << endl; cout << "----------------------------------" << endl; ++g_val; sleep(1); --cnt; } return (void *)222; } int main() { pthread_t tid; int n = pthread_create(&tid, nullptr, newThread, (void *)"Thread-1"); int cnt = 10; while (cnt) { printf("main thread , g_val : %d , &g_val : %p \n", g_val, &g_val); // cout << " I am main thread , pid : " << getpid() << " , id :" << ToHex(pthread_self()) << " ,tid : " << ToHex(tid) << endl; cout << "----------------------------------" << endl; sleep(1); --cnt; } sleep(3); // pthread_cancel(tid); // 这会使得pthread_join的ret为-1 ,也就是tid这个线程不正常退出 void *ret = nullptr; n = pthread_join(tid, &ret); // 返回值 cout << " main thread quit , ret : " << (long long)ret << " , n = " << n << endl; return 0; }
运行结果:可以发现,两个线程看到的g_val是同一个g_val,地址相同,且可以看到g_val的变化。
3.5.2、线程独立栈结构
代码:
#include <iostream> #include <unistd.h> #include <pthread.h> using namespace std; // 线程独立栈结构 void *newThread(void *arg) { string threadname = static_cast<char *>(arg); int cnt =5; while (true) { sleep(1); cout << "I am new thread, name : " << threadname << ", cnt : " << cnt << " , &cnt : " << &cnt << endl; } return (void *)123; } int main() { pthread_t tid1; pthread_t tid2; pthread_create(&tid1, nullptr, newThread, (void *)"thread 1"); pthread_create(&tid2, nullptr, newThread, (void *)"thread 2"); pthread_join(tid1,nullptr); pthread_join(tid2,nullptr); cout << "main thread quit ..." << endl; return 0; }
运行结果:可以看到,不同的线程使用同一个线程入口函数(地址),里面的局部变量的值不一样(不共享)。
3.5.3、线程的局部存储
线程可以使用一个公共变量(只能是内置类型),在不同的线程可以对该变量进行独立存储(各线程一个同名变量)。
代码:
#include <iostream> #include <unistd.h> #include <pthread.h> using namespace std; // __thread可以给不同线程分配不同的局部变量 -- 其实也就是在各线程入口函数中省得再手动创建一个变量 -- 线程局部存储 __thread int g_val = 10; __thread uint64_t starttime = 0; void *newThread1(void *arg) { starttime = time(nullptr); string threadname = static_cast<char *>(arg); while (g_val) { sleep(1); cout << "I am new thread, name : " << threadname << " , startime : " << starttime << " , g_val : " << g_val << " , &g_val : " << &g_val << endl; } return (void *)123; } void *newThread2(void *arg) { sleep(3); starttime = time(nullptr); string threadname = static_cast<char *>(arg); while (g_val) { sleep(1); cout << "I am new thread, name : " << threadname << " , startime : " << starttime << " , g_val : " << g_val << " , &g_val : " << &g_val << endl; --g_val; } return (void *)123; } int main() { pthread_t tid1; pthread_t tid2; pthread_create(&tid1, nullptr, newThread1, (void *)"thread 1"); pthread_create(&tid2, nullptr, newThread2, (void *)"thread 2"); pthread_join(tid1, nullptr); pthread_join(tid2, nullptr); cout << "main thread quit ..." << endl; return 0; }
运行结果:可以看到,线程2要晚3秒启动,它的开始时间就比线程1少3,体现在同一个starttime。
3.6、C++11多线程使用
创建新线程使用thead库的thead构造函数即可,等待新线程使用join成员函数,分离线程使用detach成员函数。其实就是对Liunx多线程进行了封装。
代码:
#include <iostream> #include <unistd.h> #include <pthread.h> #include <thread> using namespace std; // C++11 多线程 void newThread(int num) { while (num) { cout << "I am new thread , num : " << num << endl; sleep(1); num--; } } int main() { thread t1(newThread, 4); thread t2(newThread, 5); thread t3(newThread, 6); // while (true) // { // cout << "I am main thread" << endl; // sleep(1); // } t1.join(); t2.join(); t3.join(); cout << "main thread quit ..." << endl; return 0; }
运行结果:
3.7、线程封装
代码:
Thread.hpp
文件#ifndef __THREAD_HPP__ #define __THREAD_HPP__ #include <iostream> #include <string> #include <unistd.h> #include <functional> #include <pthread.h> using namespace std; // 封装Linux线程 namespace ThreadModule { template <class T> using func_t = function<void(T&)>; // using func_t = function<void(T)>; // ThreadData* template <class T> class Thread { public: // /* ThreadData* */Thread(func_t<T> func, T data, const string& name = "default name") : _func(func), _data(data), _threadname(name), _stop(true) {} Thread(func_t<T> func, T& data, const string& name = "default name") : _func(func), _data(data), _threadname(name), _stop(true) {} void Execute() { _func(_data); } // 隐含this static void *threadroutine(void *arg) { Thread<T> *self = static_cast<Thread<T> *>(arg); self->Execute(); // _func(_data);// static 访问不了成员变量 return nullptr; } bool Start() { int n = pthread_create(&_tid, nullptr, threadroutine, this); if (!n) { _stop = false; return true; } else { return false; } } void Detach() { if (!_stop) { pthread_detach(_tid); } } void Join() { if (!_stop) { pthread_join(_tid, nullptr); } } string name() { return _threadname; } void Stop() { _stop = true; } private: pthread_t _tid; string _threadname; T& _data; // T _data; // ThreadData* func_t<T> _func; bool _stop; }; } // namespace ThreadModule #endif
使用封装的线程:
代码:创建10个线程,把g_val减到1。
#include <iostream> #include <unistd.h> #include <vector> #include <string> #include <pthread.h> #include "Thread.hpp" using namespace std; using namespace ThreadModule; // 使用封装的Thread const int N = 10; int g_val = 100; void Print(int &num) { while (num) { cout << "I am new thread , num : " << num-- << endl; sleep(1); } } int main() { vector<Thread<int>> threads; // 创建一批线程 for (int i = 0; i < N; ++i) { string name = "thread - " + to_string(i + 1); // 错误写法 // int val = 100;// 局部变量,出了for(下一次进for)就销毁 // threads.emplace_back(Print, val, name); threads.emplace_back(Print, g_val, name); } // 启动一批线程 for (auto &thread : threads) { thread.Start(); } // 等待一批线程 for (auto &thread : threads) { thread.Join(); cout << "wait thread done , name : " << thread.name() << endl; } cout << "main thread quit ..." << endl; return 0; }
运行结果:10个线程同时对g_val进行
--
,因为是引用,所以g_val所有线程共享使用。
使用封装的线程进行抢票(未加锁)
代码:
#include <iostream> #include <unistd.h> #include <vector> #include <string> #include <pthread.h> #include "Thread.hpp" using namespace std; using namespace ThreadModule; // 抢票 // 使用封装的Thread const int N = 4; int tickets = 10000; // 共享资源,没有被保护 void Route(int &rtickets) { while (rtickets) { // cout << "rtickets : " << rtickets << ", &rtickets : " << &rtickets << endl; if(rtickets > 0){ usleep(1000); // 1毫秒 printf("get tickets : %d \n",rtickets); --rtickets; }else{ break; } } } int main() { vector<Thread<int>> threads; // cout << "tickets : " << tickets << ", &tickes : " << &tickets << endl; // 创建一批线程 for (int i = 0; i < N; ++i) { string name = "thread - " + to_string(i + 1); threads.emplace_back(Route, tickets, name); } // 启动一批线程 for (auto &thread : threads) { thread.Start(); } // 等待一批线程 for (auto &thread : threads) { thread.Join(); cout << "wait thread done , name : " << thread.name() << endl; } cout << "main thread quit ..." << endl; return 0; }
运行结果:我们发现,抢票抢到了负数!这是因为没有对临界资源(这里是tickets)进行保护,在同一时间内,多个线程同时访问了临界资源,并且对其进行了修改,并且本身操作不是原子的(原子操作是指在多线程环境下,操作要么全部执行完毕,要么完全不执行,期间不会被其他线程中断)。
–tickets操作可以分解为以下步骤:
- 从内存读到CPU
- CPU内部执行–操作
- 写回内存
以此时tickets已经为1(内存中)的时候为例:问题就出在当多个线程同时进入该判断语句(if(tickets > 0))的时候,都是可以进入还条件语句中的,那么在第一个使用CPU的线程–后,tickets已经变为了0,第二个使用CPU的线程–后,tickets已经变为了-1,))的时候,都是可以进入还条件语句中的,那么在第一个使用CPU的线程–后,tickets已经变为了0,第三个使用CPU的线程–后,tickets已经变为了-2,再写回内存tickets就变为了-2。
解决方案:使用互斥锁访问临界资源(下面讲)!
3.8、线程互斥
前面使用不带互斥锁的抢票系统会出现抢到负数票的情况,下面使用互斥锁解决问题。
3.8.1、进程线程间的互斥相关背景概念
临界资源:临界资源是指那些在多线程环境中需要被保护的资源,因为多个线程可能同时访问和修改这些资源。
临界区:临界区是程序中的一段代码,这段代码访问了共享资源(比如全局变量或共享内存),且在同一时间只能被一个线程执行。
互斥:互斥是指确保在任意时刻只有一个线程可以进入临界区的机制。
原子性(后面讨论如何实现):原子性指的是操作的不可分割性,即操作要么全部执行完毕,要么完全不执行,中间不会被打断。
3.8.2、互斥量mutex
大部分情况,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量归属单个线程,其他线程无法获得这种变量。
但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完成线程之间的交互。
多个线程并发的操作共享变量,会带来一些问题(前面的抢票系统就是,出现抢到负数票的情况)。
为什么可能出现抢到负数票的情况?
- if 语句判断条件为真以后,代码可以并发的切换到其他线程
- usleep 这个模拟漫长业务的过程,在这个漫长的业务过程中,可能有很多个线程会进入该代码段
- –ticket 操作本身就不是一个原子操作
--
操作并不是原子操作,而是对应三条汇编指令:
- load :将共享变量ticket从内存加载到寄存器中
- update : 更新寄存器里面的值,执行-1操作
- store :将新值,从寄存器写回共享变量ticket的内存地址
要解决以上问题,需要做到三点:
- 代码必须要有互斥行为:当代码进入临界区执行时,不允许其他线程进入该临界区。
- 如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只能允许一个线程进入该临界区。
- 如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区。
要做到这三点,本质上就是需要一把锁。Linux上提供的这把锁叫互斥量。
3.8.2.1、互斥量的接口
命令行输入:
man pthread_mutex_init
注意:如果在你的系统上没有找到
pthread_mutex_init
的手册页,可能是因为手册页没有安装。你可以尝试安装相关的手册页。如果你使用的是基于 Debian 的系统(例如 Ubuntu),你可以安装manpages-posix-dev
包来获取 POSIX 手册页:sudo apt-get update sudo apt-get install manpages-posix-dev
安装完成后,你应该能够使用以下命令查看
pthread_mutex_init
的手册页:man pthread_mutex_init
如果你使用的是其他的 Linux 发行版,类似的包管理器命令也应该可以用来安装相关的手册页包。例如,在基于 Red Hat 的系统(如 Fedora 或 CentOS)上,你可以使用:
sudo yum install man-pages-posix
3.8.2.1.1、初始化互斥量
初始化互斥量有两个方法:
- 静态分配
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
- 动态分配
int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr); 参数: mutex:要初始化的互斥量 attr:NULL -- 属性,现在我们一般设置为nullptr
3.8.2.1.2、销毁互斥量
int pthread_mutex_destroy(pthread_mutex_t *mutex);
销毁互斥量需要注意:
- 使用 PTHREAD_ MUTEX_ INITIALIZER 初始化的互斥量不需要销毁
- 不要销毁一个已经加锁的互斥量
- 已经销毁的互斥量,要确保后面不会有线程再尝试加锁
3.8.2.1.3、互斥量加锁和解锁
int pthread_mutex_lock(pthread_mutex_t *mutex); int pthread_mutex_unlock(pthread_mutex_t *mutex);
调用 pthread_ lock 时,可能会遇到以下情况:
- 互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功
- 发起函数调用时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量,那么pthread_ lock调用会陷入阻塞(执行流被挂起),等待互斥量解锁(上一个线程执行完解锁互斥量的代码)。
3.8.2.2、互斥量的使用
改进上面抢票的代码
3.8.2.2.1、使用静态分配互斥量
代码:Thread构造传参数是int类型,不够优雅哈哈哈,代码的可维护性也不高。
#include <iostream> #include <unistd.h> #include <vector> #include <string> #include <pthread.h> #include "Thread.hpp" using namespace std; using namespace ThreadModule; // 抢票 // 使用封装的Thread -- 加锁 const int N = 4; int tickets = 10000; // 共享资源,没有被保护 pthread_mutex_t g_mutex = PTHREAD_MUTEX_INITIALIZER; // 初始化 void Route(int &rtickets) { while (rtickets) { pthread_mutex_lock(&g_mutex); // 加锁 if (rtickets > 0) { usleep(1000); // 1毫秒 printf("get tickets : %d \n", rtickets); pthread_mutex_unlock(&g_mutex); // 解锁 --rtickets; } else { pthread_mutex_unlock(&g_mutex); // 解锁 break; } } } int main() { vector<Thread<int>> threads; // 创建一批线程 for (int i = 0; i < N; ++i) { string name = "thread - " + to_string(i + 1); threads.emplace_back(Route, tickets, name); } // 启动一批线程 for (auto &thread : threads) { thread.Start(); } // 等待一批线程 for (auto &thread : threads) { thread.Join(); cout << "wait thread done , name : " << thread.name() << endl; } cout << "main thread quit ..." << endl; return 0; }
运行结果:
代码:这里我们使用自定义类型传入Thread类,使代码更美观和优雅。传入的是Thread*。因此我们的修改一下Thread类的模版参数。
Thread.hpp
文件#ifndef __THREAD_HPP__ #define __THREAD_HPP__ #include <iostream> #include <string> #include <unistd.h> #include <functional> #include <pthread.h> using namespace std; // 封装Linux线程 namespace ThreadModule { template <class T> // using func_t = function<void(T&)>; using func_t = function<void(T)>; // ThreadData* template <class T> class Thread { public: /* ThreadData* */Thread(func_t<T> func, T data, const string& name = "default name") : _func(func), _data(data), _threadname(name), _stop(true) {} // Thread(func_t<T> func, T& data, const string& name = "default name") : _func(func), _data(data), _threadname(name), _stop(true) {} void Execute() { _func(_data); } // 隐含this static void *threadroutine(void *arg) { Thread<T> *self = static_cast<Thread<T> *>(arg); self->Execute(); // _func(_data);// static 访问不了成员变量 return nullptr; } bool Start() { int n = pthread_create(&_tid, nullptr, threadroutine, this); if (!n) { _stop = false; return true; } else { return false; } } void Detach() { if (!_stop) { pthread_detach(_tid); } } void Join() { if (!_stop) { pthread_join(_tid, nullptr); } } string name() { return _threadname; } void Stop() { _stop = true; } private: pthread_t _tid; string _threadname; // T& _data; T _data; // ThreadData* func_t<T> _func; bool _stop; }; } // namespace ThreadModule #endif
testThread.cc
文件#include <iostream> #include <unistd.h> #include <vector> #include <string> #include <pthread.h> #include "Thread.hpp" using namespace std; using namespace ThreadModule; // 抢票 // 使用封装的Thread -- 加锁 -- 自定义类传入 const int N = 4; int tickets = 10000; // 共享资源,没有被保护 pthread_mutex_t g_mutex = PTHREAD_MUTEX_INITIALIZER; // 初始化 class ThreadData { public: ThreadData(int &ticket, string &name) : _tickets(tickets), _name(name), total(0) {} public: int &_tickets; string _name; int total; }; void Route(ThreadData *td) { while (td->_tickets) { pthread_mutex_lock(&g_mutex); // 加锁 if (td->_tickets > 0) { usleep(1000); // 1毫秒 printf("%s running , get tickets : %d \n", td->_name.c_str(),td->_tickets); pthread_mutex_unlock(&g_mutex); // 解锁 ++td->total; --td->_tickets; } else { pthread_mutex_unlock(&g_mutex); // 解锁 break; } } } int main() { vector<Thread<ThreadData *>> threads; vector<ThreadData *> datas; // 创建一批线程 for (int i = 0; i < N; ++i) { string name = "thread - " + to_string(i + 1); // ThreadData td;// 出了作用域就销毁 ThreadData *td = new ThreadData(tickets, name); threads.emplace_back(Route, td, name); datas.emplace_back(td); } // 启动一批线程 for (auto &thread : threads) { thread.Start(); } // 等待一批线程 for (auto &thread : threads) { thread.Join(); } // 查看线程获得的票数 for (auto &data : datas) { cout << data->_name << " : tickets is : " << data->total << endl; delete data; } cout << "main thread quit ..." << endl; return 0; }
运行结果:
3.8.2.2.2、使用动态分配互斥量
代码:ThreadData类新增一个互斥量_mutex,所有线程共享此互斥量,因此是类型是引用。这个版本还含有C++11的互斥量接口使用,自行测试。
#include <stdio.h> #include <thread> #include <string> #include <mutex> #include <pthread.h> #include "Thread.hpp" using namespace std; using namespace ThreadModule; // 抢票 // 使用封装的Thread -- 加锁 const int N = 4; int tickets = 10000; // 共享资源,没有被保护 // // C++ mutex版本 // class ThreadData // { // public: // ThreadData(int &ticket, string &name, mutex &mutex) : _tickets(tickets), _name(name), _mutex(mutex), total(0) {} // public: // int &_tickets; // string _name; // int total; // mutex &_mutex; // }; // pthread_mutex_t 版本 class ThreadData { public: ThreadData(int &ticket, string &name, pthread_mutex_t& mutex) : _tickets(tickets), _name(name), _mutex(mutex), total(0) {} public: int &_tickets; string _name; int total; pthread_mutex_t& _mutex; }; void Route(ThreadData *td) { while (td->_tickets) { // td->_mutex.lock(); // C++11 pthread_mutex_lock(&td->_mutex); // pthread_mutex_lock(&g_mutex); // 加锁 if (td->_tickets > 0) { usleep(1000); // 1毫秒 printf("%s running , get tickets : %d \n", td->_name.c_str(),td->_tickets); // td->_mutex.unlock(); // C++11 pthread_mutex_unlock(&td->_mutex); // pthread_mutex_unlock(&g_mutex); // 解锁 ++td->total; --td->_tickets; } else { // td->_mutex.unlock(); // C++11 pthread_mutex_unlock(&td->_mutex); // pthread_mutex_unlock(&g_mutex); // 解锁 break; } } } int main() { vector<Thread<ThreadData *>> threads; vector<ThreadData *> datas; // 初始化mutex pthread_mutex_t mutex; pthread_mutex_init(&mutex, nullptr); // mutex mutex; // 创建一批线程 for (int i = 0; i < N; ++i) { string name = "thread - " + to_string(i + 1); // ThreadData td;// 出了作用域就销毁 ThreadData *td = new ThreadData(tickets, name, mutex); threads.emplace_back(Route, td, name); datas.emplace_back(td); } // 启动一批线程 for (auto &thread : threads) { thread.Start(); } // 等待一批线程 for (auto &thread : threads) { thread.Join(); } // 查看线程获得的票数 for(auto& data : datas){ cout << data->_name << " : tickets is : " << data->total << endl; delete data; } cout << "main thread quit ..." << endl; return 0; }
运行结果:
代码:RAII思想版本,不用自己释放锁,出了while循环作用域自动释放!也含有C++11的RAII思想的互斥量接口,自行测试。
LockGuard.hpp
文件#include <pthread.h> class LockGuard { public: LockGuard(pthread_mutex_t *mutex) : _mutex(mutex) { pthread_mutex_lock(_mutex); // 构造加锁 } ~LockGuard() { pthread_mutex_unlock(_mutex); // 析构解锁 } private: pthread_mutex_t *_mutex; };
testThread.cc
文件#include <iostream> #include <unistd.h> #include <vector> #include <assert.h> #include <stdio.h> #include <thread> #include <string> #include <mutex> #include <pthread.h> #include "Thread.hpp" #include "LockGuard.hpp" using namespace std; using namespace ThreadModule; // 抢票 // 使用封装的Thread -- 加锁 -- RAII const int N = 4; int tickets = 10000; // 共享资源,没有被保护 // // C++ mutex版本 // class ThreadData // { // public: // ThreadData(int &ticket, string &name, mutex &mutex) : _tickets(tickets), _name(name), _mutex(mutex), total(0) {} // public: // int &_tickets; // string _name; // int total; // mutex &_mutex; // }; // pthread_mutex_t 版本 class ThreadData { public: ThreadData(int &ticket, string &name, pthread_mutex_t& mutex) : _tickets(tickets), _name(name), _mutex(mutex), total(0) {} public: int &_tickets; string _name; int total; pthread_mutex_t& _mutex; }; void Route(ThreadData *td) { while (td->_tickets) { // C++11 // lock_guard<mutex> lock(td->_mutex); LockGuard guard(&td->_mutex); // RAII 思想,出了while作用域,自动解锁 if (td->_tickets > 0) { usleep(1000); // 1毫秒 printf("%s running , get tickets : %d \n", td->_name.c_str(),td->_tickets); ++td->total; --td->_tickets; } else { break; } } } int main() { vector<Thread<ThreadData *>> threads; vector<ThreadData *> datas; // 初始化mutex pthread_mutex_t mutex; pthread_mutex_init(&mutex, nullptr); // mutex mutex; // 创建一批线程 for (int i = 0; i < N; ++i) { string name = "thread - " + to_string(i + 1); // ThreadData td;// 出了作用域就销毁 ThreadData *td = new ThreadData(tickets, name, mutex); threads.emplace_back(Route, td, name); datas.emplace_back(td); } // 启动一批线程 for (auto &thread : threads) { thread.Start(); } // 等待一批线程 for (auto &thread : threads) { thread.Join(); } // 查看线程获得的票数 for(auto& data : datas){ cout << data->_name << " : tickets is : " << data->total << endl; delete data; } cout << "main thread quit ..." << endl; return 0; }
运行结果:
3.8.3、互斥量实现原理
经过上面的例子,我们已经意识到单纯的 i++ 或者 ++i 都不是原子的,有可能会有数据一致性问题为了实现互斥锁操作,大多数体系结构都提供了swap或exchange指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性,即使是多处理器平台,访问内存的总线周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。 现在我们把lock和unlock的伪代码改一下。
以当前mutex已经为1为例:CPU存该类型变量(mutex)的%al寄存器的值初始为0,内存的mutex为1,当来第一个线程,此时交换寄存器和内存中mutex的值,那么内存中的mutex的值为0,CPU寄存器的muext的值为1,那么if语句就成立,就加锁了(当解锁完再把1交给内存中的mutex),那么第二个线程来的时候,CPU存该类型变量(mutex)的寄存器的值初始为0(还上一个线程没解锁),此时内存中的mutex还是0,和内存中的mutex交换,那么寄存器的mutex还是0,if语句不成立,则挂起等待。
注意:CPU寄存器硬件只有一套,但是CPU内部寄存器的数据,也就是数据线程的硬件上下文。数据在内存里,所有线程都能访问,属于共享的,但是如果转移到CPU内部的寄存器中,就属于一个线程私有。
3.9、线程同步
3.9.1、条件变量
当一个线程互斥地访问某个变量时,它可能发现在其它线程改变状态之前,它什么也做不了。
例如一个线程访问队列时,发现队列为空,它只能等待,只能等到其它线程将一个节点添加到队列中,该线程才能被唤醒。这种情况就需要用到条件变量。
3.9.1.1、同步和竟态条件
同步:在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题,叫做同步。
竞态条件:因为时序问题,而导致程序异常,我们称之为竞态条件。
3.9.1.2、条件变量的接口
3.9.1.2.1、初始化条件变量
和互斥量一样,初始化条件变量有两个方法。
- 静态分配
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
- 动态分配
int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr); 参数: cond:要初始化的条件变量 attr:设置属性,一般为nullptr
3.9.1.2.2、销毁条件变量
int pthread_cond_destroy(pthread_cond_t *cond);
3.9.1.2.3、等待条件变量
int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex); 参数: cond:要在这个条件变量上等待 mutex:互斥量,某线程在该条件变量下等待的时候,得把锁释放。如果不释放,其他线程不能同时在该条件变量下等待,其他消费线程也消费不了
3.9.1.2.4、唤醒条件变量
- 唤醒某一个条件变量下的一个线程:
int pthread_cond_signal(pthread_cond_t *cond);
- 唤醒某一个条件变量下的所有线程:
int pthread_cond_broadcast(pthread_cond_t *cond);
3.9.1.3、条件变量的使用
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <pthread.h> pthread_cond_t cond; pthread_mutex_t mutex; void *r1(void *arg) { while (1) { pthread_cond_wait(&cond, &mutex); printf("活动\n"); } } void *r2(void *arg) { while (1) { pthread_cond_signal(&cond); sleep(1); } } int main(void) { pthread_t t1, t2; pthread_cond_init(&cond, NULL); pthread_mutex_init(&mutex, NULL); pthread_create(&t1, NULL, r1, NULL); pthread_create(&t2, NULL, r2, NULL); pthread_join(t1, NULL); pthread_join(t2, NULL); pthread_mutex_destroy(&mutex); pthread_cond_destroy(&cond); return 0; }
3.9.2、生产者消费者模型
321原则:
- 3种关系:
- 生产者 vs 生产者:互斥 (也可以有同步关系)
- 生产者 vs 消费者:互斥(也可以有同步关系)
- 消费者 vs 消费者:同步&&互斥
2个角色:生产者和消费者。
一个交易场所(超市)。
多生产者多消费者满足上述关系,单生产者单消费者没有生产者 vs 生产者的互斥和生产者 vs 消费者的互斥。
超市是共享资源,也是临界资源,是临时保存数据的内存空间,是某种数据结构(一般是使用队列)。
3.9.2.1、生产者消费者模型的优势
生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。这个阻塞队列就是用来给生产者和消费者解耦的。
优点:解耦、支持并发、支持忙闲不均
3.9.2.2、基于BlockingQueue的生产者消费者模型
在多线程编程中阻塞队列(Blocking Queue)是一种常用于实现生产者和消费者模型的数据结构。其与普通的队列区别在于,当队列为空时,从队列获取元素的操作将会被阻塞,直到队列中被放入了元素;当队列满时,往队列里存放元素的操作也会被阻塞,直到有元素被从队列中取出(以上的操作都是基于不同的线程来说的,线程在对阻塞队列进程操作时会被阻塞)。
3.9.2.3、模拟阻塞队列的生产消费模型
Main.cc
文件#include <iostream> #include <vector> #include <string> #include "Thread.hpp" #include "Task.hpp" #include "BlockQueue.hpp" using namespace ThreadModule; // BlockQueue里面存Task using blockqueue_t = BlockQueue<Task>; void Productor(blockqueue_t &bq, string &name) { // sleep(5); while (true) { srand(time(nullptr) ^ pthread_self()); int a = rand() % 10; int b = rand() % 20; Task t(a, b); bq.Enqueue(t); cout << name << " : " << t.stringDebug() << endl; sleep(1); } } void Consumer(blockqueue_t &bq, string &name) { sleep(5); while (true) { sleep(1); Task t; bq.Pop(&t); cout << name << " : " << t.stringResult() << endl; sleep(1); } } void StartComm(vector<Thread<blockqueue_t>> &threads, int num, blockqueue_t &bq, func_t<blockqueue_t> func, const char *name) { for (int i = 0; i < num; ++i) { string threadname = string(name) + to_string(i + 1); threads.emplace_back(func, bq, threadname); // threads.back().Start();// 有问题,重入threads问题 // sleep(1); } } void StartProductor(vector<Thread<blockqueue_t>> &threads, int num, blockqueue_t &bq) { StartComm(threads, num, bq, Productor, "Productor Thread - "); } void StartConsumer(vector<Thread<blockqueue_t>> &threads, int num, blockqueue_t &bq) { StartComm(threads, num, bq, Consumer, "Consumer Thread - "); } void AllStart(vector<Thread<blockqueue_t>> &threads) { for (auto &thread : threads) { cout << "start : " << thread.name() << endl; thread.Start(); } } void WaitAllThread(vector<Thread<blockqueue_t>> &threads) { for (auto &thread : threads) { thread.Join(); } } int main() { blockqueue_t *bq = new blockqueue_t(5); // 阻塞队列,容量5,相当于超市 vector<Thread<blockqueue_t>> threads; StartProductor(threads, 4, *bq); StartConsumer(threads, 3, *bq); AllStart(threads); WaitAllThread(threads); return 0; }
BlockQueue.hpp
文件#ifndef __BLOCK_QUEUE_HPP__ #define __BLOCK_QUEUE_HPP__ #include <queue> template <class T> class BlockQueue { private: bool IsFull() { return _block_queue.size() == _cap; } bool IsEmpty() { return _block_queue.size() == 0; } public: BlockQueue(int cap) : _cap(cap) { // 一开始有0个生产者等待,0个消费者等待 _product_wait_num = 0; _consume_wait_num = 0; pthread_mutex_init(&_mutex, nullptr); pthread_cond_init(&_product_cond, nullptr); pthread_cond_init(&_consume_cond, nullptr); } void Enqueue(const T &in) { // 生产者 pthread_mutex_lock(&_mutex); // 不能用if,保证代码的健壮性 // 考虑一种情况 :只有1个生产者,有5个消费者 // 此时没有数据,消费者先启动,那么5个消费者在条件变量下等待。 // 然后生产者生产了1个数据,然后生产者broadcast唤醒了所有消费者 // 此时有一个消费者竞争mutex锁成功,其他4个消费者在mutex锁下等待(所有消费者线程已唤醒,不再在条件变量下等待) // 那么当这个消费者消费完释放mutex锁后,这个等待的4个消费者中的一个消费者拿到锁,直接向下执行了!而此时队列已经没有数据了!那么就会出错。 while (IsFull()) { // 生产满了,就得等 // 1. pthread_cond_wait调用是: a. 让调用线程等待 b. 自动释放曾经持有的_mutex锁以便于其他调用线程使用 // c. 当条件满足,线程唤醒,pthread_cond_wait要求线程必须重新竞争_mutex锁,竞争成功,方可返回!!! // 因为如果可以直接不竞争锁向下执行,那么被唤醒的多个线程可能会同时访问临界区!!! // 之前:安全 ++_product_wait_num; pthread_cond_wait(&_product_cond, &_mutex); // 只要等待,必定会有唤醒,唤醒的时候,就要继续从这个位置向下运行!! --_product_wait_num; // 之后:安全 } _block_queue.push(in); // 这个时候最少有一个数据,叫消费者者来消费 if (_consume_wait_num > 0) // 没有等待的消费者就不用通知 pthread_cond_signal(&_consume_cond); // pthread_cond_broadcast(&_consume_cond); pthread_mutex_unlock(&_mutex); // pthread_cond_signal(&_consume_cond); // 也可以 } void Pop(T *out) { // 消费者 pthread_mutex_lock(&_mutex); // 不能用if,保证代码的健壮性 // 考虑一种情况 :只有1个生产者,有5个消费者 // 此时没有数据,消费者先启动,那么5个消费者在条件变量下等待。 // 然后生产者生产了1个数据,然后生产者broadcast唤醒了所有消费者 // 此时有一个消费者竞争mutex锁成功,其他4个消费者在mutex锁下等待(所有消费者线程已唤醒,不再在条件变量下等待) // 那么当这个消费者消费完释放mutex锁后,这个等待的4个消费者中的一个消费者拿到锁,直接向下执行了!而此时队列已经没有数据了!那么就会出错。 while (IsEmpty()) { // 吃完了,就得等 // 1. pthread_cond_wait调用是: a. 让调用线程等待 b. 自动释放曾经持有的_mutex锁以便于其他调用线程使用 ++_consume_wait_num; pthread_cond_wait(&_consume_cond, &_mutex); --_consume_wait_num; } *out = _block_queue.front(); _block_queue.pop(); // 这个时候最少有一个空间,叫生产者来生产 if(_product_wait_num > 0) // 没有生产者等待就不用通知 pthread_cond_signal(&_product_cond); pthread_mutex_unlock(&_mutex); } ~BlockQueue() { pthread_mutex_destroy(&_mutex); pthread_cond_destroy(&_product_cond); pthread_cond_destroy(&_consume_cond); } private: queue<T> _block_queue; pthread_mutex_t _mutex; // 互斥访问阻塞队列 pthread_cond_t _product_cond; // 生产者条件变量 pthread_cond_t _consume_cond; // 消费者条件变量 int _cap; int _product_wait_num; int _consume_wait_num; }; #endif
Task.hpp
文件#pragma once #include <string> using namespace std; class Task { public: Task(int a = 0, int b = 0) : _a(a), _b(b), _result(0) { _result = _a + _b; } int Result() { return _result; } string stringResult() { return to_string(_a) + " + " + to_string(_b) + " = " + to_string(_result); } string stringDebug() { return to_string(_a) + " + " + to_string(_b) + " = ? "; } private: int _a; int _b; int _result; };
Thread.hpp
文件#ifndef __THREAD_HPP__ #define __THREAD_HPP__ #include <iostream> #include <string> #include <unistd.h> #include <functional> #include <pthread.h> using namespace std; // 封装Linux线程 namespace ThreadModule { // template <class T> // using func_t = function<void(T &)>; // using func_t = function<void(T)>; // ThreadData* template <class T> using func_t = function<void(T &, string&)>; template <class T> class Thread { public: // /* ThreadData* */Thread(func_t<T> func, T data, const string& name = "default name") : _func(func), _data(data), _threadname(name), _stop(true) {} Thread(func_t<T> func, T &data, const string &name = "default name") : _func(func), _data(data), _threadname(name), _stop(true) {} void Execute() { _func(_data, _threadname); // _func(_data); } // 隐含this static void *threadroutine(void *arg) { Thread<T> *self = static_cast<Thread<T> *>(arg); self->Execute(); // static 访问不了成员变量 return nullptr; } bool Start() { int n = pthread_create(&_tid, nullptr, threadroutine, this); if (!n) { _stop = false; return true; } else { return false; } } void Detach() { if (!_stop) { pthread_detach(_tid); } } void Join() { if (!_stop) { pthread_join(_tid, nullptr); } } string name() { return _threadname; } void Stop() { _stop = true; } // ~Thread() {} private: pthread_t _tid; string _threadname; T &_data; // T _data; // ThreadData* func_t<T> _func; bool _stop; }; } // namespace ThreadModule #endif
运行结果:
3.10、POSIX信号量
POSIX信号量和SystemV信号量作用相同,都是用于同步操作,达到无冲突的访问共享资源目的。 但POSIX可以用于线程间同步。
3.10.1、初始化信号量
int sem_init(sem_t *sem, int pshared, unsigned int value); 参数: sem:信号量 pshared:0表示线程间共享,非0表示进程间共享 value:信号量初始值
3.10.2、销毁信号量
int sem_destroy(sem_t *sem);
3.10.3、等待信号量
int sem_wait(sem_t *sem); 等待信号量,会将信号量的值减1
3.10.4、发布信号量
int sem_post(sem_t *sem); 发布信号量,表示资源使用完毕,可以归还资源了。将信号量值加1。
3.10.5、信号量的优势
信号量(Semaphore)是一种用于控制对共享资源访问的同步机制。在并发编程中,信号量的使用可以带来多种优势,特别是在涉及资源的预定和管理时。以下是使用信号量的主要优势:
控制资源访问:信号量允许程序员精确控制对共享资源的访问次数。例如,如果有一个资源可以被多个线程共享,但同时只能有一定数量的线程访问它,信号量可以有效地管理这些访问次数。
避免竞争条件:通过使用信号量,可以避免多个线程同时访问共享资源而导致的竞争条件。这有助于维护数据的一致性和正确性。
提高系统性能:信号量通过协调线程对资源的访问,减少了资源的争用,降低了系统的负载和等待时间,从而提高了系统的性能。
解决生产者-消费者问题:信号量可以很好地解决经典的生产者-消费者问题。在这种问题中,生产者生产数据,消费者消费数据,信号量可以用来同步生产者和消费者之间的操作,确保数据不被重复消费或遗漏。
实现复杂同步逻辑:信号量可以实现比互斥锁(mutex)更复杂的同步逻辑。例如,计数信号量可以允许多个线程同时访问同一个资源,直到某个限制为止,而互斥锁通常只允许一个线程访问资源。
3.10.5、基于环形队列的生产消费模型
在数据结构中,我们有学过环形队列。有两个指针,分别是front和rear。
front指向队头元素,rear指向队尾元素的后一个位置。
有两种方法来对环形队列判空和判满:
- 不使用计数器,牺牲一个队列空间,当front和rear指向同一个位置队列为空;当rear的下一个位置是front则为满。
- 使用计数器,当front和rear指向同一个位置时计数器为0则队列为空,计数器为队列大小则为满。
这里我们使用第二种策略。
使用信号量来实现多生产者多消费者的代码:
Main.cc
文件#include <iostream> #include <vector> #include <string> #include <pthread.h> #include "Thread.hpp" #include "Task.hpp" #include "RingQueue.hpp" using namespace ThreadModule; using namespace std; // Thread传入自定义类型Task using ringqueue = RingQueue<Task>; void Productor(ringqueue &rq, string &name) { // sleep(5); int cnt = 10; while (true) { srand(time(nullptr) ^ pthread_self()); int a = rand() % 10; int b = rand() % 20; Task t(a, b); rq.Enqueue(t); cout << name << " : " << t.stringDebug() << endl; sleep(1); } } void Consumer(ringqueue &rq, string &name) { sleep(5); while (true) { Task t; rq.Pop(&t); cout << name << " : " << t.stringResult() << endl; sleep(1); } } void StartComm(vector<Thread<ringqueue>> &threads, int num, ringqueue &rq, func_t<ringqueue> func, const char *name) { for (int i = 0; i < num; ++i) { string threadname = string(name) + to_string(i + 1); threads.emplace_back(func, rq, threadname); } } void StartProductor(vector<Thread<ringqueue>> &threads, int num, ringqueue &rq) { StartComm(threads, num, rq, Productor, "Productor Thread - "); } void StartConsumer(vector<Thread<ringqueue>> &threads, int num, ringqueue &rq) { StartComm(threads, num, rq, Consumer, "Consumer Thread - "); } void AllStart(vector<Thread<ringqueue>> &threads) { for (auto &thread : threads) { // cout << "start : " << thread.name() << endl; thread.Start(); } } void WaitAllThread(vector<Thread<ringqueue>> &threads) { for (auto &thread : threads) { thread.Join(); } } int main() { ringqueue *rq = new ringqueue(5); // 阻塞队列,容量5,相当于超市 vector<Thread<ringqueue>> threads; StartProductor(threads, 4, *rq); StartConsumer(threads, 3, *rq); AllStart(threads); WaitAllThread(threads); return 0; }
Thread.hpp
文件#ifndef __THREAD_HPP__ #define __THREAD_HPP__ #include <iostream> #include <string> #include <unistd.h> #include <functional> #include <pthread.h> using namespace std; // 封装Linux线程 namespace ThreadModule { // template <class T> // using func_t = function<void(T &)>; // using func_t = function<void(T)>; // ThreadData* template <class T> using func_t = function<void(T &, string&)>; template <class T> class Thread { public: // /* ThreadData* */Thread(func_t<T> func, T data, const string& name = "default name") : _func(func), _data(data), _threadname(name), _stop(true) {} Thread(func_t<T> func, T &data, const string &name = "default name") : _func(func), _data(data), _threadname(name), _stop(true) {} void Execute() { _func(_data, _threadname); // _func(_data); } // 隐含this static void *threadroutine(void *arg) { Thread<T> *self = static_cast<Thread<T> *>(arg); self->Execute(); // static 访问不了成员变量 return nullptr; } bool Start() { int n = pthread_create(&_tid, nullptr, threadroutine, this); if (!n) { _stop = false; return true; } else { return false; } } void Detach() { if (!_stop) { pthread_detach(_tid); } } void Join() { if (!_stop) { pthread_join(_tid, nullptr); } } string name() { return _threadname; } void Stop() { _stop = true; } // ~Thread() {} private: pthread_t _tid; string _threadname; T &_data; // T _data; // ThreadData* func_t<T> _func; bool _stop; }; } // namespace ThreadModule #endif
RingQueue.hpp
文件#pragma once #include <semaphore.h> // 单生产,单消费 // 多生产,多消费 // "321": // 3: 三种关系 // a: 生产和消费互斥和同步 // b: 生产者之间: // c: 消费者之间: // 解决方案:加锁 // 1. 需要几把锁?2把 // 2. 如何加锁? template <typename T> class RingQueue { void P(sem_t &sem) { sem_wait(&sem); } void V(sem_t &sem) { sem_post(&sem); } void Lock(pthread_mutex_t &_mutex) { pthread_mutex_lock(&_mutex); } void UnLock(pthread_mutex_t &_mutex) { pthread_mutex_unlock(&_mutex); } public: RingQueue(int cap) : _cap(cap), _ring_queue(cap), _productor_index(0), _consumer_index(0) { sem_init(&_room_sem, 0, _cap); // 生产者 -- 开始空间是满的 sem_init(&_data_sem, 0, 0); // 消费者 -- 开始数据是没有的 pthread_mutex_init(&_procductor_mutex, nullptr); pthread_mutex_init(&_consumer_mutex, nullptr); } void Enqueue(const T &in) { // 这里生产者和消费者存在的同步关系只在环形队列为满或者为空的时候,但是因为有信号量的保护,可以在这两种情况直接实现同步(因为信号量为0的时候会等待) P(_room_sem); // 预定机制,此时必定有一个空间可以放数据 Lock(_procductor_mutex); _ring_queue[_productor_index++] = in; _productor_index %= _cap; UnLock(_procductor_mutex); V(_data_sem); // 生产好了一个数据 } void Pop(T *out) { P(_data_sem); // 预定机制,此时必定有一个数据可以消费 Lock(_consumer_mutex); *out = _ring_queue[_consumer_index++]; // 覆盖式的使用环形队列 _consumer_index %= _cap; UnLock(_consumer_mutex); V(_room_sem); // 释放一个空间 } ~RingQueue() { sem_destroy(&_room_sem); sem_destroy(&_data_sem); pthread_mutex_destroy(&_procductor_mutex); pthread_mutex_destroy(&_consumer_mutex); } private: vector<T> _ring_queue; int _cap; // 容量上限 int _productor_index; // 生产者的下标 int _consumer_index; // 消费者的下标 // 信号量 sem_t _room_sem; // 生产者关心 sem_t _data_sem; // 消费者关心 // 使用锁,解决多生产者多消费者的互斥访问 pthread_mutex_t _procductor_mutex; pthread_mutex_t _consumer_mutex; };
Task.hpp
文件#pragma once #include <string> using namespace std; class Task { public: Task(int a = 0, int b = 0) : _a(a), _b(b), _result(0) { _result = _a + _b; } int Result() { return _result; } string stringResult() { return to_string(_a) + " + " + to_string(_b) + " = " + to_string(_result); } string stringDebug() { return to_string(_a) + " + " + to_string(_b) + " = ? "; } private: int _a; int _b; int _result; };
运行结果:
3.11、线程池
3.11.1、多例模式线程池
线程池: 一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利用,还能防止过分调度。可用线程数量应该取决于可用的并发处理器、处理器内核、内存、网络sockets等的数量。
线程池的应用场景:
需要大量的线程来完成任务,且完成任务的时间比较短。 WEB服务器完成网页请求这样的任务,使用线程池技术是非常合适的。因为单个任务小,而任务数量巨大,你可以想象一个热门网站的点击次数。 但对于长时间的任务,比如一个Telnet连接请求,线程池的优点就不明显了。因为Telnet会话时间比线程的创建时间大多了。
对性能要求苛刻的应用,比如要求服务器迅速响应客户请求。
接受突发性的大量请求,但不至于使服务器因此产生大量线程的应用。突发性大量客户请求,在没有线程池情况下,将产生大量线程,虽然理论上大部分操作系统线程数目最大值不是问题,短时间内产生大量线程可能使内存到达极限,出现错误.
线程池的种类:可以是多例模式,也可以是单例模式。
线程池示例:
创建固定数量线程池,循环从任务队列中获取任务对象,
获取到任务对象后,执行任务对象中的任务接口
多例模式:
代码:
Threadpool.hpp
文件:·#pragma once #include <vector> #include <queue> #include <queue> #include "Thread.hpp" #include <pthread.h> using namespace ThreadModule; template <typename T> class Threadpool { void LockQueue(pthread_mutex_t &mutex) { pthread_mutex_lock(&mutex); } void UnLockQueue(pthread_mutex_t &mutex) { pthread_mutex_unlock(&mutex); } void SleepThread(pthread_cond_t &cond, pthread_mutex_t &mutex) { pthread_cond_wait(&cond, &mutex); } void WakeUpThread(pthread_cond_t &cond) { pthread_cond_signal(&cond); } void WakeUpAll(pthread_cond_t &cond) { pthread_cond_broadcast(&_cond); } public: Threadpool(const int threadnum = 3) : _threadnum(threadnum), _waitnum(0), _isrunning(false) { pthread_mutex_init(&_mutex, nullptr); pthread_cond_init(&_cond, nullptr); } void TaskHandler(string &name) { // sleep(1); // cout << name << " : hh " << endl; // sleep(1); while (true) { LockQueue(_mutex); while (_task_queue.empty() && _isrunning) { // 等待 ++_waitnum; SleepThread(_cond, _mutex); --_waitnum; } // 此时一定大于一个线程没有休眠 if (_task_queue.empty() && !_isrunning) { // 此时任务队列已经没有内容,且此时线程池已经停止 UnLockQueue(_mutex); cout << name << " quit ... " << endl; break; } LOG(DEBUG, "%s get task sucessful !", name.c_str()); // 其他情况就得处理任务 T t = _task_queue.front(); _task_queue.pop(); UnLockQueue(_mutex); // 处理任务 t(); cout << name << " : " << t.stringResult() << endl; sleep(1); } } void InitThreadPool() { for (int i = 0; i < _threadnum; ++i) { string name = "Thread - " + to_string(i + 1); _threads.emplace_back(bind(&Threadpool::TaskHandler, this, placeholders::_1), name); } _isrunning = true; } bool Enqueue(const T &in) { bool ret = false; LockQueue(_mutex); if (_isrunning) { _task_queue.push(in); if (_waitnum > 0) WakeUpThread(_cond); ret = true; } UnLockQueue(_mutex); return ret; } void Stop() { LockQueue(_mutex); _isrunning = false; if (_waitnum > 0) WakeUpAll(_cond); UnLockQueue(_mutex); } void Start() { for (auto &thread : _threads) { thread.Start(); } } void Wait() { for (auto &thread : _threads) { thread.Join(); } } ~Threadpool() { pthread_mutex_destroy(&_mutex); pthread_cond_destroy(&_cond); } private: vector<Thread> _threads; queue<T> _task_queue; int _threadnum; int _waitnum; pthread_mutex_t _mutex; // 互斥访问任务队列 pthread_cond_t _cond; bool _isrunning; };
Thread.hpp
文件:#ifndef __THREAD_HPP__ #define __THREAD_HPP__ #include <iostream> #include <string> #include <unistd.h> #include <functional> #include <pthread.h> using namespace std; // 封装Linux线程 namespace ThreadModule { using func_t = function<void(string &)>; class Thread { public: // /* ThreadData* */Thread(func_t<T> func, T data, const string& name = "default name") : _func(func), _data(data), _threadname(name), _stop(true) {} Thread(func_t func, const string &name = "default name") : _func(func), _threadname(name), _stop(true) {} void Execute() { _func(_threadname); // _func(_data); } // 隐含this static void *threadroutine(void *arg) { Thread *self = static_cast<Thread *>(arg); self->Execute(); // static 访问不了成员变量 return nullptr; } bool Start() { int n = pthread_create(&_tid, nullptr, threadroutine, this); if (!n) { _stop = false; return true; } else { return false; } } void Detach() { if (!_stop) { pthread_detach(_tid); } } void Join() { if (!_stop) { pthread_join(_tid, nullptr); } } string name() { return _threadname; } void Stop() { _stop = true; } // ~Thread() {} private: pthread_t _tid; string _threadname; func_t _func; bool _stop; }; } // namespace ThreadModule #endif
Task.hpp
文件:#pragma once #include <string> using namespace std; class Task { public: Task(int a = 0, int b = 0) : _a(a), _b(b), _result(0) { _result = _a + _b; } int Result() { return _result; } string stringResult() { return to_string(_a) + " + " + to_string(_b) + " = " + to_string(_result); } string stringDebug() { return to_string(_a) + " + " + to_string(_b) + " = ? "; } void operator()(){ // 重载operator方便threadpool使用t() Result(); } private: int _a; int _b; int _result; };
Main.cc
文件:#include <iostream> #include <memory> #include "Task.hpp" #include "Threadpool.hpp" int main() { // unique_ptr<Threadpool<Task>> tp(new Threadpool<Task>(5)); unique_ptr<Threadpool<Task>> tp = make_unique<Threadpool<Task>>(5); tp->InitThreadPool(); tp->Start(); int cnt = 10; while (cnt--) { srand(time(nullptr) ^ cnt); int a = rand() % 10; int b = rand() % 20; Task t(a, b); cout << "main thread - " << t.stringDebug() << endl; tp->Enqueue(t); } tp->Stop(); tp->Wait(); sleep(3); return 0; }
Makefile
文件:threadpool:Main.cc g++ -o $@ $^ -std=c++14 -lpthread .PHONY:clean clean: rm -f threadpool
运行结果:
3.11.2、带日志版本线程池
LOG.hpp
文件:#pragma once #include <string> #include <iostream> #include <fstream> #include <unistd.h> #include <stdarg.h> #include <sys/types.h> bool isSave = false; // 默认向显示器打印 #define FILEPATH "./log.txt" enum level { DEBUG = 0, INFO, WARNING, ERROR, FATAL }; void SaveToFile(const string &message) { ofstream out(FILEPATH, ios_base::app); if (!out.is_open()) return; out << message; out.close(); } std::string LevelToString(int level) { switch (level) { case DEBUG: return "Debug"; case INFO: return "Info"; case WARNING: return "Warning"; case ERROR: return "Error"; case FATAL: return "Fatal"; default: return "Unknow"; } } std::string GetTimeString() { time_t curr_time = time(nullptr); struct tm *format_time = localtime(&curr_time); if (format_time == nullptr) return "None"; char buff[1024]; snprintf(buff, sizeof(buff), "%d-%d-%d %d:%d:%d", format_time->tm_year + 1900, format_time->tm_mon + 1, format_time->tm_mday, format_time->tm_hour, format_time->tm_min, format_time->tm_sec); return buff; } void LogMessage(const std::string filename, int line, bool issave, int level, const char *format, ...) { std::string levelstr = LevelToString(level); std::string timestr = GetTimeString(); pid_t pid = getpid(); char buff[1024]; va_list arg; // int vsnprintf(char *str, size_t size, const char *format, va_list ap); // 使用可变参数 va_start(arg, format); vsnprintf(buff, sizeof(buff), format, arg); va_end(arg); std::string message = "[" + timestr + "]" + "[" + levelstr + "]" + "[pid:" + std::to_string(pid) + "]" + "[" + filename + "]" + "[" + std::to_string(line) + "] " + buff + '\n'; if (issave == false) std::cout << message; else SaveToFile(message); } // 固定文件名和行数 #define LOG(level, format, ...) \ do \ { \ LogMessage(__FILE__, __LINE__, isSave, level, format, ##__VA_ARGS__); \ } while (0) #define EnableScreen() \ do \ { \ isSave = false; \ } while (0) #define EnableFile() \ do \ { \ isSave = true; \ } while (0) void Test(int num, ...) { va_list arg; va_start(arg, num); while (num--) { int data = va_arg(arg, int); std::cout << data << " "; } std::cout << std::endl; va_end(arg); }
Threadpool.hpp
文件:#pragma once #include <vector> #include <queue> #include <queue> #include "Thread.hpp" #include <pthread.h> using namespace ThreadModule; template <typename T> class Threadpool { void LockQueue(pthread_mutex_t &mutex) { pthread_mutex_lock(&mutex); } void UnLockQueue(pthread_mutex_t &mutex) { pthread_mutex_unlock(&mutex); } void SleepThread(pthread_cond_t &cond, pthread_mutex_t &mutex) { pthread_cond_wait(&cond, &mutex); } void WakeUpThread(pthread_cond_t &cond) { pthread_cond_signal(&cond); } void WakeUpAll(pthread_cond_t &cond) { pthread_cond_broadcast(&_cond); } public: Threadpool(const int threadnum = 3) : _threadnum(threadnum), _waitnum(0), _isrunning(false) { pthread_mutex_init(&_mutex, nullptr); pthread_cond_init(&_cond, nullptr); LOG(INFO, "Threadpool Constructor successful ! "); } void TaskHandler(string &name) { // sleep(1); // cout << name << " : hh " << endl; // sleep(1); LOG(DEBUG, "%s is running", name.c_str()); while (true) { LockQueue(_mutex); while (_task_queue.empty() && _isrunning) { // 等待 ++_waitnum; SleepThread(_cond, _mutex); --_waitnum; } // 此时一定大于一个线程没有休眠 if (_task_queue.empty() && !_isrunning) { // 此时任务队列已经没有内容,且此时线程池已经停止 UnLockQueue(_mutex); cout << name << " quit ... " << endl; break; } LOG(DEBUG, "%s get task sucessful !", name.c_str()); // 其他情况就得处理任务 T t = _task_queue.front(); _task_queue.pop(); UnLockQueue(_mutex); // 处理任务 t(); // cout << name << " : " << t.stringResult() << endl; LOG(DEBUG, "%s handler task sucessful ! Result is %s", name.c_str(), t.stringResult().c_str()); sleep(1); } } void InitThreadPool() { for (int i = 0; i < _threadnum; ++i) { string name = "Thread - " + to_string(i + 1); _threads.emplace_back(bind(&Threadpool::TaskHandler, this, placeholders::_1), name); } _isrunning = true; LOG(INFO, "Init Threadpool successful !"); } bool Enqueue(const T &in) { bool ret = false; LockQueue(_mutex); if (_isrunning) { _task_queue.push(in); if (_waitnum > 0) WakeUpThread(_cond); LOG(DEBUG, "enqueue sucessful..."); ret = true; } UnLockQueue(_mutex); return ret; } void Stop() { LockQueue(_mutex); _isrunning = false; if (_waitnum > 0) WakeUpAll(_cond); UnLockQueue(_mutex); } void Start() { for (auto &thread : _threads) { thread.Start(); LOG(INFO, "%s is start sucessful...", thread.name().c_str()); } } void Wait() { for (auto &thread : _threads) { thread.Join(); LOG(INFO, "%s is quit...", thread.name().c_str()); } } ~Threadpool() { pthread_mutex_destroy(&_mutex); pthread_cond_destroy(&_cond); LOG(INFO, "delete mutex sucessful !"); } private: vector<Thread> _threads; queue<T> _task_queue; int _threadnum; int _waitnum; pthread_mutex_t _mutex; // 互斥访问任务队列 pthread_cond_t _cond; bool _isrunning; };
Thread.hpp
文件#ifndef __THREAD_HPP__ #define __THREAD_HPP__ #include <iostream> #include <string> #include <unistd.h> #include <functional> #include <pthread.h> using namespace std; // 封装Linux线程 namespace ThreadModule { using func_t = function<void(string &)>; class Thread { public: // /* ThreadData* */Thread(func_t<T> func, T data, const string& name = "default name") : _func(func), _data(data), _threadname(name), _stop(true) {} Thread(func_t func, const string &name = "default name") : _func(func), _threadname(name), _stop(true) {} void Execute() { _func(_threadname); // _func(_data); } // 隐含this static void *threadroutine(void *arg) { Thread *self = static_cast<Thread *>(arg); self->Execute(); // static 访问不了成员变量 return nullptr; } bool Start() { int n = pthread_create(&_tid, nullptr, threadroutine, this); if (!n) { _stop = false; return true; } else { return false; } } void Detach() { if (!_stop) { pthread_detach(_tid); } } void Join() { if (!_stop) { pthread_join(_tid, nullptr); } } string name() { return _threadname; } void Stop() { _stop = true; } // ~Thread() {} private: pthread_t _tid; string _threadname; func_t _func; bool _stop; }; } // namespace ThreadModule #endif
Task.hpp
文件:#pragma once #include <string> using namespace std; class Task { public: Task(int a = 0, int b = 0) : _a(a), _b(b), _result(0) { _result = _a + _b; } int Result() { return _result; } string stringResult() { return to_string(_a) + " + " + to_string(_b) + " = " + to_string(_result); } string stringDebug() { return to_string(_a) + " + " + to_string(_b) + " = ? "; } void operator()(){ // 重载operator方便threadpool使用t() Result(); } private: int _a; int _b; int _result; };
Main.cc
文件:#include <iostream> #include <memory> #include "Task.hpp" #include "Log.hpp" #include "Threadpool.hpp" int main() { EnableFile(); LOG(DEBUG, "hello"); LOG(DEBUG, "hello %d", 1); LOG(DEBUG, "hello hh %f", 2.1); LOG(DEBUG, "hello hhhh %d %f", 1, 2.1); LOG(DEBUG, "hello hhhhhh %d %d %c", 2, 31, 'c'); LOG(DEBUG, "hello hhhhhh %d %d %c %s", 2, 31, 'c', "oo"); // LogMessage(__FILE__, __LINE__, DEBUG, "hello %d", 1); // LogMessage(__FILE__, __LINE__, DEBUG, "hello hh %f", 2.1); // LogMessage(__FILE__, __LINE__, DEBUG, "hello hhhh %d %f", 1, 2.1); // LogMessage(__FILE__, __LINE__, DEBUG, "hello hhhhhh %d %d %c", 2, 31, 'c'); // LogMessage(__FILE__, __LINE__, DEBUG, "hello hhhhhh %d %d %c %s", 2, 31, 'c', "oo"); // Test(3,1,2,1); // Test(7,1,2,1,23,4,5,8); // LogMessage(__FILE__, __LINE__, DEBUG, "hello"); // LogMessage(__FILE__, __LINE__, DEBUG, "hello hh"); // LogMessage(__FILE__, __LINE__, DEBUG, "hello hhhh"); // LogMessage(__FILE__, __LINE__, DEBUG, "hello hhhhhh"); // LogMessage(__FILE__, __LINE__, DEBUG, "hello hhhhhhhhhh"); // unique_ptr<Threadpool<Task>> tp(new Threadpool<Task>(5)); unique_ptr<Threadpool<Task>> tp = make_unique<Threadpool<Task>>(5); tp->InitThreadPool(); tp->Start(); int cnt = 10; while (cnt--) { srand(time(nullptr) ^ cnt); int a = rand() % 10; int b = rand() % 20; Task t(a, b); LOG(DEBUG, "main thread : %s", t.stringDebug().c_str()); // cout << "main thread - " << t.stringDebug() << endl; tp->Enqueue(t); } tp->Stop(); tp->Wait(); sleep(3); return 0; }
运行结果:
3.11.3、单例模式线程池
单例模式:分为懒汉模式和饿汉模式。在C++博客中有讲链接。
LockGuard.hpp
文件:#include <pthread.h> class LockGuard { public: LockGuard(pthread_mutex_t *mutex) : _mutex(mutex) { pthread_mutex_lock(_mutex); // 构造加锁 } ~LockGuard() { pthread_mutex_unlock(_mutex); // 析构解锁 } private: pthread_mutex_t *_mutex; };
LOG.hpp
文件:#pragma once #include <string> #include <iostream> #include <fstream> #include <unistd.h> #include <stdarg.h> #include <sys/types.h> bool isSave = false; // 默认向显示器打印 #define FILEPATH "./log.txt" enum level { DEBUG = 0, INFO, WARNING, ERROR, FATAL }; void SaveToFile(const string &message) { ofstream out(FILEPATH, ios_base::app); if (!out.is_open()) return; out << message; out.close(); } std::string LevelToString(int level) { switch (level) { case DEBUG: return "Debug"; case INFO: return "Info"; case WARNING: return "Warning"; case ERROR: return "Error"; case FATAL: return "Fatal"; default: return "Unknow"; } } std::string GetTimeString() { time_t curr_time = time(nullptr); struct tm *format_time = localtime(&curr_time); if (format_time == nullptr) return "None"; char buff[1024]; snprintf(buff, sizeof(buff), "%d-%d-%d %d:%d:%d", format_time->tm_year + 1900, format_time->tm_mon + 1, format_time->tm_mday, format_time->tm_hour, format_time->tm_min, format_time->tm_sec); return buff; } void LogMessage(const std::string filename, int line, bool issave, int level, const char *format, ...) { std::string levelstr = LevelToString(level); std::string timestr = GetTimeString(); pid_t pid = getpid(); char buff[1024]; va_list arg; // int vsnprintf(char *str, size_t size, const char *format, va_list ap); // 使用可变参数 va_start(arg, format); vsnprintf(buff, sizeof(buff), format, arg); va_end(arg); std::string message = "[" + timestr + "]" + "[" + levelstr + "]" + "[pid:" + std::to_string(pid) + "]" + "[" + filename + "]" + "[" + std::to_string(line) + "] " + buff + '\n'; if (issave == false) std::cout << message; else SaveToFile(message); } // 固定文件名和行数 #define LOG(level, format, ...) \ do \ { \ LogMessage(__FILE__, __LINE__, isSave, level, format, ##__VA_ARGS__); \ } while (0) #define EnableScreen() \ do \ { \ isSave = false; \ } while (0) #define EnableFile() \ do \ { \ isSave = true; \ } while (0) void Test(int num, ...) { va_list arg; va_start(arg, num); while (num--) { int data = va_arg(arg, int); std::cout << data << " "; } std::cout << std::endl; va_end(arg); }
Main.cc
文件:#include <iostream> #include <memory> #include "Task.hpp" #include "Log.hpp" #include "Threadpool.hpp" // 单例模式 int main() { EnableFile(); LOG(DEBUG, "程序已经加载"); sleep(3); Threadpool<Task> *tp = Threadpool<Task>::GetInstance(10); int cnt = 10; while (cnt--) { srand(time(nullptr) ^ cnt); int a = rand() % 10; int b = rand() % 20; Task t(a, b); LOG(DEBUG, "main thread : %s", t.stringDebug().c_str()); // cout << "main thread - " << t.stringDebug() << endl; tp->Enqueue(t); } sleep(3); Threadpool<Task>::GetInstance(); sleep(3); Threadpool<Task>::GetInstance(); sleep(3); Threadpool<Task>::GetInstance(); cnt = 5; while (cnt--) { srand(time(nullptr) ^ cnt); int a = rand() % 10; int b = rand() % 20; Task t(a, b); LOG(DEBUG, "main thread : %s", t.stringDebug().c_str()); // cout << "main thread - " << t.stringDebug() << endl; tp->Enqueue(t); } tp->Stop(); tp->Wait(); sleep(3); return 0; }
Task.hpp
文件:#pragma once #include <string> using namespace std; class Task { public: Task(int a = 0, int b = 0) : _a(a), _b(b), _result(0) { _result = _a + _b; } int Result() { return _result; } string stringResult() { return to_string(_a) + " + " + to_string(_b) + " = " + to_string(_result); } string stringDebug() { return to_string(_a) + " + " + to_string(_b) + " = ? "; } void operator()(){ // 重载operator方便threadpool使用t() Result(); } private: int _a; int _b; int _result; };
Thread.hpp
文件:#pragma once #include <string> using namespace std; class Task { public: Task(int a = 0, int b = 0) : _a(a), _b(b), _result(0) { _result = _a + _b; } int Result() { return _result; } string stringResult() { return to_string(_a) + " + " + to_string(_b) + " = " + to_string(_result); } string stringDebug() { return to_string(_a) + " + " + to_string(_b) + " = ? "; } void operator()(){ // 重载operator方便threadpool使用t() Result(); } private: int _a; int _b; int _result; };
Thread.hpp
文件:#ifndef __THREAD_HPP__ #define __THREAD_HPP__ #include <iostream> #include <string> #include <unistd.h> #include <functional> #include <pthread.h> using namespace std; // 封装Linux线程 namespace ThreadModule { using func_t = function<void(string &)>; class Thread { public: // /* ThreadData* */Thread(func_t<T> func, T data, const string& name = "default name") : _func(func), _data(data), _threadname(name), _stop(true) {} Thread(func_t func, const string &name = "default name") : _func(func), _threadname(name), _stop(true) {} void Execute() { _func(_threadname); // _func(_data); } // 隐含this static void *threadroutine(void *arg) { Thread *self = static_cast<Thread *>(arg); self->Execute(); // static 访问不了成员变量 return nullptr; } bool Start() { int n = pthread_create(&_tid, nullptr, threadroutine, this); if (!n) { _stop = false; return true; } else { return false; } } void Detach() { if (!_stop) { pthread_detach(_tid); } } void Join() { if (!_stop) { pthread_join(_tid, nullptr); } } string name() { return _threadname; } void Stop() { _stop = true; } // ~Thread() {} private: pthread_t _tid; string _threadname; func_t _func; bool _stop; }; } // namespace ThreadModule #endif
Threadpool.hpp
文件:#pragma once #include <vector> #include <queue> #include <queue> #include "Thread.hpp" #include <pthread.h> #include "LockGuard.hpp" using namespace ThreadModule; const int NUM = 3; template <typename T> class Threadpool { void LockQueue(pthread_mutex_t &mutex) { pthread_mutex_lock(&mutex); } void UnLockQueue(pthread_mutex_t &mutex) { pthread_mutex_unlock(&mutex); } void SleepThread(pthread_cond_t &cond, pthread_mutex_t &mutex) { pthread_cond_wait(&cond, &mutex); } void WakeUpThread(pthread_cond_t &cond) { pthread_cond_signal(&cond); } void WakeUpAll(pthread_cond_t &cond) { pthread_cond_broadcast(&_cond); } Threadpool(const int threadnum = NUM) : _threadnum(threadnum), _waitnum(0), _isrunning(false) { pthread_mutex_init(&_mutex, nullptr); pthread_cond_init(&_cond, nullptr); LOG(INFO, "Threadpool Constructor successful ! "); } void TaskHandler(string &name) { // sleep(1); // cout << name << " : hh " << endl; // sleep(1); LOG(DEBUG, "%s is running", name.c_str()); while (true) { LockQueue(_mutex); while (_task_queue.empty() && _isrunning) { // 等待 ++_waitnum; SleepThread(_cond, _mutex); --_waitnum; } // 此时一定大于一个线程没有休眠 if (_task_queue.empty() && !_isrunning) { // 此时任务队列已经没有内容,且此时线程池已经停止 UnLockQueue(_mutex); cout << name << " quit ... " << endl; break; } LOG(DEBUG, "%s get task sucessful !", name.c_str()); // 其他情况就得处理任务 T t = _task_queue.front(); _task_queue.pop(); UnLockQueue(_mutex); // 处理任务 t(); // cout << name << " : " << t.stringResult() << endl; LOG(DEBUG, "%s handler task sucessful ! Result is %s", name.c_str(), t.stringResult().c_str()); sleep(1); } } void InitThreadPool() { for (int i = 0; i < _threadnum; ++i) { string name = "Thread - " + to_string(i + 1); _threads.emplace_back(bind(&Threadpool::TaskHandler, this, placeholders::_1), name); } _isrunning = true; LOG(INFO, "Init Threadpool successful !"); } public: static Threadpool<T> *GetInstance(int threadnum = NUM) { if (_instance == nullptr) { LockGuard lockguard(&_lock); if (_instance == nullptr) { // pthread_mutex_lock(&_lock); // 第一次创建线程池 _instance = new Threadpool<T>(threadnum); _instance->InitThreadPool(); _instance->Start(); LOG(DEBUG, "第一次创建线程池"); // pthread_mutex_unlock(&_lock); return _instance; } } LOG(DEBUG, "获取线程池"); return _instance; } bool Enqueue(const T &in) { bool ret = false; LockQueue(_mutex); if (_isrunning) { _task_queue.push(in); if (_waitnum > 0) WakeUpThread(_cond); LOG(DEBUG, "enqueue sucessful..."); ret = true; } UnLockQueue(_mutex); return ret; } void Stop() { LockQueue(_mutex); _isrunning = false; if (_waitnum > 0) WakeUpAll(_cond); UnLockQueue(_mutex); } void Start() { for (auto &thread : _threads) { thread.Start(); LOG(INFO, "%s is start sucessful...", thread.name().c_str()); } } void Wait() { for (auto &thread : _threads) { thread.Join(); LOG(INFO, "%s is quit...", thread.name().c_str()); } } ~Threadpool() { pthread_mutex_destroy(&_mutex); pthread_cond_destroy(&_cond); LOG(INFO, "delete mutex sucessful !"); } private: vector<Thread> _threads; queue<T> _task_queue; int _threadnum; int _waitnum; pthread_mutex_t _mutex; // 互斥访问任务队列 pthread_cond_t _cond; bool _isrunning; // 懒汉模式 static Threadpool<T> *_instance; static pthread_mutex_t _lock; }; template <typename T> Threadpool<T> *Threadpool<T>::_instance = nullptr; template <typename T> pthread_mutex_t Threadpool<T>::_lock = PTHREAD_MUTEX_INITIALIZER;
运行结果:
4、线程的优点
- 创建一个新线程的代价要比创建一个新进程小得多
- 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多
- 线程占用的资源要比进程少很多
- 能充分利用多处理器的可并行数量
- 在等待慢速I/O操作结束的同时,程序可执行其他的计算任务
- 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
- I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。
5、线程的缺点
性能损失
一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变。
健壮性降低
编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。
缺乏访问控制
进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。编程难度提高编写与调试一个多线程程序比单线程程序困难得多。
6、线程用途
合理的使用多线程,能提高CPU密集型程序的执行效率
合理的使用多线程,能提高IO密集型程序的用户体验(如生活中我们一边写代码一边下载开发工具,就是多线程运行的一种表现)
7、可重入VS线程安全
7.1、概念
线程安全:多个线程并发同一段代码时,不会出现不同的结果。常见对全局变量或者静态变量进行操作,并且没有锁保护的情况下,会出现该问题。
重入:同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则,是不可重入函数。
7.2、常见的线程不安全的情况
- 不保护共享变量的函数
- 函数状态随着被调用,状态发生变化的函数
- 返回指向静态变量指针的函数
- 调用线程不安全函数的函数
7.3、常见的线程安全的情况
- 每个线程对全局变量或者静态变量只有读取的权限,而没有写入的权限,一般来说这些线程是安全的类或者接口对于线程来说都是原子操作
- 多个线程之间的切换不会导致该接口的执行结果存在二义性
7.4、常见不可重入的情况
- 调用了malloc/free函数,因为malloc函数是用全局链表来管理堆的
- 调用了标准I/O库函数,标准I/O库的很多实现都以不可重入的方式使用全局数据结构
- 可重入函数体内使用了静态的数据结构
7.5、常见可重入的情况
- 不使用全局变量或静态变量
- 不使用用malloc或者new开辟出的空间
- 不调用不可重入函数
- 不返回静态或全局数据,所有数据都有函数的调用者提供
- 使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据
7.6、可重入与线程安全联系
- 函数是可重入的,那就是线程安全的
- 函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题
- 如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的。
7.7、可重入与线程安全区别
- 可重入函数是线程安全函数的一种
- 线程安全不一定是可重入的(一开始就加锁,最后才解锁),而可重入函数则一定是线程安全的。
- 如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数若锁还未释放则会产生死锁,因此是不可重入的。
8、常见锁概念
8.1、死锁
死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所站用不会释放的资源而处于的一种永久等待状态。
8.2、死锁四个必要条件
互斥条件:一个资源每次只能被一个执行流使用
请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放
不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺
循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系
8.3、避免死锁
- 破坏死锁的四个必要条件
- 加锁顺序一致
- 避免锁未释放的场景
- 资源一次性分配
8.4、避免死锁算法
- 死锁检测算法
- 银行家算法
9、STL,智能指针和线程安全
9.1、STL中的容器是否是线程安全的
不是。原因是,STL 的设计初衷是将性能挖掘到极致,而一旦涉及到加锁保证线程安全,会对性能造成巨大的影响。而且对于不同的容器, 加锁方式的不同, 性能可能也不同(例如hash表的锁表和锁桶)。因此 STL 默认不是线程安全。如果需要在多线程环境下使用,往往需要调用者自行保证线程安全。
9.2、智能指针是否是线程安全的
对于 unique_ptr,由于只是在当前代码块范围内生效,因此不涉及线程安全问题。对于 shared_ptr,多个对象需要共用一个引用计数变量,所以会存在线程安全问题。但是标准库实现的时候考虑到了这个问题,基于原子操作(CAS)的方式保证 shared_ptr 能够高效,原子的操作引用计数。
10、其他常见的各种锁
悲观锁:在每次取数据时,总是担心数据会被其他线程修改,所以会在取数据前先加锁(读锁,写锁,行锁等),当其他线程想要访问数据时,被阻塞挂起。
乐观锁:每次取数据时候,总是乐观的认为数据不会被其他线程修改,因此不上锁。但是在更新数据前,会判断其他数据在更新前有没有对数据进行修改。主要采用两种方式:版本号机制和CAS操作。
CAS操作:当需要更新数据时,判断当前内存值和之前取得的值是否相等。如果相等则用新值更新。若不等则失败,失败则重试,一般是一个自旋的过程,即不断重试。
自旋锁(pthread_spin_t),公平锁,非公平锁。
11、读者写者问题
读写锁
在编写多线程的时候,有一种情况是十分常见的。那就是,有些公共数据修改的机会比较少。相比较改写,它们读的机会反而高的多。通常而言,在读的过程中,往往伴随着查找的操作,中间耗时很长。给这种代码段加锁,会极大地降低我们程序的效率。那么有没有一种方法,可以专门处理这种多读少写的情况呢? 有,那就是读写锁。
注意:写独占,读共享,读锁优先级高
读写锁接口
设置读写优先
int pthread_rwlockattr_setkind_np(pthread_rwlockattr_t *attr, int pref); pref 共有 3 种选择 PTHREAD_RWLOCK_PREFER_READER_NP (默认设置) 读者优先,可能会导致写者饥饿情况 PTHREAD_RWLOCK_PREFER_WRITER_NP 写者优先,目前有 BUG,导致表现行为和 PTHREAD_RWLOCK_PREFER_READER_NP 一致 PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP 写者优先,但写者不能递归加锁
初始化
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,const pthread_rwlockattr_t* restrict attr);
销毁
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
加锁和解锁
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock); int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock); int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
代码:
#include <vector> #include <sstream> #include <cstdio> #include <cstdlib> #include <cstring> #include <unistd.h> #include <pthread.h> volatile int ticket = 1000; pthread_rwlock_t rwlock; void *reader(void *arg) { char *id = (char *)arg; while (1) { pthread_rwlock_rdlock(&rwlock); if (ticket <= 0) { pthread_rwlock_unlock(&rwlock); break; } printf("%s: %d\n", id, ticket); pthread_rwlock_unlock(&rwlock); usleep(1); } return nullptr; } void *writer(void *arg) { char *id = (char *)arg; while (1) { pthread_rwlock_wrlock(&rwlock); if (ticket <= 0) { pthread_rwlock_unlock(&rwlock); break; } printf("%s: %d\n", id, --ticket); pthread_rwlock_unlock(&rwlock); usleep(1); } return nullptr; } struct ThreadAttr { pthread_t tid; std::string id; }; std::string create_reader_id(std::size_t i) { // 利用 ostringstream 进行 string 拼接 std::ostringstream oss("thread reader ", std::ios_base::ate); oss << i; return oss.str(); } std::string create_writer_id(std::size_t i) { // 利用 ostringstream 进行 string 拼接 std::ostringstream oss("thread writer ", std::ios_base::ate); oss << i; return oss.str(); } void init_readers(std::vector<ThreadAttr> &vec) { for (std::size_t i = 0; i < vec.size(); ++i) { vec[i].id = create_reader_id(i); pthread_create(&vec[i].tid, nullptr, reader, (void *)vec[i].id.c_str()); } } void init_writers(std::vector<ThreadAttr> &vec) { for (std::size_t i = 0; i < vec.size(); ++i) { vec[i].id = create_writer_id(i); pthread_create(&vec[i].tid, nullptr, writer, (void *)vec[i].id.c_str()); } } void join_threads(std::vector<ThreadAttr> const &vec) { // 我们按创建的 逆序 来进行线程的回收 for (std::vector<ThreadAttr>::const_reverse_iterator it = vec.rbegin(); it != vec.rend(); ++it) { pthread_t const &tid = it->tid; pthread_join(tid, nullptr); } } void init_rwlock() { #if 0 // 写优先 pthread_rwlockattr_t attr; pthread_rwlockattr_init(&attr); pthread_rwlockattr_setkind_np(&attr, PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP); pthread_rwlock_init(&rwlock, &attr); pthread_rwlockattr_destroy(&attr); #else // 读优先,会造成写饥饿 pthread_rwlock_init(&rwlock, nullptr); #endif } int main() { // 测试效果不明显的情况下,可以加大 reader_nr // 但也不能太大,超过一定阈值后系统就调度不了主线程了 const std::size_t reader_nr = 1000; const std::size_t writer_nr = 2; std::vector<ThreadAttr> readers(reader_nr); std::vector<ThreadAttr> writers(writer_nr); init_rwlock(); init_readers(readers); init_writers(writers); join_threads(writers); join_threads(readers); pthread_rwlock_destroy(&rwlock); return 0; }
OKOK,Linux线程就到这里,如果你对Linux和C++也感兴趣的话,可以看看我的主页哦。下面是我的github主页,里面记录了我的学习代码和leetcode的一些题的题解,有兴趣的可以看看。