在一个程序里的一个执行路线就叫做线程(thread)。更准确的定义是:线程是“一个进程内部的控制序列”
一切进程至少都有一个执行线程
线程在进程内部运行,本质是在进程地址空间内运行
在Linux系统中,在CPU眼中,看到的PCB都要比传统的进程更加轻量化
透过进程虚拟地址空间,可以看到进程的大部分资源,将进程资源合理分配给每个执行流,就形成了线程执行流
在进程中,有一个task_struct的结构体,其中的一个指针指向地址空间。如果我在创建一个进程,这个进程不创建地址空间,不创建页表,也不再物理内存中创建属于这个进程的资源,它只需要指向某个已经创建好的进程的地址空间,页表,物理内存。并且地址空间中的代码区,堆区等区域都给这个新进程一部分,就可以让这两个进程都运行起来。如果我创建跟多新进程,都指向某个创建好进程的地址空间,这些新进程都是进程的执行分支,那么这些新进程就可以叫做线程。
那么Linux的实现方案是什么?
不同操作系统对线程的概念是一样的,但是实现方案可能会有不同。
在进程这里提到过,进程=内核数据结构(task_struct) + 代码和数据,从内核观点来说:进程是承担分配系统资源的基本实体。执行流是资源吗?是的,我们的线程是进程内部的执行流资源。操作系统就是以进程为单位,来进行的分配资源,而我们当前进程的内部,只有一个执行流。
当线程创建出来了,操作系统就要把线程管理起来,如何管理?跟进程一样吗?通过创建task_struct,调度算法,优先级算法等操作都需要搞一个,这样就会太复杂了,Linux的设计者直接用struct task_struct来模拟线程。用进程的数据结构进行复用,来模拟线程。这样就将线程管理了起来。简单来说,就是Linux没有真正的线程(因为并没有创建线程的PCB),而是用进程的内核数据结构来模拟的线程。
文章开头说,线程比进程更轻量化,这是为什么?
线程在切换的时候肯定会有自己的上下文,CPU内有大量的寄存器,线程在切换的时候要进程上下文保护,但是页表和地址空间是不需要切换的,所以切换效率就会提高。在CPU中,除了有寄存器,CPU所有以进程为载体,线程在执行的时候,本质就是进程在执行,线程是进程的一个执行分支。所以CPU内部,会有一个硬件级别的缓存,叫cache。
通过 cat /proc/cpuinfo可以查看,比如说你当前要访问第10行代码,它会将第10行到第50行(或n行)的代码全部弄到内存中去,这就叫做cache,所以进程在调度的时候,会越来越快。这个cache称为缓存的热数据,这部分数据高频被访问。一个进程内的多个线程切换的时候,上下文会变化,但是缓存的数据不会变化或者是做少量的更新。切换过程中,只需要切换,不需要做保存。如果要切换另一个进程,热数据就需要切换,重新缓存,所以线程要比进程更加轻量化。
创建一个新线程的代价要比创建一个新进程小得多
与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多
线程占用的资源要比进程少很多
能充分利用多处理器的可并行数量
在等待慢速I/O操作结束的同时,程序可执行其他的计算任务
计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作
单个线程如果出现除零,野指针问题导致线程崩溃,进程也会随着崩溃
线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该进程内的所有线程也就随即退出
合理的使用多线程,能提高CPU密集型程序的执行效率
合理的使用多线程,能提高IO密集型程序的用户体验(如生活中我们一边写代码一边下载开发工具,就是多线程运行的一种表现 )
进程是资源分配的基本单位
线程是调度的基本单位
线程共享进程数据,但也拥有自己的一部分数据:
线程ID
一组寄存器
栈
errno
信号屏蔽字
调度优先级
进程的多个线程共享 同一地址空间,因此Text Segment、Data Segment都是共享的,如果定义一个函数,在各线程中都可以调用,如果定义一个全局变量,在各线程中都可以访问到,除此之外,各线程还共享以下进程资源和环境:
在内核中没有很明确的线程概念,但是有轻量级进程的概念,这就导致系统没有直接提供线程的系统调用,只提供了轻量级进程的系统调用。我们需要线程的接口,所以就有程序员在应用层开发了pthread线程库,以轻量级进程接口进行封装,为用户提供直接线程的接口。几乎所有Linux平台,都默认自带这个库 --- pthread库。
跟线程有关的函数构成了一个完整的系列,大多数函数的名字都是以pthread_开头的,要使用这些函数,引入头文件
功能:创建一个新的线程
原型
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;失败返回错误码
thread:thread.cc
g++ -o $@ $^ -g -lpthread -std=c++11
.PHONY:clean
clean:
rm -rf thread
#include
#include
#include
using namespace std;
void *handler(void *arg)
{
while (1)
{
cout << "handler " << getpid() << endl;
sleep(1);
}
return nullptr;
}
int main()
{
pthread_t pt;
pthread_create(&pt, nullptr, handler, nullptr);
while (1)
{
cout << "main " << getpid() << endl;
sleep(1);
}
return 0;
}
通过ps -aL可以查看轻量级进程.每一个轻量级进程有一个pid,LWP(light weight process)就是这个id。仔细观察会发现,存在一个线程的LWP和PID是相等的,说明这个线程其实是最先创建出来的那个线程,也就是进程(单进程不就是只有一个线程吗)。
当线程是因为异常退出的时候,该进程会被杀死。
#include
#include
#include
using namespace std;
void show()
{
cout << "show" << endl;
}
void *handler(void *arg)
{
show();
return nullptr;
}
int main()
{
pthread_t pt;
pthread_create(&pt, nullptr, handler, nullptr);
show();
return 0;
}
一个函数可以被不同的执行流执行。
当一个线程创建出来的时候,谁先运行?谁先退出呢?
谁先运行不清楚,但主线程应该最后一个退出,当主线程提前退出的时候,其他线程就不能运行了。所以线程是需要等待的。对于已经退出的线程来说,其空间没有被释放,仍然在进程的地址空间内。
功能:等待线程结束
原型
int pthread_join(pthread_t thread, void **value_ptr);
参数
thread:线程ID
value_ptr:它指向一个指针,后者指向线程的返回值
返回值:成功返回0;失败返回错误码
调用该函数的线程将挂起等待,直到id为thread的线程终止。thread线程以不同的方法终止,通过pthread_join得到的
终止状态是不同的,总结如下:
1. 如果thread线程通过return返回,value_ ptr所指向的单元里存放的是thread线程函数的返回值。
2. 如果thread线程被别的线程调用pthread cancel异常终掉,value ptr所指向的单元里存放的是常数
PTHREAD_ CANCELED。
3. 如果thread线程是自己调用pthread_exit终止的,value_ptr所指向的单元存放的是传给pthread_exit的参数。
4. 如果对thread线程的终止状态不感兴趣,可以传NULL给value_ ptr参数
#include
#include
#include
using namespace std;
void *handler(void *arg)
{
int cnt = 0;
while (cnt++ != 10)
{
cout << "handler" << endl;
}
return nullptr;
}
int main()
{
pthread_t pt;
pthread_create(&pt, nullptr, handler, nullptr);
pthread_join(pt, nullptr);
cout << "main quit" << endl;
return 0;
}
使用线程等待,可以保证主线程是在线程执行完毕之后,在退出。
如何获取线程的返回值?
通过 pthread_join来获取。
#include
#include
#include
using namespace std;
void *handler(void *arg)
{
int cnt = 0;
while (cnt++ != 10)
{
cout << "handler" << endl;
}
return (void*)1;
}
int main()
{
pthread_t pt;
pthread_create(&pt, nullptr, handler, nullptr);
void* retval;
pthread_join(pt, &retval);
cout << (long long int)retval << endl;
return 0;
}
这就拿到了线程的返回值。
功能:线程终止
原型
void pthread_exit(void *value_ptr);
参数
value_ptr:value_ptr不要指向一个局部变量。
返回值:无返回值,跟进程一样,线程结束的时候无法返回到它的调用者(自身)
需要注意,pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是用malloc分配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了。
#include
#include
#include
using namespace std;
void *handler(void *arg)
{
int cnt = 0;
while (cnt++ != 10)
{
cout << "handler" << endl;
}
pthread_exit((void*)100);
}
int main()
{
pthread_t pt;
pthread_create(&pt, nullptr, handler, nullptr);
void* retval;
pthread_join(pt, &retval);
cout << (long long int)retval << endl;
return 0;
}
功能:取消一个执行中的线程
原型
int pthread_cancel(pthread_t thread);
参数
thread:线程ID
返回值:成功返回0;失败返回错误码
#include
#include
#include
using namespace std;
void *handler(void *arg)
{
int cnt = 0;
while (cnt++ != 10)
{
cout << "handler" << endl;
}
pthread_exit((void*)100);
}
int main()
{
pthread_t pt;
pthread_create(&pt, nullptr, handler, nullptr);
pthread_cancel(pt);
cout << "main quit" << endl;
return 0;
}
pt线程并没有执行handler函数,而是主线程运行到cancel函数的时候,线程直接取消了。
了解上面函数之后,可以自己尝试写一个用线程来计算区间和。
#include
#include
#include
using namespace std;
class Request
{
public:
Request(int start, int end, const string &threadname)
:_start(start)
,_end(end)
,_threadname(threadname)
{}
public:
int _start;
int _end;
string _threadname;
};
class Response
{
public:
Response(int result, int exitcode)
:_result(result)
,_exitcode(exitcode)
{}
public:
int _result;
int _exitcode;
};
void *sumCount(void *args)
{
Request *rq = static_cast(args);
Response *rsp = new Response(0,0);
for (int i = rq->_start; i <= rq->_end; i++)
{
rsp->_result += i;
}
return rsp;
}
int main()
{
pthread_t tid;
Request *rq = new Request(1, 100, "thread 1");
pthread_create(&tid, nullptr, sumCount, rq);
void *ret;
pthread_join(tid, &ret);
Response *rsp = static_cast(ret);
cout << "result " << rsp->_result << endl;
cout << "exitcode " << rsp->_exitcode << endl;
return 0;
}
默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放
资源,从而造成系统泄漏。
如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线
程资源。
int pthread_detach(pthread_t thread);
可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离:
pthread_detach(pthread_self());
joinable和分离是冲突的,一个线程不能既是joinable又是分离的。
#include
#include
#include
#include
#define N 10
struct threadData
{
std::string tid;
std::string threadname;
}
void *threadRoutine(void *args)
{
int i = 0;
while (i < 10)
{
std::cout << "pid: " << getpid() << std::endl;
}
return nullptr;
}
int main()
{
std::vector tids;
for (int i = 0; i < N; i++)
{
threadDatta td;
td.threadname =
td.tid =
pthread_create(&tid, nullptr, threadRoutine, &td);
tids.push_back(tid);
}
for (int i = 0; i < tids.size(); i++)
{
pthread_join(tids[i], nullptr);
}
return 0;
}
利用struct结构体创建在for循环中创建出一批线程,然后这些线程干其他的事情,这样写可不可以?不可以,因为threadDatta td;这个代码块是在for循环内部更是在主线程的栈中,而且还是一个临时变量。但是我们可以用指针,然后new出一块空间(threadData *td = new threadData;),这样这块空间是位于堆区。在执行for循环的时候,会new出来多个空间,每个线程访问的也是不同的堆空间,就算主线程中,for循环结束了,线程中把new出来的空间给保存下来。所有的线程,是共享堆空间的。
#include
#include
#include
#include
#define N 10
struct threadData
{
std::string tid;
std::string threadname;
};
void InitThreadData(threadData* td, int number, pthread_t tid)
{
td->threadname = "thread-" + std::to_string(number);
char buf[128];
snprintf(buf, sizeof(buf), "0x%x", tid);
td->tid = buf;
}
void *threadRoutine(void *args)
{
threadData *td = static_cast(args);
int i = 0;
while (i < 10)
{
std::cout << "pid: " << getpid() << ", tid : " << td->tid << ", threadname : " << td->threadname << std::endl;
sleep(1);
i++;
}
delete td;
return nullptr;
}
int main()
{
std::vector tids;
for (int i = 0; i < N; i++)
{
pthread_t tid;
threadData *td = new threadData;
InitThreadData(td, i, tid);
pthread_create(&tid, nullptr, threadRoutine, td);
tids.push_back(tid);
sleep(1);
}
for (int i = 0; i < tids.size(); i++)
{
pthread_join(tids[i], nullptr);
}
return 0;
}
代码不难理解,这样就创建出来了一批线程。
所有的线程执行的都是同一个函数。
#include
#include
#include
#include
#define N 10
struct threadData
{
std::string tid;
std::string threadname;
};
void InitThreadData(threadData* td, int number, pthread_t tid)
{
td->threadname = "thread-" + std::to_string(number);
char buf[128];
snprintf(buf, sizeof(buf), "0x%x", tid);
td->tid = buf;
}
void *threadRoutine(void *args)
{
threadData *td = static_cast(args);
int i = 0;
int test = 0;
while (i < 10)
{
std::cout << "pid: " << getpid() << ", tid : " << td->tid << " test " << &test << std::endl;
sleep(1);
i++;
}
delete td;
return nullptr;
}
int main()
{
std::vector tids;
for (int i = 0; i < N; i++)
{
pthread_t tid;
threadData *td = new threadData;
InitThreadData(td, i, tid);
pthread_create(&tid, nullptr, threadRoutine, td);
tids.push_back(tid);
sleep(1);
}
for (int i = 0; i < tids.size(); i++)
{
pthread_join(tids[i], nullptr);
}
return 0;
}
在一个函数中定义了一个test变量,然后每个线程都打印这个变量的地址。
每一个线程都会有自己独立的栈结构。其实线程和线程之间,几乎没有秘密,线程的栈上数据,也是可以被其他线程看到并访问的。
#include
#include
#include
#include
#define N 10
int test = 0;
struct threadData
{
std::string tid;
std::string threadname;
};
void InitThreadData(threadData* td, int number, pthread_t tid)
{
td->threadname = "thread-" + std::to_string(number);
char buf[128];
snprintf(buf, sizeof(buf), "0x%x", tid);
td->tid = buf;
}
void *threadRoutine(void *args)
{
threadData *td = static_cast(args);
int i = 0;
while (i < 10)
{
std::cout << "pid: " << getpid() << ", tid : " << td->tid << " test " << &test << std::endl;
sleep(1);
i++;
}
delete td;
return nullptr;
}
int main()
{
std::vector tids;
for (int i = 0; i < N; i++)
{
pthread_t tid;
threadData *td = new threadData;
InitThreadData(td, i, tid);
pthread_create(&tid, nullptr, threadRoutine, td);
tids.push_back(tid);
sleep(1);
}
for (int i = 0; i < tids.size(); i++)
{
pthread_join(tids[i], nullptr);
}
return 0;
}
将test定义为全局变量的话,各个线程打印出来的地址都是一样的。全局变量是被所有的线程同时看到并访问的。
线程可以要一个私有的全局变量吗?
可以
#include
#include
#include
#include
#define N 10
__thread int test = 0;
struct threadData
{
std::string tid;
std::string threadname;
};
void InitThreadData(threadData* td, int number, pthread_t tid)
{
td->threadname = "thread-" + std::to_string(number);
char buf[128];
snprintf(buf, sizeof(buf), "0x%x", tid);
td->tid = buf;
}
void *threadRoutine(void *args)
{
threadData *td = static_cast(args);
int i = 0;
while (i < 10)
{
std::cout << "pid: " << getpid() << ", tid : " << td->tid << " test " << &test << std::endl;
sleep(1);
i++;
}
delete td;
return nullptr;
}
int main()
{
std::vector tids;
for (int i = 0; i < N; i++)
{
pthread_t tid;
threadData *td = new threadData;
InitThreadData(td, i, tid);
pthread_create(&tid, nullptr, threadRoutine, td);
tids.push_back(tid);
sleep(1);
}
for (int i = 0; i < tids.size(); i++)
{
pthread_join(tids[i], nullptr);
}
return 0;
}
在全局变量的类型前面加上 __thread,线程就可以私有一份全局变量了,这种技术叫做线程的局部存储。__thread不是C++提供的东西,而是一个编译选项。只能定义内置类型,不能用来修饰自定义类型。
新电影出来了,电影院要卖票,我们可以模拟一下卖票的过程。
#include
#include
#include
#include
#include
#include
using namespace std;
#define N 4
int tickets = 1000; // 用多线程,模拟抢票
class threadData
{
public:
threadData(int number)
{
threadname = "thread-" + to_string(number);
}
public:
string threadname;
};
void *getTicket(void *args)
{
threadData* td = static_cast(args);
const char* name = td->threadname.c_str();
while (true)
{
if (tickets > 0)
{
usleep(1000);
printf("who=%s, get a ticket: %d\n", name, tickets);
tickets--;
}
else
{
break;
}
}
printf("%s ... quit\n", name);
return nullptr;
}
int main()
{
vector tids;
vector thread_datas;
for (int i = 1; i <= N; i++)
{
pthread_t tid;
threadData* td = new threadData(i);
thread_datas.push_back(td);
pthread_create(&tid, nullptr, getTicket, thread_datas[i - 1]);
tids.push_back(tid);
}
for (auto thread : tids)
{
pthread_join(thread, nullptr);
}
for (auto td : thread_datas)
{
delete td;
}
return 0;
}
在程序退出的时候,票抢到了-2。我们的代码写的是,>0才能抢票,但是居然出现了<0的数字。
为什么会出现这样的问题?
在所有线程执行的时候,tickets是一个共享数据,这个数据在多线程的并发访问下,造成了数据不一致问题。这个问题肯定和多线程并发访问是有关系的。那么就会出现,一个进程正在进行tickets--操作的时候,另一个进程读取到了--之前的数字,这就造成了数据不一致的问题。
想一下,对一个全局变量进行多线程并发--/++操作是否安全?
线程1读取数据,在读取的时候,线程可能会被切换,在任意时间,任意地点都可以切换。当线程1刚读取数据,就被切换了,要知道,寄存器不等于寄存器的内容。线程在执行的时候,将共享数据加载到CPU寄存器的本质是把数据的内存,变成了自己的上下文(这个变量的数据,以拷贝的形式,给自己单独拿了一份),此时线程2一直对tickets做--操作,在此过程中并没有切换线程。tickets已经被减到10了,再要切换线程的时候,线程2读取了当前tickets的数10并保存到自己的上下文中,切换到线程1,切换回来的时候并不会进行--操作,先恢复上下文,然后做--操作,此时线程1进行完--操作后,tickets由1000变成了999,然后写回到内存当中。之前线程2将tickets--到了10,经过线程1之后,票数回到了999,这就造成了数据不一致的问题。
假设票数为1,我们的判断是票数大于0就可以买票,当线程1判断之后,进入买票情况,此时切换到其他线程,tickets的数量没有改变,完成判断后,也进入了买票情况。票数就出现了不合理的情况。
如何解决??
解决这个问题,要做到3点:
做到这三点,本质上就是需要一把锁。Linux上提供的这把锁叫互斥量。
临界资源:多线程执行流共享的资源就叫做临界资源
临界区:每个线程内部,访问临界资源的代码,就叫做临界区
互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用
原子性(后面讨论如何实现):不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成
互斥量mutex
大部分情况,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量归属单个线程,其他线程无法获得这种变量。
但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完成线程之间的交互。
多个线程并发的操作共享变量,会带来一些问题。
可以将锁定义为全局变量,然后用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
不要销毁一个已经加锁的互斥量
已经销毁的互斥量,要确保后面不会有线程再尝试加锁
int pthread_mutex_destroy(pthread_mutex_t *mutex);
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
返回值:成功返回0,失败返回错误号
调用 pthread_ lock 时,可能会遇到以下情况:
互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功
发起函数调用时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量,那么pthread_ lock调用会陷入阻塞(执行流被挂起),等待互斥量解锁。
只有申请锁成功,才能往后执行,不成功,则阻塞。
加锁的本质其实就是用时间换安全。
加锁的表现:线程对于临界区代码串行执行。
加锁原则:尽量要保证临界区代码越少越好。
#include
#include
#include
#include
#include
#include
using namespace std;
#define N 4
int tickets = 1000; // 用多线程,模拟抢票
class threadData
{
public:
threadData(int number, pthread_mutex_t *mutex)
{
threadname = "thread-" + to_string(number);
lock = mutex;
}
public:
string threadname;
pthread_mutex_t *lock;
};
void *getTicket(void *args)
{
threadData* td = static_cast(args);
const char* name = td->threadname.c_str();
while (true)
{
pthread_mutex_lock(td->lock);
if (tickets > 0)
{
usleep(1000);
printf("who=%s, get a ticket: %d\n", name, tickets);
tickets--;
}
else
{
pthread_mutex_unlock(td->lock);
break;
}
pthread_mutex_unlock(td->lock);
}
printf("%s ... quit\n", name);
return nullptr;
}
int main()
{
pthread_mutex_t lock;
pthread_mutex_init(&lock, nullptr);
vector tids;
vector thread_datas;
for (int i = 1; i <= N; i++)
{
pthread_t tid;
threadData* td = new threadData(i, &lock);
thread_datas.push_back(td);
pthread_create(&tid, nullptr, getTicket, thread_datas[i - 1]);
tids.push_back(tid);
}
for (auto thread : tids)
{
pthread_join(thread, nullptr);
}
for (auto td : thread_datas)
{
delete td;
}
pthread_mutex_destroy(&lock);
return 0;
}
通过加锁就解决了数据不一致问题,但是只有thread-1这个线程在执行,没有切换到其他线程。
#include
#include
#include
#include
#include
#include
using namespace std;
#define N 4
int tickets = 1000; // 用多线程,模拟抢票
class threadData
{
public:
threadData(int number, pthread_mutex_t *mutex)
{
threadname = "thread-" + to_string(number);
lock = mutex;
}
public:
string threadname;
pthread_mutex_t *lock;
};
void *getTicket(void *args)
{
threadData* td = static_cast(args);
const char* name = td->threadname.c_str();
while (true)
{
pthread_mutex_lock(td->lock);
if (tickets > 0)
{
usleep(1000);
printf("who=%s, get a ticket: %d\n", name, tickets);
tickets--;
pthread_mutex_unlock(td->lock);
}
else
{
pthread_mutex_unlock(td->lock);
break;
}
usleep(13);
}
printf("%s ... quit\n", name);
return nullptr;
}
int main()
{
pthread_mutex_t lock;
pthread_mutex_init(&lock, nullptr);
vector tids;
vector thread_datas;
for (int i = 1; i <= N; i++)
{
pthread_t tid;
threadData* td = new threadData(i, &lock);
thread_datas.push_back(td);
pthread_create(&tid, nullptr, getTicket, thread_datas[i - 1]);
tids.push_back(tid);
}
for (auto thread : tids)
{
pthread_join(thread, nullptr);
}
for (auto td : thread_datas)
{
delete td;
}
pthread_mutex_destroy(&lock);
return 0;
}
添上一个usleep即可。
第一个代码只有一个线程执行(该线程的竞争能力比较强),这就导致锁分配不够合理,容易导致其他线程的饥饿问题。我们可以考虑让所有的线程获取锁,按照一定的顺序。按照一定顺序性获取资源就叫做同步。
当程序执行的时候,所有线程就要竞争锁这个资源,所以,锁本身也是共享资源。既然是共享资源,谁来保证锁的安全? 因此,申请锁和释放锁本身就被设计成了原子性操作。
在临界区中,线程可以被切换吗?可以切换。在线程被切出去的时候,是持有锁被切走的。我不在期间,照样没有线程能进入临界区访问临界资源。通过加锁保证我在访问临界区打的时候,对其他线程是原子的。
也可以先封装一个锁
#pragma once
#include
class Mutex{
public:
Mutex(pthread_mutex_t *lock):_lock(lock)
{}
void Lock()
{
pthread_mutex_lock(_lock);
}
void Unlock()
{
pthread_mutex_unlock(_lock);
}
~Mutex()
{}
private:
pthread_mutex_t *_lock;
};
class LockGuard
{
public:
LockGuard(pthread_mutex_t *lock):_mutex(lock)
{
_mutex.Lock();
}
~LockGuard()
{
_mutex.Unlock();
}
private:
Mutex _mutex;
};
利用类来完成锁的操作。
#include
#include
#include
#include
#include
#include
#include "mutex.hpp"
using namespace std;
#define N 4
int tickets = 1000; // 用多线程,模拟抢票
class threadData
{
public:
threadData(int number, pthread_mutex_t *mutex)
{
threadname = "thread-" + to_string(number);
lock = mutex;
}
public:
string threadname;
pthread_mutex_t *lock;
};
void *getTicket(void *args)
{
threadData* td = static_cast(args);
const char* name = td->threadname.c_str();
while (true)
{
{
LockGuard lg(td->lock); // 先调用构造函数,出了作用域在调用析构函数
if (tickets > 0)
{
usleep(1000);
printf("who=%s, get a ticket: %d\n", name, tickets);
tickets--;
pthread_mutex_unlock(td->lock);
}
else
{
pthread_mutex_unlock(td->lock);
break;
}
}
usleep(13);
}
printf("%s ... quit\n", name);
return nullptr;
}
int main()
{
pthread_mutex_t lock;
pthread_mutex_init(&lock, nullptr);
vector tids;
vector thread_datas;
for (int i = 1; i <= N; i++)
{
pthread_t tid;
threadData* td = new threadData(i, &lock);
thread_datas.push_back(td);
pthread_create(&tid, nullptr, getTicket, thread_datas[i - 1]);
tids.push_back(tid);
}
for (auto thread : tids)
{
pthread_join(thread, nullptr);
}
for (auto td : thread_datas)
{
delete td;
}
pthread_mutex_destroy(&lock);
return 0;
}
tickets-- 不是原子的,会变成三条汇编语句。原子:一条汇编语句就是原子的。
为了实现互斥锁操作,大多数体系结构都提供了swap或exchange指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性,即使是多处理器平台,访问内存的 总线周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。
死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所站用不会释放的资源而处于的一种永久等待状态 。
连续申请锁就会造成死锁。
死锁的四个必要条件
互斥条件:一个资源每次只能被一个执行流使用
请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放
不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺
循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系
避免死锁
破坏死锁的四个必要条件
加锁顺序一致
避免锁未释放的场景
资源一次性分配
在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题,叫做同步。
在上面的代码中,出现了一种情况,就是某一个线程的竞争能力很强,导致其他线程处于空闲状态导致的饥饿问题。这样的问题可以用条件变量来解决。条件变量必须依赖锁的使用
初始化
int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict
attr);
参数:
cond:要初始化的条件变量
attr:NULL
销毁
int pthread_cond_destroy(pthread_cond_t *cond)
等待条件满足
int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);
参数:
cond:要在这个条件变量上等待
mutex:互斥量,后面详细解释
唤醒等待
int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);
#include
#include
#include
int cnt = 0;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER; // 使用这个参数就不需要初始化和销毁了。
void *Count(void *args)
{
uint64_t number = (uint64_t)args;
pthread_detach(pthread_self());
while (true)
{
pthread_mutex_lock(&mutex);
pthread_cond_wait(&cond, &mutex); // 等待条件满足为什么写在这里?
// 因为当进行等待的时候,会自动释放锁。所以这个等待条件中会存在大量线程。
// 在main函数中的唤醒线程中,会唤醒线程(一般是第一个),如果用的是broadcast则唤醒一批线程。
std::cout << "pthread: " << number << " , cnt: " << cnt++ << std::endl;
pthread_mutex_unlock(&mutex);
sleep(3);
}
return nullptr;
}
int main()
{
for (uint64_t i = 0; i < 5; i++)
{
pthread_t tid;
pthread_create(&tid, nullptr, Count, (void*)i);
}
sleep(3);
std::cout << "main thread ctrl begin: " << std::endl;
while (true)
{
sleep(1);
pthread_cond_signal(&cond);//唤醒一个线程
std::cout << "signal one thread ... " << std::endl;
}
return 0;
}
换成 pthread_cond_broadcast。
唤醒一批线程。
要知道,让一个线程去休眠,也就是临界资源不就绪,没错,临界资源也是有状态的。怎么知道临界资源是就绪还是不就绪?判断出来的,判断就是访问临界资源,也就是判断必须在加锁之后。等待在加锁和解锁之间。
生产者消费者(consumer producter)模型
为什么要有仓库,消费者不可以直接找到生产者吗?那么生产者不就变成了生产者+仓库吗?仓库的存在是能提高效率的。快该过年了,生产者可以把仓库给塞满,让消费者进行消费,当仓库中的货快消费完的时候,生产者可以继续生产,不会出现仓库没货,消费者等着消费,生产者生产一个消费掉一个这种情况。
这个仓库就是一个共享资源,会存在一些问题
总结一下就是
优点
在多线程编程中阻塞队列(Blocking Queue)是一种常用于实现生产者和消费者模型的数据结构。其与普通的队列区别在于,当队列为空时,从队列获取元素的操作将会被阻塞,直到队列中被放入了元素;当队列满时,往队列里存放元素的操作也会被阻塞,直到有元素被从队列中取出(以上的操作都是基于不同的线程来说的,线程在对阻塞队列进程操作时会被阻塞)
利用C++STL中的queue来完成这个模型。
#pragma once
#include
#include
#include
template
class BlockQueue
{
static const int defalutnum = 5;
public:
BlockQueue(int maxcap = defalutnum):_maxcap(maxcap)
{
pthread_mutex_init(&_mutex, nullptr);
pthread_cond_init(&c_cond, nullptr);
pthread_cond_init(&p_cond, nullptr);
}
T pop()
{
pthread_mutex_lock(&_mutex);
if (_q.size() == 0)
{
pthread_cond_wait(&c_cond, &_mutex); // 等待之后谁来唤醒呢?,生产之后就有了数据,有了数据就可以消费了。
}
T out = _q.front();
_q.pop();
pthread_cond_signal(&p_cond);
pthread_mutex_unlock(&_mutex);
return out;
}
void push(const T& in)
{
/*
并不是你想生产就生产,当队列中的数量超过maxcap的时候,就需要进行等待
*/
pthread_mutex_lock(&_mutex);
if (_q.size() == _maxcap)
{
pthread_cond_wait(&p_cond, &_mutex); // 通过判断确定是否要进行等待
}
_q.push(in);
pthread_cond_signal(&c_cond);
pthread_mutex_unlock(&_mutex);
}
~BlockQueue()
{
pthread_mutex_destroy(&_mutex);
pthread_cond_destroy(&c_cond);
pthread_cond_destroy(&p_cond);
}
private:
std::queue _q; // 共享资源
// int _mincap;
int _maxcap; // 极值
pthread_mutex_t _mutex; // C++中的STL是线程不安全的,所以需要一把锁来保护它
pthread_cond_t c_cond;
pthread_cond_t p_cond;
};
#include "BlockQueue.hpp"
#include
void *Consumer(void *args)
{
BlockQueue *bq = static_cast*>(args);
while (true)
{
int data = bq->pop();
std::cout << "消费了一个数据 " << data << std::endl;
}
}
void *Productor(void *args)
{
BlockQueue *bq = static_cast*>(args);
int data = 0;
while (true)
{
data++;
bq->push(data);
std::cout << "生产了一个数据 " << data << std::endl;
}
}
int main()
{
BlockQueue *bq = new BlockQueue();
pthread_t c, p;
pthread_create(&p, nullptr, Productor, bq);
pthread_create(&c, nullptr, Consumer, bq);
pthread_join(c, nullptr);
pthread_join(p, nullptr);
delete bq;
return 0;
}
这是单线程版本的。生产五个数据,消费五个数据,也可以定义一个low和up变量,当作下界和上界,当队列中的数据 == low的时候,生产数据,当队列中的数据位于 low 和 up之间,就让消费者来消费。
多线程伪唤醒问题通常是指在多线程编程中,由于竞争条件或者错误使用条件变量等原因,导致线程在没有实际被唤醒的情况下似乎被唤醒了。这可能会引起程序逻辑错误或性能问题。
在这个pop函数当中,多个线程在wait中进行等待,生产者生产了一个数据在释放锁后,pop中在等待的线程获得了一个锁,然后通知生产者生产数据,并释放锁,此时,pop中wait中的线程会与生产者中的线程竞争这个锁资源,如果pop中的线程竞争到了这个资源,会继续往下执行代码,删除队列中的数据,此时上一个线程已经把队列中的数据给删除了,再次进行删除,就会删除错误的数据,如果队列中没有数据的话,再次进行pop操作,就会出现错误。 上面的代码中,是单生产单消费,几乎不会有这种问题,如果是多线程,可能就会出现这种问题了。
int main()
{
BlockQueue *bq = new BlockQueue();
pthread_t c[3], p[5];
for (int i = 0; i < 3; i++)
{
pthread_create(p + i, nullptr, Productor, bq);
}
for (int i = 0; i < 5; i++)
{
pthread_create(c + i, nullptr, Consumer, bq);
}
for (int i = 0; i < 3; i++)
{
pthread_create(p + i, nullptr, Productor, bq);
}
for (int i = 0; i < 5; i++)
{
pthread_join(p[i], nullptr);
}
for (int i = 0; i < 3; i++)
{
pthread_join(c[i], nullptr);
}
delete bq;
return 0;
}
这样就变成了多生产,多消费了。队列是所有生产者消费者的共享资源。
注意要将pop和push中的if改成while,避免出现伪唤醒的情况。
POSIX信号量和SystemV信号量作用相同,都是用于同步操作,达到无冲突的访问共享资源目的。 但POSIX可以用于线程间同步。
初始化信号量
#include
int sem_init(sem_t *sem, int pshared, unsigned int value);
参数:
pshared:0表示线程间共享,非零表示进程间共享
value:信号量初始值
销毁信号量
int sem_destroy(sem_t *sem);
等待信号量
功能:等待信号量,会将信号量的值减1
int sem_wait(sem_t *sem); //P()
发布信号量
功能:发布信号量,表示资源使用完毕,可以归还资源了。将信号量值加1。
int sem_post(sem_t *sem);//V()
环形队列采用数组模拟,用模运算来模拟环状特性。
环形结构起始状态和结束状态都是一样的,不好判断为空或者为满,所以可以通过加计数器或者标记位来判断满或者空。另外也可以预留一个空的位置,作为满的状态 。
基于环形队列的生产消费模型有三个原则
#pragma once
#include
#include
#include
const static int defaultcap = 5;
template
class RingQueue
{
// 对PV操作进行一个封装
private:
void P(sem_t &sem)
{
sem_wait(&sem);
}
void V(sem_t &sem)
{
sem_post(&sem);
}
public:
RingQueue(int cap = defaultcap):_ringqueue(cap),_cap(cap),_c_step(0),_p_step(0)
{
sem_init(&_cdata_sem, 0, 0);
sem_init(&_pspace_sem, 0, cap);
}
void push(const T &in) // 生产
{
P(_pspace_sem);
_ringqueue[_p_step++] = in;
V(_cdata_sem);
_p_step %= _cap; //维持环形特征
}
void pop(T *out) // 消费
{
P(_cdata_sem);
*out = _ringqueue[_c_step++];
V(_pspace_sem);
_c_step %= _cap;
}
~RingQueue()
{
sem_destroy(&_cdata_sem);
sem_destroy(&_pspace_sem);
}
private:
std::vector _ringqueue;
int _cap;
int _c_step; // 消费者下标
int _p_step; // 生产者下标
sem_t _cdata_sem; // 消费者关注的数据资源
sem_t _pspace_sem; // 生产者关注的空间资源
};
#include
#include "RingQueue.hpp"
#include
#include
#include
using namespace std;
void *Productor(void *args)
{
RingQueue *rq = static_cast*>(args);
while (true)
{
int data = rand() % 10 + 1;
rq->push(data);
cout << "Productor data : " << data << endl;
}
return nullptr;
}
void *Consumer(void* args)
{
RingQueue *rq = static_cast*>(args);
while (true)
{
int data = 0;
rq->pop(&data);
cout << "Consumer data : " << data << endl;
sleep(1);
}
return 0;
}
int main()
{
srand(time(nullptr));
RingQueue *rq = new RingQueue();
pthread_t c, p;
pthread_create(&c, nullptr, Productor, rq);
pthread_create(&p, nullptr, Consumer ,rq);
pthread_join(c, nullptr);
pthread_join(p, nullptr);
return 0;
}
这是一个单生产,单消费的例子。
现在改成多生产,多消费的例子。
#pragma once
#include
#include
#include
#include
const static int defaultcap = 5;
template
class RingQueue
{
// 对PV操作进行一个封装
private:
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 = defaultcap):_ringqueue(cap),_cap(cap),_c_step(0),_p_step(0)
{
sem_init(&_cdata_sem, 0, 0);
sem_init(&_pspace_sem, 0, cap);
pthread_mutex_init(&_c_mutex, nullptr);
pthread_mutex_init(&_p_mutex, nullptr);
}
void push(const T &in) // 生产
{
P(_pspace_sem);
Lock(_p_mutex);
_ringqueue[_p_step++] = in;
_p_step %= _cap; //维持环形特征
Unlock(_p_mutex);
V(_cdata_sem);
}
void pop(T *out) // 消费
{
P(_cdata_sem);
Lock(_c_mutex);
*out = _ringqueue[_c_step++];
_c_step %= _cap;
Unlock(_c_mutex);
V(_pspace_sem);
}
~RingQueue()
{
sem_destroy(&_cdata_sem);
sem_destroy(&_pspace_sem);
pthread_mutex_destroy(&_c_mutex);
pthread_mutex_destroy(&_p_mutex);
}
private:
std::vector _ringqueue;
int _cap;
int _c_step; // 消费者下标
int _p_step; // 生产者下标
sem_t _cdata_sem; // 消费者关注的数据资源
sem_t _pspace_sem; // 生产者关注的空间资源
pthread_mutex_t _c_mutex;
pthread_mutex_t _p_mutex;
};
#include
#include "RingQueue.hpp"
#include
#include
#include
using namespace std;
void *Productor(void *args)
{
RingQueue *rq = static_cast*>(args);
while (true)
{
int data = rand() % 10 + 1;
rq->push(data);
cout << "Productor data : " << data << endl;
}
return nullptr;
}
void *Consumer(void* args)
{
RingQueue *rq = static_cast*>(args);
while (true)
{
int data = 0;
rq->pop(&data);
cout << "Consumer data : " << data << endl;
sleep(1);
}
return 0;
}
int main()
{
srand(time(nullptr));
RingQueue *rq = new RingQueue();
pthread_t c[3], p[5];
for (int i = 0; i < 3; i++)
{
pthread_create(c + i, nullptr, Productor, rq);
}
for (int i = 0; i < 5; i++)
{
pthread_create(p + i, nullptr, Consumer, rq);
}
for (int i = 0; i < 5; i++)
{
pthread_join(p[i], nullptr);
}
for (int i = 0; i < 3; i++)
{
pthread_join(c[i], nullptr);
}
return 0;
}
一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利用,还能防止过分调度。可用线程数量应该取决于可用的并发处理器、处理器内核、内存、网络sockets等的数量。
线程池的应用场景:
#pragma once
#include
#include
#include
#include
#include
struct ThreadInfo
{
pthread_t tid;
std::string name;
};
static const int defaultnum = 5;
template
class ThreadPool
{
public:
void Lock()
{
pthread_mutex_lock(&_mutex);
}
void Unlock()
{
pthread_mutex_unlock(&_mutex);
}
void Wakeup()
{
pthread_cond_signal(&_cond);
}
void ThreadSleep()
{
pthread_cond_wait(&_cond, &_mutex);
}
bool IsQueueEmpty()
{
return _task.empty();
}
public:
ThreadPool(int num = defaultnum):_threads(num)
{
pthread_mutex_init(&_mutex, nullptr);
pthread_cond_init(&_cond, nullptr);
}
static void *Handler(void* args)
{
ThreadPool *tp = static_cast*>(args);
while (true)
{
tp->Lock();
while (tp->IsQueueEmpty())
{
tp->ThreadSleep();
}
T t = tp->Pop();
tp->Unlock();
t();
}
}
T Pop()
{
T t = _task.front();
_task.pop();
return t;
}
void Start()
{
int num = _threads.size();
for (int i = 0; i < num; i++)
{
_threads[i].name = "thread-" + std::to_string(i + 1);
pthread_create(&_threads[i].tid, nullptr, Handler, this);
}
}
void Push(const T& t)
{
Lock();
_task.push(t);
Wakeup();
Unlock();
}
~ThreadPool()
{
pthread_mutex_destroy(&_mutex);
pthread_cond_destroy(&_cond);
}
private:
std::vector _threads;
std::queue _task;
pthread_mutex_t _mutex;
pthread_cond_t _cond;
};
#pragma once
#include
#include
std::string opers="+-*/%";
enum{
DivZero=1,
ModZero,
Unknown
};
class Task
{
public:
Task()
{}
Task(int x, int y, char op) : data1_(x), data2_(y), oper_(op), result_(0), exitcode_(0)
{
}
void run()
{
switch (oper_)
{
case '+':
result_ = data1_ + data2_;
break;
case '-':
result_ = data1_ - data2_;
break;
case '*':
result_ = data1_ * data2_;
break;
case '/':
{
if(data2_ == 0) exitcode_ = DivZero;
else result_ = data1_ / data2_;
}
break;
case '%':
{
if(data2_ == 0) exitcode_ = ModZero;
else result_ = data1_ % data2_;
} break;
default:
exitcode_ = Unknown;
break;
}
}
void operator ()()
{
run();
}
std::string GetResult()
{
std::string r = std::to_string(data1_);
r += oper_;
r += std::to_string(data2_);
r += "=";
r += std::to_string(result_);
r += "[code: ";
r += std::to_string(exitcode_);
r += "]";
return r;
}
std::string GetTask()
{
std::string r = std::to_string(data1_);
r += oper_;
r += std::to_string(data2_);
r += "=?";
return r;
}
~Task()
{
}
private:
int data1_;
int data2_;
char oper_;
int result_;
int exitcode_;
};
#include
#include "ThreadPool.hpp"
#include "Task.hpp"
#include
int main()
{
srand(time(nullptr));
ThreadPool *tp = new ThreadPool(5);
tp->Start();
while (true)
{
int x = rand() % 100 + 1;
int y = rand() % 100 + 1;
char op = opers[rand() % opers.size()];
Task t(x, y, op);
tp->Push(t);
std::cout << "main thread make task: " << t.GetTask() << std::endl;
sleep(1);
}
}