在单核CPU时代,程序的执行效率单纯的依赖更快的硬件;随着时间的变化,在2005年3月,Herb Sutter在Dobb's Journal发表了《The Free Lunch is over: A Fundamental Turn Toward Concurrency is Software》一文,文章分析处理器厂商改善CPU性能的传统方法,如提升时钟速度和指令吞吐量的方式,遇到了瓶颈,处理器发展方向趋向于超线程和多架构模式。
为了让代码执行的更快、更高效,开发人员可通过编写并发代码,充分大会多核处理器的优势,使程序性能得到提升。
线程与进程有着千丝万缕的联系,两者密不可分。在Linux下程序或可执行文件只是一组指令的集合,没有执行的含义。进程拥有自己独立的进程地址空间和上下文堆栈,但对一个程序本身的执行操作来说,进程不执行任何代码,只是提供一个大环境容器,进程中实际执行体是线程,因此每个进程中至少的有一个线程,即主线程。
多进程、多线程都能做并发,两者的差异何在?
进程之间,彼此的地址空间是独立的,但线程会共享内存地址空间。同一个进程的多个线程共享一份全局内存区域,包括初始化数据段、未初始化数据段和动态分配的堆内存。
这种共享方式给线程带来了很多优势:
创建线程花费的时间少于创建进程花费的时间 线程间的上下文切换小于进程间的上下文切换 线程间数据共享比进程之间的共享要简单 线程的缺点:
多个线程中只要有一个线程出现BUG,就会出现SegmenFault导致进程退出,相比之下进程的地址空间互相独立,多个进程通过进程间通信(IPC),一个进程的异常退出不会影响其他进程 多线程控制流、时许很难控制,每次执行都不一样 多进程和多线程都能够很好的发挥出多核处理器的性能,两者孰优孰劣需要依赖具体的应用场景去抉择。
在内核2.6以前的调度实体都是进程,内核并没有真正执行线程,为了遵循POSIX标准,Redhat发布了NPTL(native posix thread library)库,它的实现方式是,在内核中线程仍然被当作进程,也是用task_struct进行描述,这种线程又叫LWP(用户态线程)。
Struct task_struct {
…
pid_t pid; //线程id
pid_t tgid; //Thread Group ID(对应用户层的进程ID)
…
struct task_struct *group_leader;
struct list_head thread_group;
…
}
没有线程之前,一个进程对应内核的一个进程描述符,对应一个进程ID。但是引入线程概念后,情况发生了变化,一个进程下管辖的N个线程。每个线程作为一个独立的调度实体在内核中有自己的进程描述符。进程和内核的进程描述符由1:1变成1:n。POSIX标准要求所有线程调用getpid函数返回相同的进程ID。为了解决这个问题,引入了线程组的概念。
线程组内的第一个线程,在用户态被称为主线程,在内核中被称为Group Leader.内核在创建第一个线程时,会将线程组ID的值设置成第一个线程ID,group_leader指针则指向自身,即主线程的进程描述符。
//线程组ID等于主线程ID
p->tgid = p->pid;
p->group_leader = p;
INIT_LIST_HEAD(&p->thread_group);
通过group_leader指针,每个线程都能够找到主线程,主线程中存在一个链表头结构,后面创建的每一个线程都会被链入到该双向链表中。通过链表结构能偶遍历组内其他线程,也能够找到主线程。
Linux线程ID:
用ps -eLf查询得,进程syslog(680)组内有4个线程,每个线程的ID号分别为680、703、704、705共4个线程。
yp@ubuntu:~$ ps -eLf
UID PID PPID LWP C NLWP STIME TTY TIME CMD
........
syslog 680 1 680 0 4 01:02 ? 00:00:00 /usr/sbin/rsyslogd -n
syslog 680 1 703 0 4 01:02 ? 00:00:00 /usr/sbin/rsyslogd -n
syslog 680 1 704 0 4 01:02 ? 00:00:00 /usr/sbin/rsyslogd -n
syslog 680 1 705 0 4 01:02 ? 00:00:00 /usr/sbin/rsyslogd -n
在已知进程ID前提下,可在/proc/PID/task目录下的子目录来查看进程内的线程个数及其线程ID。示例如下:
yp@ubuntu:~$ ll /proc/680/task/
total 0
dr-xr-xr-x 6 syslog syslog 0 Jul 5 03:36 ./
dr-xr-xr-x 9 syslog syslog 0 Jul 5 01:02 ../
dr-xr-xr-x 7 syslog syslog 0 Jul 5 03:36 680/
dr-xr-xr-x 7 syslog syslog 0 Jul 5 03:36 703/
dr-xr-xr-x 7 syslog syslog 0 Jul 5 03:36 704/
dr-xr-xr-x 7 syslog syslog 0 Jul 5 03:36 705/
调用pthread_create函数时,函数调用成功,通过第一个参数返回
#include <pthread.h>
pthread_t tid;
pthread_create(&tid, NULL, thread_proc, NULL);
在对应线程中调用pthread_self()
#include <pthread.h>
pthread_t tid = pthread_self();
系统调用
#include <sys/syscall.h>
int tid = syscall(SYS_gettid);
pthread_t输出的是一块内存地址空间,由于进程的虚拟地址与物理地址的映射关系,pthread_t类型的线程ID可能不是全系统唯一的。通过syscall(SYS_gettid)获取的线程ID是系统范围内全局唯一的。
Linux互斥量与Windows的临界区对象用法相似,也是通过限制多个线程同时访问临界资源以达到保护的目的。Linux互斥量在NPTL中实现,使用数据结构pthread_mutex_t表示一个互斥体对象。
互斥量初始化
(1) 使用PTHREAD_MUTEX_INITIALIZER直接给互斥体变量赋值
#include <pthread.h>
pthread_mutex_t = PTHREAD_MUTEX_INITIALIZER;
(2) 若互斥量是动态分配或者需要设置其属性,则使用pthread_mutex_init函数
int pthread_mutex_init(pthread_mutex_t* restrict_mutex,
const pthread_mutexattr_init* restrict_attr)
当我们不在需要一个互斥体对象时,可以使用pthread_mutex_destroy函数来销毁它,函数签名如下:
int pthread_mutex_lock(pthread_mutex_t* mutex);
int pthread_mutex_trylock(pthread_mutex_t* mutex);
int pthread_mutex_unlock(pthread_mutex_t* mutex);
设置互斥量的属性
设置互斥体属性时需要创建一个pthread_mutexattr_t类型的对象,并用pthread_mutexattr_init进行初始化。
#include <pthread.h>
int main()
{
...
pthread_mutexattr_t mtx_attr;
pthread_mutexattr_init(&mtx_attr);
pthread_mutexattr_settype(&mtx_attr, PTHREAD_MUTEX_NORMAL); //普通锁
pthread_mutex_init(&g_mutex, &mtx_attr);
...
}
互斥体的加锁解锁
互斥量一般配合加锁、解锁操作完成对临界资源的安全访问,Linux相关函数签名如下:
int pthread_mutex_lock(pthread_mutex_t* mutex);
int pthread_mutex_trylock(pthread_mutex_t* mutex);
int pthread_mutex_unlock(pthread_mutex_t* mutex);
-
为什么使用环境变量?
不使用条件变量的情形,伪代码如下:
pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;
int WaitForTrue()
{
pthread_mutex_lock(&mtx);
while (condition is false) // 条件不满足
{
pthread_mutex_unlock(&mtx); //解锁,等待其他线程改变共享数据
sleep(n); //睡眠等待,再次加锁验证条件是否满足
pthread_mutex_lock(&mtx);
}
}
这段代码对共享资源进行访问时存在严重的效率问题。假设等待的条件突然满足了,但是while循环仍然会消耗一下时间,响应不及时。
因此需要一种机制:线程在条件不满足的时候,主动让出互斥量,让其他线程执行,其他线程发现条件满足后向其发送信号,就可以立即唤醒它。
-
条件变量的使用
环境变量的初始化和销毁,使用如下API函数:
int pthread_cond_init(pthread_cond_t* cond, const pthread_condattr_t* attr);
int pthread_cond_destory(pthread_cond_t* cond);
可使用如下API函数等待条件变量被唤醒:
int pthread_cond_wait(pthread_cond_t* restrict cond, pthread_mutex_t* restrict mutex);
int pthread_cond_timedwait(pthread_cond_t* restrict cond, pthread_mutex_t* restrict mutex
const struct timespec* restrict abstime);
如果条件变量等待的条件没有被满足,则条用pthread_cond_wait的线程会一直阻塞,pthread_cond_timedwait是带超时时间的阻塞,超过abstime设置的时间后,立即返回。
因调用pthread_cond_wait而阻塞的线程可被如下API唤醒:
int pthread_cond_signal(pthread_cond_t* cond);
int pthread_cond_broadcast(pthread_cond_t* cond);
如果多个线程阻塞在pthread_cond_wait,由于pthread_cond_signal一次只能唤醒一个线程,具体那个线程被唤醒是随机的。pthread_cond_broadcast可以唤醒所有等待的线程。
- 条件变量虚假唤醒
if (tasks.empty()) {
pthread_cond_wait(&cv, &mutex);
}
while (tasks.empty()) {
pthread_cond_wait(&cv, &mutex);
}
没有其他线程向条件变量发送信号,但等待此条件变量的线程可能被唤醒,即操作系统唤醒pthread_cond_wait时,tasks.empty()仍然为true,这种情形称为虚假唤醒。因此需要在while循环中,当条件变量被唤醒时,还需要判断条件是否满足。
互斥量的使用一般是通过lock()方法来阻塞线程,直到获得互斥量的所有权为止。在线程获得互斥量并完成任务后,就必须使用unlock()来解除对互斥量的占用,lock ()和unlock()必须成对出现。代码示例如下:
#include <iostream>
#include <thread>
#include <mutex>
#include <chrono>
std::mutex g_lock;
void ThreadProc()
{
g_lock.lock();
std::cout << "entered thread " << std::this_thread::get_id() << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(1));
std::cout << "leaving thread " << std::this_thread::get_id() << std::endl;
g_lock.unlock();
}
int main(void)
{
std::thread t1(ThreadProc);
std::thread t2(ThreadProc);
t1.join();
t2.join();
system("pause");
return 0;
}
使用lock_guard()可以简化lock/unlock写法,避免忘记unlock()。lock_guard()出作用域会自动解锁。
void ThreadProc()
{
std::lock_guard<std::mutex> locker(g_lock);
std::cout << "entered thread " << std::this_thread::get_id() << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(1));
std::cout << "leaving thread " << std::this_thread::get_id() << std::endl;
}
#include <iostream>
#include <thread>
#include <mutex>
#include <unistd.h>
int g_resourceID = 1;
std::recursive_mutex mtx;
void ThreadFunc_2() {
mtx.lock(); //第二次上锁
g_resourceID = g_resourceID * g_resourceID ;
printf("g_resourceID = %d \n", g_resourceID );
mtx.unlock();
}
void ThreadFunc_1() {
mtx.lock();
g_resourceID = g_resourceID + 1;
ThreadFunc_2();
printf("g_resourceID = %d \n", g_resourceID );
mtx.unlock();
}
int main() {
std::thread t1(ThreadFunc_1);
t1.join();
return 0;
}
递归互斥锁可对同一互斥量多次加锁,可以用来解决同一线程对互斥量多次加锁导致死锁阻塞的问题。
C++11使用condition_variable类表示条件变量,与上文介绍的Linux系统原生的条件变量一样,此外还提供了等待条件满足的wait方法(wait、wait_for、wait_until)。发送信号使用notify方法(notify_one、notify_all)。与Linux系统的条件变量相比,C++11的std::condition_variable不在需要显示地初始化和销毁。
#include <thread>
#include <condition_variable>
#include <mutex>
#include <list>
#include <iostream>
template<typename T>
class SimpleSyncQueue
{
public:
SimpleSyncQueue() { }
void Put(const T& x)
{
std::lock_guard<std::mutex> locker(m_mutex);
m_queue.push_back(x);
m_notEmpty.notify_one();
———————————————— 版权声明:本文为CSDN博主「FooNY」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:https://blog.csdn.net/zouph/article/details/125560751