Visual Studio 11增强支持的标准 C + + 11
现在支持此预览的 Visual Studio 的 STL 中的新头文件可以进行多线程编程和异步操作管理。
<thread>,<future>,<atomic>,<time>,<mutex>,<condition_variable>,<ratio>,<filesystem>
头文件<thread>作为其名称来创建和操作线程
- 1.thread t([]()
- 2. {
- 3. cout << "ThreadID : " << std::this_thread::get_id() << endl;
- 4. });
- 5. t.join();
1.thread t([]()
2. {
3. cout << "ThreadID : " << std::this_thread::get_id() << endl;
4. });
5. t.join();
这是传递给线程的类的构造函数的一种方法,而不是在这里我们使用Lambda 表达式中引入C + + 11Join ()方法,这是一个调用阻塞,使主线程等待,直到线程完成他的工作。如果要解耦变量的类型线程,线程在 Windows 那里 调用 的detach()方法,这样做违背计划的detach()方法,不会影响与线程句柄关联的窗口 (CloseHandle)。因此可能是使用变量的 t 型线,旧 Windows API 通过检索的本机句柄,但代码将成为便携式少得多。
- 1.WaitForSingleObject(t.native_handle()._Hnd ,INFINITE);
- 2. t.detach();
1.WaitForSingleObject(t.native_handle()._Hnd ,INFINITE);
2. t.detach();
在线程, join ()方法是实质相同,上述代码 (在 Windows 平台) 。
很可能也与要检索的可用使用hardware_concurrency()方法的虚拟处理器数目的线程 ,
- unsigned numLogicalProc=t.hardware_concurrency();
unsigned numLogicalProc=t.hardware_concurrency();
操作的线程,总是会对同步与保护的关键地区。头<mutex>提供这种排斥同步对象相互示例的效果
注意,使用锁来总是对性能的影响 !
- std::this_thread::sleep_for (chrono::seconds(1));
- 5. for(int i=0;i<10;i++)
- 6. {
- 7. m.lock();
- 8. cout << "ThreadID : " << std::this_thread::get_id() << ":" << i << endl;
- 9. m.unlock ();
- 10. }
- 11.});
- 12.thread t2([&m]()
10. 13.{
11. 14. std::this_thread::sleep_for (chrono::seconds(1));
12. 15. for(int i=0;i<10;i++)
13. 16. {
14. 17. m.lock ();
15. 18. cout << "ThreadID : " << std::this_thread::get_id() << ":" << i << endl;
16. 19. m.unlock();
17. 20. }
18. 21.});
19. 22.t1.join();
20. 23.t2.join();
std::this_thread::sleep_for (chrono::seconds(1));
5. for(int i=0;i<10;i++)
6. {
7. m.lock();
8. cout << "ThreadID : " << std::this_thread::get_id() << ":" << i << endl;
9. m.unlock ();
10. }
11.});
12.thread t2([&m]()
13.{
14. std::this_thread::sleep_for (chrono::seconds(1));
15. for(int i=0;i<10;i++)
16. {
17. m.lock ();
18. cout << "ThreadID : " << std::this_thread::get_id() << ":" << i << endl;
19. m.unlock();
20. }
21.});
22.t1.join();
23.t2.join();
注意this_thread命名空间以检索当前线程的标识号或时间类结合创建点的介绍.
它也是执行的可以控制对生产者/消费者下面的示例使用头文件<condition_variable>,作为多个线程流。
注意到我们使消费者和生产者为互斥体,我们转向方法wait()变量的类型condition_variable_any (它可能还使用condition_variable unique_lock <mutex>型,后者互斥体直接传递到类型unique_lock的初始化过程中未报告的状态。非终止状态指示可以获得互斥体。)
- 1.mutex lockBuffer;
- 2.volatile BOOL ArretDemande=FALSE;
- 3.queue<long> buffer;
- 4.condition_variable_any cndNotifierConsommateurs;
- 5.condition_variable_any cndNotifierProducteur;
- 6.
- 7.thread ThreadConsommateur([&]()
- 8.{
- 9.
10. 10. while(true)
11. 11. {
12. 12.
13. 13. lockBuffer.lock ();
14. 14. while(buffer.empty () && ArretDemande==FALSE)
15. 15. {
16. 16. cndNotifierConsommateurs.wait(lockBuffer);
17. 17. }
18. 18. if (ArretDemande==TRUE && buffer.empty ())
19. 19. {
20. 20. lockBuffer.unlock();
21. 21. cndNotifierProducteur.notify_one ();
22. 22. break;
23. 23. }
24. 24.
25. 25. long element=buffer.front();
26. 26. buffer.pop ();
27. 27. cout << "Consommation element :" << element << " Taille de la file :" << buffer.size() << endl;
28. 28.
29. 29. lockBuffer.unlock ();
30. 30. cndNotifierProducteur.notify_one ();
31. 31. }
32. 32.
33. 33.});
34. 34.
35. 35.thread ThreadProducteur([&]()
36. 36.{
37. 37. //Operation atomic sur un long
38. 38. std::atomic<long> interlock;
39. 39. interlock=1;
40. 40. while(true)
41. 41. {
42. 42. ////Simule une charge
43. 43. std::this_thread::sleep_for (chrono::milliseconds (15));
44. 44. long element=interlock.fetch_add (1);
45. 45. lockBuffer.lock ();
46. 46. while(buffer.size()==10 && ArretDemande ==FALSE)
47. 47. {
48. 48.
49. 49. cndNotifierProducteur.wait (lockBuffer);
50. 50. }
51. 51. if (ArretDemande==TRUE)
52. 52. {
53. 53.
54. 54. lockBuffer.unlock ();
55. 55. cndNotifierConsommateurs.notify_one ();
56. 56. break;
57. 57. }
58. 58. buffer.push(element);
59. 59. cout << "Production unlement :" << element << " Taille de la file :" << buffer.size() << endl;
60. 60. lockBuffer.unlock ();
61. 61. cndNotifierConsommateurs.notify_one ();
62. 62. }
63. 63.
64. 64.});
65. 65.
66. 66.
67. 67.std::cout << "Pour arreter pressez [ENTREZ]" << std::endl;
68. 68.getchar();
69. 69.
70. 70.std::cout << "Arret demande" << endl;
71. 71.ArretDemande=TRUE;
72. 72.
73. 73.ThreadProducteur.join();
74. 74.ThreadConsommateur.join();
1.mutex lockBuffer;
2.volatile BOOL ArretDemande=FALSE;
3.queue<long> buffer;
4.condition_variable_any cndNotifierConsommateurs;
5.condition_variable_any cndNotifierProducteur;
6.
7.thread ThreadConsommateur([&]()
8.{
9.
10. while(true)
11. {
12.
13. lockBuffer.lock ();
14. while(buffer.empty () && ArretDemande==FALSE)
15. {
16. cndNotifierConsommateurs.wait(lockBuffer);
17. }
18. if (ArretDemande==TRUE && buffer.empty ())
19. {
20. lockBuffer.unlock();
21. cndNotifierProducteur.notify_one ();
22. break;
23. }
24.
25. long element=buffer.front();
26. buffer.pop ();
27. cout << "Consommation element :" << element << " Taille de la file :" << buffer.size() << endl;
28.
29. lockBuffer.unlock ();
30. cndNotifierProducteur.notify_one ();
31. }
32.
33.});
34.
35.thread ThreadProducteur([&]()
36.{
37. //Operation atomic sur un long
38. std::atomic<long> interlock;
39. interlock=1;
40. while(true)
41. {
42. ////Simule une charge
43. std::this_thread::sleep_for (chrono::milliseconds (15));
44. long element=interlock.fetch_add (1);
45. lockBuffer.lock ();
46. while(buffer.size()==10 && ArretDemande ==FALSE)
47. {
48.
49. cndNotifierProducteur.wait (lockBuffer);
50. }
51. if (ArretDemande==TRUE)
52. {
53.
54. lockBuffer.unlock ();
55. cndNotifierConsommateurs.notify_one ();
56. break;
57. }
58. buffer.push(element);
59. cout << "Production unlement :" << element << " Taille de la file :" << buffer.size() << endl;
60. lockBuffer.unlock ();
61. cndNotifierConsommateurs.notify_one ();
62. }
63.
64.});
65.
66.
67.std::cout << "Pour arreter pressez [ENTREZ]" << std::endl;
68.getchar();
69.
70.std::cout << "Arret demande" << endl;
71.ArretDemande=TRUE;
72.
73.ThreadProducteur.join();
74.ThreadConsommateur.join();
在示例中,该互斥体将传递给无信号使用锁() 方法。不过如果队列为空 ,就可以开始在执行序列中执行。
此互斥体用来保护尾 <int> 缓冲区类型。等待() 方法使用另一种机制将这挂起,并将等待唤醒,制造者线程仅当它将调用它的方法notify_one()。
使用这里的元素类型,递增 1 在单个原子操作中我们的队列的元素。在多线程的上下文,另外,例如将总是公平的保证元素操作,而不是抢占式。
头文件<future>。未来用于执行异步操作的返回结果,要检索后,没有不同步或线程流量控制机制。示例中,作为互斥体的多个线程的交会点的方法 join () 和控制流对象。
事实上,假设您想要简单的加法的两个整数 A + B,但是来自两个不同的线程所返回的结果。
在下面的示例中,作为不确定何时执行的概念
- 1.std::cout << "Thread Principale : ID : " << std::this_thread::get_id() << endl;
- 2. future<int> f1(async([]()->int
- 3. {
- 4. //Simule une charge
- 5. std::this_thread::sleep_for (chrono::milliseconds (2000));
- 6. std::cout << "Future 1 ID : " << std::this_thread::get_id() << endl;
- 7.
- 8. return 42;
- 9. }));
10. 10.
11. 11. future<int> f2(async([]()->int
12. 12. {
13. 13.
14. 14. std::cout << "Future 2 ID : " << std::this_thread::get_id() << endl;
15. 15.
16. 16. return 84;
17. 17. }));
18. 18.
19. 19. std::cout << "Resultat : " << f1.get () + f2.get() << endl ;
20. 20.
1.std::cout << "Thread Principale : ID : " << std::this_thread::get_id() << endl;
2. future<int> f1(async([]()->int
3. {
4. //Simule une charge
5. std::this_thread::sleep_for (chrono::milliseconds (2000));
6. std::cout << "Future 1 ID : " << std::this_thread::get_id() << endl;
7.
8. return 42;
9. }));
10.
11. future<int> f2(async([]()->int
12. {
13.
14. std::cout << "Future 2 ID : " << std::this_thread::get_id() << endl;
15.
16. return 84;
17. }));
18.
19. std::cout << "Resultat : " << f1.get () + f2.get() << endl ;
20.
在这里宣布int类型的两个数值以异步类型作为参数的构造函数,它作为其名称在不同的线程中执行异步操作的指示。
两个未来将返回的结果,但不知道何时执行Get ()方法,这是一个调用中担保两个整数的增加会正确的范例。
在将来的VS11调用中,我们使用语法强烈靠近同步语法的异步执行。
Visual Studio 2010以后有可能在 c + + 代码中,更具体地 STL 使用 Lambda 表达式 (匿名方法的窗体)。例如,看下面的代码执行,当使用这些类型的算法for_each、 parallel_for、 parallel_for_each等等的时候。
- 1.std::deque<int> d1;
- 2. d1.push_back (2);
- 3. d1.push_back (1);
- 4. d1.push_back(3);
- 5. d1.push_back(0);
- 6. auto a=d1.begin ();
- 7. auto b=d1.end ();
- 8. std::sort(a,b);
- 9.
10. 10. std::for_each (a,b,[](int i)
11. 11. {
12. 12. std::cout << i << std::endl;
13. 13. });
1.std::deque<int> d1;
2. d1.push_back (2);
3. d1.push_back (1);
4. d1.push_back(3);
5. d1.push_back(0);
6. auto a=d1.begin ();
7. auto b=d1.end ();
8. std::sort(a,b);
9.
10. std::for_each (a,b,[](int i)
11. {
12. std::cout << i << std::endl;
13. });
Lambda 这里开始用两个字符[]以指示我们捕获语法相对于没有本地变量[] 或 [=]或我们捕获所有的本地变量由引用或备份分别。做不捕获任何变量是 lambda 说是无限定的。
现在,这种类型的 lambda 隐式转换为函数指针,换句话说,成功调用旧的 Win32 API。
此处的示例与CreateThreadpoolWorkAPI,指向函数的指针参数 1,键入PTP_WORK_CALLBACK
,比原来的 lambda明显更好一些。
- 1.PTP_POOL pool=CreateThreadpool(NULL);
- 2. TP_CALLBACK_ENVIRON cbEnviron;
- 3. InitializeThreadpoolEnvironment(&cbEnviron);
- 4. SetThreadpoolThreadMaximum (pool,4);
- 5. BOOL bRet=SetThreadpoolThreadMinimum (pool,2);
- 6.
- 7.
- 8. PTP_WORK work=CreateThreadpoolWork([]( PTP_CALLBACK_INSTANCE Instance,PVOID Context,PTP_WORK Work)
- 9. {
10. 10.
11. 11. wprintf(L"Fait du boulot\n");
12. 12. },NULL,&cbEnviron);
13. 13. SubmitThreadpoolWork(work);
14. 14. WaitForThreadpoolWorkCallbacks(work,FALSE);
15. 15. CloseThreadpoolWork(work);
16. 16. CloseThreadpool(pool);
1.PTP_POOL pool=CreateThreadpool(NULL);
2. TP_CALLBACK_ENVIRON cbEnviron;
3. InitializeThreadpoolEnvironment(&cbEnviron);
4. SetThreadpoolThreadMaximum (pool,4);
5. BOOL bRet=SetThreadpoolThreadMinimum (pool,2);
6.
7.
8. PTP_WORK work=CreateThreadpoolWork([]( PTP_CALLBACK_INSTANCE Instance,PVOID Context,PTP_WORK Work)
9. {
10.
11. wprintf(L"Fait du boulot\n");
12. },NULL,&cbEnviron);
13. SubmitThreadpoolWork(work);
14. WaitForThreadpoolWorkCallbacks(work,FALSE);
15. CloseThreadpoolWork(work);
16. CloseThreadpool(pool);
另一个示例 API EnumWindows,或我们可以对"现代 c + +代码"混合使用旧 API 调用
- 1.BOOL ret=EnumWindows ([](HWND hwnd,LPARAM lParam)->BOOL
- 2. {
- 3. const size_t MAX_SIZE=2048;
- 4. LPWSTR title=static_cast<LPWSTR>(_malloca(MAX_SIZE));
- 5. if (title!=nullptr)
- 6. {
- 7. ZeroMemory (title,MAX_SIZE);
- 8. if (GetWindowTextLength (hwnd) >0)
- 9. {
10. 10. GetWindowTextW (hwnd,title,MAX_SIZE);
11. 11. wprintf(L"%ls\n",title);
12. 12. _freea(title);
13. 13. }
14. 14. }
15. 15.
16. 16. return TRUE;
17. 17. },0);
- 在编写多线程程序时,多个线程同时访问某个共享资源,会导致同步的问题,这篇文章中我们将介绍 C++11 多线程编程中的数据保护。
- 19. 数据丢失
- 让我们从一个简单的例子开始,请看如下代码:
01 |
#include <iostream> |
|
02 |
#include <string> |
03 |
#include <thread> |
04 |
#include <vector> |
05 |
||
06 |
using std::thread; |
07 |
using std::vector; |
|
08 |
using std::cout; |
09 |
using std::endl; |
|
10 |
11 |
class Incrementer |
|
12 |
{ |
13 |
private: |
|
14 |
int counter; |
15 |
||
16 |
public: |
17 |
Incrementer() : counter{0} { }; |
|
18 |
19 |
void operator()() |
|
20 |
{ |
21 |
for(int i = 0; i < 100000; i++) |
|
22 |
{ |
23 |
this->counter++; |
|
24 |
} |
25 |
} |
|
26 |
27 |
int getCounter() const |
|
28 |
{ |
29 |
return this->counter; |
|
30 |
} |
31 |
}; |
|
32 |
33 |
int main() |
|
34 |
{ |
35 |
// Create the threads which will each do some counting |
|
36 |
vector<thread> threads; |
37 |
||
38 |
Incrementer counter; |
39 |
||
40 |
threads.push_back(thread(std::ref(counter))); |
41 |
threads.push_back(thread(std::ref(counter))); |
42 |
threads.push_back(thread(std::ref(counter))); |
43 |
||
44 |
for(auto &t : threads) |
45 |
{ |
|
46 |
t.join(); |
47 |
} |
|
48 |
49 |
cout << counter.getCounter() << endl; |
|
50 |
51 |
return 0; |
|
52 |
} |
- 这个程序的目的就是数数,数到30万,某些傻叉程序员想要优化数数的过程,因此创建了三个线程,使用一个共享变量 counter,每个线程负责给这个变量增加10万计数。
- 这段代码创建了一个名为 Incrementer 的类,该类包含一个私有变量 counter,其构造器非常简单,只是将 counter 设置为 0.
- 紧接着是一个操作符重载,这意味着这个类的每个实例都是被当作一个简单函数来调用的。一般我们调用类的某个方法时会这样 object.fooMethod(),但现在你实际上是直接调用了对象,如 object(). 因为我们是在操作符重载函数中将整个对象传递给了线程类。最后是一个 getCounter 方法,返回 counter 变量的值。
- 再下来是程序的入口函数 main(),我们创建了三个线程,不过只创建了一个 Incrementer 类的实例,然后将这个实例传递给三个线程,注意这里使用了 std::ref ,这相当于是传递了实例的引用对象,而不是对象的拷贝。
- 现在让我们来看看程序执行的结果,如果这位傻叉程序员还够聪明的话,他会使用 GCC 4.7 或者更新版本,或者是 Clang 3.1 来进行编译,编译方法:
1 |
g++ -std=c++11 -lpthread -o threading_example main.cpp |
- 运行结果:
01 |
[lucas@lucas-desktop src]$ ./threading_example |
|
02 |
218141 |
03 |
[lucas@lucas-desktop src]$ ./threading_example |
|
04 |
208079 |
05 |
[lucas@lucas-desktop src]$ ./threading_example |
|
06 |
100000 |
07 |
[lucas@lucas-desktop src]$ ./threading_example |
|
08 |
202426 |
09 |
[lucas@lucas-desktop src]$ ./threading_example |
|
10 |
172209 |
- 但等等,不对啊,程序并没有数数到30万,有一次居然只数到10万,为什么会这样呢?好吧,加1操作对应实际的处理器指令其实包括:
1 |
movl counter(%rip), %eax |
|
2 |
addl $1, %eax |
3 |
movl %eax, counter(%rip) |
- 首个指令将装载 counter 的值到 %eax 寄存器,紧接着寄存器的值增1,然后将寄存器的值移给内存中 counter 所在的地址。
- 我听到你在嘀咕:这不错,可为什么会导致数数错误的问题呢?嗯,还记得我们以前说过线程会共享处理器,因为只有单核。因此在某些点上,一个线程会依照指令执行完成,但在很多情况下,操作系统会对线程说:时间结束了,到后面排队再来,然后另外一个线程开始执行,当下一个线程开始执行时,它会从被暂停的那个位置开始执行。所以你猜会发生什么事,当前线程正准备执行寄存器加1操作时,系统把处理器交给另外一个线程?
- 我真的不知道会发生什么事,可能我们在准备加1时,另外一个线程进来了,重新将 counter 值加载到寄存器等多种情况的产生。谁也不知道到底发生了什么。
- 63. 正确的做法
- 解决方案就是要求同一个时间内只允许一个线程访问共享变量。这个可通过 std::mutex 类来解决。当线程进入时,加锁、执行操作,然后释放锁。其他线程想要访问这个共享资源必须等待锁释放。
- 互斥(mutex) 是操作系统确保锁和解锁操作是不可分割的。这意味着线程在对互斥量进行锁和解锁的操作是不会被中断的。当线程对互斥量进行锁或者解锁时,该操作会在操作系统切换线程前完成。
- 而最好的事情是,当你试图对互斥量进行加锁操作时,其他的线程已经锁住了该互斥量,那你就必须等待直到其释放。操作系统会跟踪哪个线程正在等待哪个互斥量,被堵塞的线程会进入 "blocked on m" 状态,意味着操作系统不会给这个堵塞的线程任何处理器时间,直到互斥量解锁,因此也不会浪费 CPU 的循环。如果有多个线程处于等待状态,哪个线程最先获得资源取决于操作系统本身,一般像 Windows 和 Linux 系统使用的是 FIFO 策略,在实时操作系统中则是基于优先级的。
- 现在让我们对上面的代码进行改进:
01 |
#include <iostream> |
|
02 |
#include <string> |
03 |
#include <thread> |
04 |
#include <vector> |
05 |
#include <mutex> |
|
06 |
07 |
using std::thread; |
08 |
using std::vector; |
09 |
using std::cout; |
10 |
using std::endl; |
11 |
using std::mutex; |
|
12 |
13 |
class Incrementer |
|
14 |
{ |
15 |
private: |
|
16 |
int counter; |
17 |
mutex m; |
|
18 |
19 |
public: |
|
20 |
Incrementer() : counter{0} { }; |
21 |
||
22 |
void operator()() |
23 |
{ |
|
24 |
for(int i = 0; i < 100000; i++) |
25 |
{ |
|
26 |
this->m.lock(); |
27 |
this->counter++; |
|
28 |
this->m.unlock(); |
29 |
} |
30 |
} |
31 |
||
32 |
int getCounter() const |
33 |
{ |
|
34 |
return this->counter; |
35 |
} |
|
36 |
}; |
37 |
||
38 |
int main() |
39 |
{ |
|
40 |
// Create the threads which will each do some counting |
41 |
vector<thread> threads; |
|
42 |
43 |
Incrementer counter; |
|
44 |
45 |
threads.push_back(thread(std::ref(counter))); |
46 |
threads.push_back(thread(std::ref(counter))); |
47 |
threads.push_back(thread(std::ref(counter))); |
|
48 |
49 |
for(auto &t : threads) |
|
50 |
{ |
51 |
t.join(); |
|
52 |
} |
53 |
||
54 |
cout << counter.getCounter() << endl; |
55 |
||
56 |
return 0; |
57 |
} |
- 注意代码上的变化:我们引入了 mutex 头文件,增加了一个 m 的成员,类型是 mutex,在 operator()() 中我们锁住互斥量 m 然后对 counter 进行加1操作,然后释放互斥量。
- 再次执行上述程序,结果如下:
1 |
[lucas@lucas-desktop src]$ ./threading_example |
|
2 |
300000 |
3 |
[lucas@lucas-desktop src]$ ./threading_example |
|
4 |
300000 |
- 这下数对了。不过在计算机科学中,没有免费的午餐,使用互斥量会降低程序的性能,但这总比一个错误的程序要强吧。
- 100. 防范异常
- 当对变量进行加1操作时,是可能会发生异常的,当然在我们这个例子中发生异常的机会微乎其微,但是在一些复杂系统中是极有可能的。上面的代码并不是异常安全的,当异常发生时,程序已经结束了,可是互斥量还是处于锁的状态。
- 为了确保互斥量在异常发生的情况下也能被解锁,我们需要使用如下代码:
01 |
for(int i = 0; i < 100000; i++) |
|
02 |
{ |
03 |
this->m.lock(); |
|
04 |
try |
05 |
{ |
|
06 |
this->counter++; |
07 |
this->m.unlock(); |
|
08 |
} |
09 |
catch(...) |
|
10 |
{ |
11 |
this->m.unlock(); |
|
12 |
throw; |
13 |
} |
14 |
} |
- 但是,这代码太多了,而只是为了对互斥量进行加锁和解锁。没关系,我知道你很懒,因此推荐个更简单的单行代码解决方法,就是使用 std::lock_guard 类。这个类在创建时就锁定了 mutex 对象,然后在结束时释放。
继续修改代码:
01 |
void operator()() |
|
02 |
{ |
03 |
for(int i = 0; i < 100000; i++) |
|
04 |
{ |
05 |
lock_guard<mutex> lock(this->m); |
|
06 |
07 |
// The lock has been created now, and immediatly locks the mutex |
|
08 |
this->counter++; |
09 |
||
10 |
// This is the end of the for-loop scope, and the lock will be |
11 |
// destroyed, and in the destructor of the lock, it will |
|
12 |
// unlock the mutex |
13 |
} |
14 |
} |
上面代码已然是异常安全了,因为当异常发生时,将会调用 lock 对象的析构函数,然后自动进行互斥量的解锁。
- 记住,请使用放下代码模板来编写:
01 |
void long_function() |
|
02 |
{ |
03 |
// some long code |
|
04 |
05 |
// Just a pair of curly braces |
|
06 |
{ |
07 |
// Temp scope, create lock |
|
08 |
lock_guard<mutex> lock(this->m); |
09 |
||
10 |
// do some stuff |
11 |
||
12 |
// Close the scope, so the guard will unlock the mutex |
13 |
} |
14 |
} |
来源:https://www.cnblogs.com/codeword/archive/2012/10/23/2735283.html