线程的概念
线程的组成:
栈区和栈区指针
程序计数器:PC
寄存器集合
线程的状态:
新建状态(New):刚被创建
准备状态(Runnable):加载所需的所有资源,等待CPU
运行状态(Running):被CPU执行
挂起状态(Blocked):阻塞,等待唤醒
退出状态:
线程和进程的区别:
-
进程是资源分配的最小单元,线程是程序执行的最小单元。一个进程可以由一个或多个进程组成。
-
从内存上:进程创建时会被分配地址空间,并且包含以下几种内存空间:堆区、栈区、代码区、全局变量区。
线程创建时会分配线程的私有栈,包括:维护参数和局部变量线程栈区,程序计数器(维护线程挂起再运行),寄存器集合等。
线程共享进程中除了线程上下文外的所有内存空间,包括(文件、系统资源等)
-
从效率上:进程包含线程,并且拥有更多的数据结构需要维护。所以切换或者创建,进程的效率要慢于线程。
-
安全性上:进程间有独立的地址空间,安全性较好;线程间虽然有私有的栈区,当理论上只要知道栈帧地址即可修改其他线程的变量。
线程的使用:
C++11之前:
- __beginthreadex ( process.h中)
接口介绍:
unsigned long _beginthread(
void(_cdecl *start_address)(void *), //声明为void (*start_address)(void *)形式
unsigned stack_size, //是线程堆栈大小,一般默认为0
void *arglist //向线程传递的参数,一般为结构体
);
unsigned long _beginthreadex( //推荐使用
void *security, //安全属性,NULL表示默认安全性
unsigned stack_size, //是线程堆栈大小,一般默认为0
unsigned(_stdcall *start_address)(void *), //声明为unsigned(*start_address)(void *)形式
void *argilist, //向线程传递的参数,一般为结构体
unsigned initflag, //新线程的初始状态,0表示立即执行,CREATE_SUSPEND表示创建后挂起(可用ResumeThread唤醒)。
unsigned *thrdaddr //该变量存放线程标识符,它是CreateThread函数中的线程ID。
); //创建成功条件下的将线程句柄转化为unsigned long型返回,创建失败条件下返回0
使用示例:
#include<iostream>
#include "windows.h"
#include "process.h"
using namespace std;
unsigned __stdcall add100(void*) {
long long sum = 0;
for (int i = 0; i < 1000000; i++) {
sum += i;
}
cout << sum << endl;
return 1;
}
int main()
{
// 开启线程
unsigned int threadId;
HANDLE hd1 = (HANDLE)_beginthreadex(NULL, 0, add100, NULL, NORMAL_PRIORITY_CLASS, &threadId);
// 阻塞,等待线程函数结束
WaitForSingleObject(hd1, INFINITE);
// 获取线程函数的返回值,线程函数如果没有执行return,则返回默认值
DWORD dwExitCode;
GetExitCodeThread(hd1, &dwExitCode);
}
__beginthreadex内部实现是调用CreareThread,但一般不推荐直接使用CreateThread,因为前者做了许多安全保护的工作。
具体原有参考:https://www.cnblogs.com/ay-a/p/9135652.html
中介三种创建线程的方式:
1) Create/EndThread是Win32方法开始/结束一个线程
2) _beginthreadx/_endthreadex是C RunTime方式开始/结束一个线程
3) AfxBeginThread是在MFC中开始/结束一个线程
https://www.cnblogs.com/lujin49/p/4557655.html
Note:
-
直接在CreateThread API创建的线程中使用sprintf,malloc,strcat等涉及CRT存储堆操作的CRT库函数是很危险的,容易造成线程的意外中止。 在使用_beginthread和_beginthreadex创建的线程中可以安全的使用CRT函数。但是必须在线程结束的时候相应的调用_endthread或_endthreadex
-
_beginthread成对调用的_endthread函数内部隐式的调用CloseHandle关闭了线程句柄,而与_beginthreadex成对使用的_endthreadex则没有关闭线程的句柄,需要显示的调用CloseHandle关闭线程句柄,不要使用_beginthread,使用._beginthreadex代替之
-
尽量不要在一个MFC程序中使用_beginthreadex()或CreateThread()。
-
没有使用到MFC的线程尽量用_beginthreadex启动
C++11之后:
1. thread (thread.h中)
使用方式:所有可执行的对象都可以放入thread中,包括,全局函数、类的成员函数、lambda表达式等。
#include<iostream>
#include "thread"
using namespace std;
int add100(int cnt) {
long long sum = 0;
for (int i = 0; i < cnt; i++) {
sum += i;
}
cout << sum << endl;
return 1;
}
class A{
public:
A() {}
void test(int t) {
this_thread::sleep_for(chrono::seconds(t));
cout << "sleep seconds: " << t << endl;
}
};
int main()
{
// 1. 普通函数放入线程执行
thread t1(add100, 100000);
// 2. lambda表达式方式线程执行
thread t2([] {
this_thread::sleep_for(chrono::seconds(2));
cout << "sleep 2 seconds. " << endl;
});
// 3. 类的成员变量放入线程中执行
A a;
thread t3(&A::test, a, 3);
t1.join();
t2.join();
t3.join();
}
join:
等待子线程结束,阻塞。
调用后线程状态joinable()处于false,线程资源被回收,只能调用一次。
detach:
将主线程与子线程分离,即不需要等待子线程结束,主线程也可以退出并不会报错。子线程的管理交由C++运行时库。
joinable()返回fasle,只能调用一次。
但线程函数退出结束时,系统自动回收线程资源。
yield:
交出当前线程的时间片,让当前线程放弃执行,让操作系统优先调用其他线程执行。
比如某个线程要等待某个变量,如果用死循环不断判断变量会耗费CPU性能,可以在等待时调用yield,交出时间片。
while(!isDone()); // Bad
while(!isDone()) yield(); // Good
thread对象的析构:
- thread对象析构时会判断,线程是否处于joinable()的状态,如果是joinable的状态会导致程序直接terminate,所以线程对象析构时要保证线程不是joinable的状态
也即是说在线程对象析构前要显示的调用join()或detach()。
- 可利用RAII的策略保证线程对象在析构时为不可joinable的,即封装下thread。
3.
Note:
-
线程thread对象无法被复制或拷贝,只能被move或swap
-
detach后不能调用join,同样join之后不能调用detach
其他关于C++11多线程的用法查考future。
lock_guard
类 lock_guard
是互斥体包装器,为在作用域块期间占有互斥提供便利 RAII 风格机制。
创建 lock_guard
对象时,它试图接收给定互斥的所有权。控制离开创建 lock_guard
对象的作用域时,销毁 lock_guard
并释放互斥。
lock_guard
类不可复制。
相当于:
mutex mtx;
{
mtx.lock();
// Do Your Jobs
mtx.unlock();
}
{
lock_guard<mutex> lck(mtx);
// Do Your Jobs
}
unique_lock
类unique_lock 是通用互斥包装器,允许延迟锁定、锁定的有时限尝试、递归锁定、所有权转移和与条件变量一同使用。
类unique_lock 可移动,但不可复制——它满足可移动构造 (MoveConstructible)和可移动赋值 (MoveAssignable)但不满足可复制构造 (CopyConstructible)或可复制赋值 (CopyAssignable)。
* 独占所有权,就是没有其他的 unique_lock 对象同时拥有某个 mutex 对象的所有权。
* std::condition_variable 对象通常使用 std::unique_lock 来等待
call_once
对于多线程同时调用初始化函数这个问题,传统的方法可能使用“双重检查锁定“的方法,可以搜索单例模式的实现方法。
对于这种常见有比较麻烦处理的问题,C++11提出了新的解决方案。即call_once
双重检查锁定双重检查锁定”
template< class Callable, class... Args >
void call_once( std::once_flag& flag, Callable&& f, Args&&... args );
auto f = []() // 在线程里运行的lambda表达式
{
std::call_once(flag, // 仅一次调用,注意要传flag
[](){ // 匿名lambda,初始化函数,只会执行一次
cout << "only once" << endl;
} // 匿名lambda结束
); // 在线程里运行的lambda表达式结束
};
thread t1(f); // 启动两个线程,运行函数f
thread t2(f);
当然,如果call_once运行的函数有参数的话,只会保证参数相同时只执行一次,参数不同时会再次执行。
thread_local
多线程读写一个全局变量,如果这个全局变量可以在不同线程中有不同的值,也就是现在独占这个变量。
可能的场景:线程开启前需要对这个变量进行初始化,后续线程运行就可以独占这个变量。这时可以使用thread_local这个关键字来修饰变量,然后再对这个变量访问时就不需要进行锁的操作。
而且每个线程中这个变量值独立。
原文链接: https://www.cnblogs.com/dylan-liang/p/14751094.html
欢迎关注
微信关注下方公众号,第一时间获取干货硬货;公众号内回复【pdf】免费获取数百本计算机经典书籍
原创文章受到原创版权保护。转载请注明出处:https://www.ccppcoding.com/archives/210378
非原创文章文中已经注明原地址,如有侵权,联系删除
关注公众号【高性能架构探索】,第一时间获取最新文章
转载文章受原作者版权保护。转载请注明原作者出处!