Praktik Terbaik Umum dalam Runtime Konkurensi

Dokumen ini menjelaskan praktik terbaik yang berlaku untuk beberapa area Runtime Konkurensi.

Bagian

Dokumen ini berisi bagian berikut:

Gunakan Konstruksi Sinkronisasi Koperasi Jika Memungkinkan

Runtime Konkurensi menyediakan banyak konstruksi yang aman konkurensi yang tidak memerlukan objek sinkronisasi eksternal. Misalnya, kelas konkurensi::concurrent_vector menyediakan operasi akses tambahan dan elemen yang aman konkurensi. Di sini, konkurensi-aman berarti pointer atau iterator selalu valid. Ini bukan jaminan inisialisasi elemen, atau urutan traversal tertentu. Namun, untuk kasus di mana Anda memerlukan akses eksklusif ke sumber daya, runtime menyediakan konkurensi::critical_section, konkurensi::reader_writer_lock, dan konkurensi::kelas peristiwa . Jenis-jenis ini berperilaku kooperatif; oleh karena itu, penjadwal tugas dapat merealokasi sumber daya pemrosesan ke konteks lain saat tugas pertama menunggu data. Jika memungkinkan, gunakan jenis sinkronisasi ini alih-alih mekanisme sinkronisasi lainnya, seperti yang disediakan oleh Windows API, yang tidak berperilaku kooperatif. Untuk informasi selengkapnya tentang jenis sinkronisasi ini dan contoh kode, lihat Struktur Data Sinkronisasi dan Membandingkan Struktur Data Sinkronisasi dengan API Windows.

[Atas]

Hindari Tugas Panjang yang Tidak Menghasilkan

Karena penjadwal tugas berperilaku kooperatif, penjadwal tugas tidak memberikan kewajaran di antara tugas. Oleh karena itu, tugas dapat mencegah tugas lain dimulai. Meskipun ini dapat diterima dalam beberapa kasus, dalam kasus lain ini dapat menyebabkan kebuntuan atau kelaparan.

Contoh berikut melakukan lebih banyak tugas daripada jumlah sumber daya pemrosesan yang dialokasikan. Tugas pertama tidak menghasilkan penjadwal tugas dan oleh karena itu tugas kedua tidak dimulai sampai tugas pertama selesai.

// cooperative-tasks.cpp
// compile with: /EHsc
#include <ppl.h>
#include <iostream>
#include <sstream>

using namespace concurrency;
using namespace std;

// Data that the application passes to lightweight tasks.
struct task_data_t
{
   int id;  // a unique task identifier.
   event e; // signals that the task has finished.
};

// A lightweight task that performs a lengthy operation.
void task(void* data)
{   
   task_data_t* task_data = reinterpret_cast<task_data_t*>(data);

   // Create a large loop that occasionally prints a value to the console.
   int i;
   for (i = 0; i < 1000000000; ++i)
   {
      if (i > 0 && (i % 250000000) == 0)
      {
         wstringstream ss;
         ss << task_data->id << L": " << i << endl;
         wcout << ss.str();
      }
   }
   wstringstream ss;
   ss << task_data->id << L": " << i << endl;
   wcout << ss.str();

   // Signal to the caller that the thread is finished.
   task_data->e.set();
}

int wmain()
{
   // For illustration, limit the number of concurrent 
   // tasks to one.
   Scheduler::SetDefaultSchedulerPolicy(SchedulerPolicy(2, 
      MinConcurrency, 1, MaxConcurrency, 1));

   // Schedule two tasks.

   task_data_t t1;
   t1.id = 0;
   CurrentScheduler::ScheduleTask(task, &t1);

   task_data_t t2;
   t2.id = 1;
   CurrentScheduler::ScheduleTask(task, &t2);

   // Wait for the tasks to finish.

   t1.e.wait();
   t2.e.wait();
}

Contoh ini menghasilkan output berikut:

1: 250000000 1: 500000000 1: 750000000 1: 1000000000 2: 250000000 2: 500000000 2: 750000000 2: 1000000000

Ada beberapa cara untuk mengaktifkan kerja sama antara kedua tugas. Salah satu caranya adalah dengan kadang-kadang menghasilkan penjadwal tugas dalam tugas yang berjalan lama. Contoh berikut memodifikasi task fungsi untuk memanggil metode konkurensi::Context::Yield untuk menghasilkan eksekusi ke penjadwal tugas sehingga tugas lain dapat berjalan.

// A lightweight task that performs a lengthy operation.
void task(void* data)
{   
   task_data_t* task_data = reinterpret_cast<task_data_t*>(data);

   // Create a large loop that occasionally prints a value to the console.
   int i;
   for (i = 0; i < 1000000000; ++i)
   {
      if (i > 0 && (i % 250000000) == 0)
      {
         wstringstream ss;
         ss << task_data->id << L": " << i << endl;
         wcout << ss.str();

         // Yield control back to the task scheduler.
         Context::Yield();
      }
   }
   wstringstream ss;
   ss << task_data->id << L": " << i << endl;
   wcout << ss.str();

   // Signal to the caller that the thread is finished.
   task_data->e.set();
}

Contoh ini menghasilkan output berikut:

1: 250000000
2: 250000000
1: 500000000
2: 500000000
1: 750000000
2: 750000000
1: 1000000000
2: 1000000000

Metode ini Context::Yield hanya menghasilkan utas aktif lain pada penjadwal tempat utas saat ini berada, tugas ringan, atau utas sistem operasi lainnya. Metode ini tidak menghasilkan pekerjaan yang dijadwalkan untuk berjalan dalam konkurensi::task_group atau konkurensi::structured_task_group objek tetapi belum dimulai.

Ada cara lain untuk mengaktifkan kerja sama di antara tugas yang berjalan lama. Anda dapat memecah tugas besar menjadi subtugas yang lebih kecil. Anda juga dapat mengaktifkan oversubscription selama tugas yang panjang. Oversubscription memungkinkan Anda membuat lebih banyak utas daripada jumlah utas perangkat keras yang tersedia. Oversubscription sangat berguna ketika tugas yang panjang berisi sejumlah besar latensi, misalnya, membaca data dari disk atau dari koneksi jaringan. Untuk informasi selengkapnya tentang tugas ringan dan oversubscription, lihat Penjadwal Tugas.

[Atas]

Gunakan Oversubscription untuk Mengimbangi Operasi yang Memblokir atau Memiliki Latensi Tinggi

Runtime Konkurensi menyediakan primitif sinkronisasi, seperti konkurensi::critical_section, yang memungkinkan tugas untuk secara kooperatif memblokir dan menghasilkan satu sama lain. Ketika satu tugas secara kooperatif memblokir atau menghasilkan, penjadwal tugas dapat merealokasi sumber daya pemrosesan ke konteks lain saat tugas pertama menunggu data.

Ada kasus di mana Anda tidak dapat menggunakan mekanisme pemblokiran kooperatif yang disediakan oleh Concurrency Runtime. Misalnya, pustaka eksternal yang Anda gunakan mungkin menggunakan mekanisme sinkronisasi yang berbeda. Contoh lain adalah ketika Anda melakukan operasi yang dapat memiliki latensi dalam jumlah tinggi, misalnya, saat Anda menggunakan fungsi Windows API ReadFile untuk membaca data dari koneksi jaringan. Dalam kasus ini, oversubscription dapat mengaktifkan tugas lain untuk dijalankan ketika tugas lain menganggur. Oversubscription memungkinkan Anda membuat lebih banyak utas daripada jumlah utas perangkat keras yang tersedia.

Pertimbangkan fungsi berikut, download, yang mengunduh file di URL yang diberikan. Contoh ini menggunakan metode konkurensi::Context::Oversubscribe untuk meningkatkan jumlah utas aktif untuk sementara.

// Downloads the file at the given URL.
string download(const string& url)
{
   // Enable oversubscription.
   Context::Oversubscribe(true);

   // Download the file.
   string content = GetHttpFile(_session, url.c_str());
   
   // Disable oversubscription.
   Context::Oversubscribe(false);

   return content;
}

GetHttpFile Karena fungsi melakukan operasi yang berpotensi laten, oversubscription dapat memungkinkan tugas lain berjalan saat tugas saat ini menunggu data. Untuk versi lengkap contoh ini, lihat Cara: Menggunakan Oversubscription untuk Mengimbangi Latensi.

[Atas]

Gunakan Fungsi Manajemen Memori Bersamaan Jika Memungkinkan

Gunakan fungsi manajemen memori, konkurensi::Alokasi dan konkurensi::Gratis, ketika Anda memiliki tugas terperinci yang sering mengalokasikan objek kecil yang memiliki masa pakai yang relatif singkat. Runtime Konkurensi menyimpan cache memori terpisah untuk setiap utas yang sedang berjalan. Fungsi Alloc dan Free mengalokasikan dan membebaskan memori dari cache ini tanpa menggunakan kunci atau penghalang memori.

Untuk informasi selengkapnya tentang fungsi manajemen memori ini, lihat Penjadwal Tugas. Untuk contoh yang menggunakan fungsi-fungsi ini, lihat Cara: Menggunakan Alokasi dan Gratis untuk Meningkatkan Performa Memori.

[Atas]

Menggunakan RAII untuk Mengelola Objek Konkurensi Seumur Hidup

Runtime Konkurensi menggunakan penanganan pengecualian untuk menerapkan fitur seperti pembatalan. Oleh karena itu, tulis kode pengecualian-aman saat Anda memanggil runtime atau memanggil pustaka lain yang memanggil ke dalam runtime.

Pola Resource Acquisition Is Initialization (RAII) adalah salah satu cara untuk mengelola masa pakai objek konkurensi dengan aman di bawah cakupan tertentu. Di bawah pola RAII, struktur data dialokasikan pada tumpukan. Struktur data tersebut menginisialisasi atau memperoleh sumber daya saat dibuat dan menghancurkan atau merilis sumber daya tersebut saat struktur data dihancurkan. Pola RAII menjamin bahwa destruktor dipanggil sebelum cakupan penutup keluar. Pola ini berguna ketika fungsi berisi beberapa return pernyataan. Pola ini juga membantu Anda menulis kode yang aman pengecualian. throw Ketika pernyataan menyebabkan tumpukan melepas lelah, destruktor untuk objek RAII dipanggil; oleh karena itu, sumber daya selalu dihapus atau dirilis dengan benar.

Runtime menentukan beberapa kelas yang menggunakan pola RAII, misalnya, konkurensi::critical_section::scoped_lock dan konkurensi::reader_writer_lock::scoped_lock. Kelas pembantu ini dikenal sebagai kunci terlingkup. Kelas-kelas ini memberikan beberapa manfaat saat Anda bekerja dengan objek konkurensi::critical_section atau konkurensi::reader_writer_lock . Konstruktor kelas-kelas ini memperoleh akses ke objek atau reader_writer_lock yang disediakancritical_section; destruktor melepaskan akses ke objek tersebut. Karena kunci tercakup melepaskan akses ke objek pengecualian bersamanya secara otomatis ketika dihancurkan, Anda tidak membuka kunci objek yang mendasar secara manual.

Pertimbangkan kelas berikut, account, yang ditentukan oleh pustaka eksternal dan oleh karena itu tidak dapat dimodifikasi.

// account.h
#pragma once
#include <exception>
#include <sstream>

// Represents a bank account.
class account
{
public:
   explicit account(int initial_balance = 0)
      : _balance(initial_balance)
   {
   }

   // Retrieves the current balance.
   int balance() const
   {
      return _balance;
   }

   // Deposits the specified amount into the account.
   int deposit(int amount)
   {
      _balance += amount;
      return _balance;
   }

   // Withdraws the specified amount from the account.
   int withdraw(int amount)
   {
      if (_balance < 0)
      {
         std::stringstream ss;
         ss << "negative balance: " << _balance << std::endl;
         throw std::exception((ss.str().c_str()));
      }

      _balance -= amount;
      return _balance;
   }

private:
   // The current balance.
   int _balance;
};

Contoh berikut melakukan beberapa transaksi pada account objek secara paralel. Contoh menggunakan critical_section objek untuk menyinkronkan akses ke account objek karena account kelas tidak aman konkurensi. Setiap operasi paralel menggunakan critical_section::scoped_lock objek untuk menjamin bahwa critical_section objek tidak terkunci ketika operasi berhasil atau gagal. Ketika saldo akun negatif, withdraw operasi gagal dengan melemparkan pengecualian.

// account-transactions.cpp
// compile with: /EHsc
#include "account.h"
#include <ppl.h>
#include <iostream>
#include <sstream>

using namespace concurrency;
using namespace std;

int wmain()
{
   // Create an account that has an initial balance of 1924.
   account acc(1924);

   // Synchronizes access to the account object because the account class is 
   // not concurrency-safe.
   critical_section cs;

   // Perform multiple transactions on the account in parallel.   
   try
   {
      parallel_invoke(
         [&acc, &cs] {
            critical_section::scoped_lock lock(cs);
            wcout << L"Balance before deposit: " << acc.balance() << endl;
            acc.deposit(1000);
            wcout << L"Balance after deposit: " << acc.balance() << endl;
         },
         [&acc, &cs] {
            critical_section::scoped_lock lock(cs);
            wcout << L"Balance before withdrawal: " << acc.balance() << endl;
            acc.withdraw(50);
            wcout << L"Balance after withdrawal: " << acc.balance() << endl;
         },
         [&acc, &cs] {
            critical_section::scoped_lock lock(cs);
            wcout << L"Balance before withdrawal: " << acc.balance() << endl;
            acc.withdraw(3000);
            wcout << L"Balance after withdrawal: " << acc.balance() << endl;
         }
      );
   }
   catch (const exception& e)
   {
      wcout << L"Error details:" << endl << L"\t" << e.what() << endl;
   }
}

Contoh ini menghasilkan contoh output berikut:

Balance before deposit: 1924
Balance after deposit: 2924
Balance before withdrawal: 2924
Balance after withdrawal: -76
Balance before withdrawal: -76
Error details:
    negative balance: -76

Untuk contoh tambahan yang menggunakan pola RAII untuk mengelola masa pakai objek konkurensi, lihat Panduan: Menghapus Pekerjaan dari Utas Antarmuka Pengguna, Cara: Menggunakan Kelas Konteks untuk Menerapkan Semaphore Kooperatif, dan Cara: Menggunakan Oversubscription untuk Mengimbangi Latensi.

[Atas]

Jangan Buat Objek Konkurensi di Cakupan Global

Saat Anda membuat objek konkurensi pada cakupan global, Anda dapat menyebabkan masalah seperti kebuntuan atau pelanggaran akses memori terjadi di aplikasi Anda.

Misalnya, saat Anda membuat objek Runtime Konkurensi, runtime membuat penjadwal default untuk Anda jika belum dibuat. Objek runtime yang dibuat selama konstruksi objek global akan menyebabkan runtime membuat penjadwal default ini. Namun, proses ini mengambil kunci internal, yang dapat mengganggu inisialisasi objek lain yang mendukung infrastruktur Concurrency Runtime. Kunci internal ini mungkin diperlukan oleh objek infrastruktur lain yang belum diinisialisasi, dan dengan demikian dapat menyebabkan kebuntuan terjadi di aplikasi Anda.

Contoh berikut menunjukkan pembuatan konkurensi global ::Objek scheduler . Pola ini tidak hanya berlaku untuk Scheduler kelas tetapi semua jenis lain yang disediakan oleh Concurrency Runtime. Kami menyarankan agar Anda tidak mengikuti pola ini karena dapat menyebabkan perilaku tak terduga dalam aplikasi Anda.

// global-scheduler.cpp
// compile with: /EHsc
#include <concrt.h>

using namespace concurrency;

static_assert(false, "This example illustrates a non-recommended practice.");

// Create a Scheduler object at global scope.
// BUG: This practice is not recommended because it can cause deadlock.
Scheduler* globalScheduler = Scheduler::Create(SchedulerPolicy(2,
   MinConcurrency, 2, MaxConcurrency, 4));

int wmain() 
{   
}

Untuk contoh cara yang benar untuk membuat Scheduler objek, lihat Penjadwal Tugas.

[Atas]

Jangan Gunakan Objek Konkurensi di Segmen Data Bersama

Runtime Konkurensi tidak mendukung penggunaan objek konkurensi di bagian data bersama, misalnya, bagian data yang dibuat oleh arahan data_seg#pragma . Objek konkurensi yang dibagikan di seluruh batas proses dapat menempatkan runtime dalam status tidak konsisten atau tidak valid.

[Atas]

Baca juga

Praktik Terbaik Runtime Konkurensi
Parallel Patterns Library (PPL)
Pustaka Agen Asinkron
Tugas Microsoft Azure Scheduler
Struktur Data Sinkronisasi
Membandingkan Struktur Data Sinkronisasi dengan WINDOWS API
Cara: Gunakan Alokasi dan Gratis untuk Meningkatkan Performa Memori
Cara: Menggunakan Oversubscription untuk Mengimbangi Latensi
Cara: Menggunakan Kelas Konteks untuk Menerapkan Koperasi Semaphore
Panduan: Menghapus Pekerjaan dari Utas Antarmuka Pengguna
Praktik Terbaik di Pustaka Pola Paralel
Praktik Terbaik di Pustaka Agen Asinkron